简单单点登录(SSO)解决方案_.NET_编程开发_程序员俱乐部

中国优秀的程序员网站程序员频道CXYCLUB技术地图
热搜:
更多>>
 
您所在的位置: 程序员俱乐部 > 编程开发 > .NET > 简单单点登录(SSO)解决方案

简单单点登录(SSO)解决方案

 2013/12/12 23:09:27  阿蛆  博客园  我要评论(0)
  • 摘要:先上渣图一张:再上伪代码。这个方案跨域只用到了jquery的jsonp跨域,利用一次性密钥简单有限的解决了安全问题,如果特别严谨需要扩展。这里代码用的是mvc3框架。授权站登陆页面代码:[HttpGet]publicActionResultLogin(stringReturnUrl){if(base.CurrentUser!=null&&!string.IsNullOrEmpty(ReturnUrl))//已经登录{returnRedirect(SSOUrl(ReturnUrl
  • 标签:解决方案 解决 单点登录 SSO

先上渣图一张:

 

再上伪代码。这个方案跨域只用到了jquery的jsonp跨域,利用一次性密钥简单有限的解决了安全问题,如果特别严谨需要扩展。

这里代码用的是mvc3框架。

授权站登陆页面代码:

[HttpGet]
public ActionResult Login(string ReturnUrl)
{
    if (base.CurrentUser != null && !string.IsNullOrEmpty(ReturnUrl))//已经登录
    {
        return Redirect(SSOUrl(ReturnUrl));//生成密钥并返回
    }
    //...返回登录页面,正常流程。
}

//登录提交逻辑
[HttpPost]
public ActionResult Login(string name, string pwd, string ReturnUrl)
{
    if(//登录成功 && 分站的请求)
    {
        ReturnUrl = SSOUrl(ReturnUrl);
    }
    ReturnUrl = string.IsNullOrWhiteSpace(ReturnUrl) ? FormsAuthentication.DefaultUrl : ReturnUrl;
    return Redirect(ReturnUrl);
}

 

这里的代码很重要:
1:定义一个SSO类
2:检测到登录状态或者登录成功即生成一次性密钥以参数形式返回给分站。
3:如何生成密钥。我这里用的缓存来存储(可以用其它方式代替),只要有请求,每次都生成密钥(这里new了一个GUID),把密钥和当前用户的令牌ID存储起来(可以看做是键值对形式),放入缓存。
4:关于token,我这里比较简单的在User表中放了一个单一的tokenId列(仅仅在user对象发生变化的时候,比如修改密码,更新token,这样就会使这个user的所有登录客户端全部失效),考虑到更加严谨可以把用户ID和token做成一对多的关系,动态生成token分发给不同的登录对象,这里就不深究了。


class
SSO { public Guid Cert { get; set; }//一次性密钥 public Guid Token { get; set; }//分站令牌 public DateTime Time { get; set; }//过期时间 } private string SSOUrl(string url) { var sso = HttpContext.Cache["SSO"] == null ? new List<SSO>() : (List<SSO>)HttpContext.Cache["SSO"]; Guid cert = Guid.NewGuid(); sso.Add(new SSO { Cert = cert, Token = base.CurrentUser.Token, Time = DateTime.Now.AddHours(3) }); HttpContext.Cache["SSO"] = sso; if (!string.IsNullOrWhiteSpace(url)) { if (url.Contains('?')) { var args = url.Substring(url.IndexOf('?') + 1).Split('&'); url = url.Substring(0, url.IndexOf('?') + 1); foreach (var arg in args) { if (!arg.StartsWith("cert=")) { url += arg; } } url += "cert=" + cert; } else { url += "?cert=" + cert; } } return url; }

 这样登录的功能就完成了。

下面是授权站的jsonp接口:一个返回token,一个返回用户信息。

刚才生成的缓存在这里进行维护,通过一次性密钥从缓存中获取token,返回给分站,并把过期的缓存条目和密钥删除掉。

正常情况下密钥生成后马上就会被获取->删除,条目的过期时间其实设置成几分钟就行了,这个缓存中的数据也不会积累

PS:

1 为什么要返回密钥而不是直接返回token,因为密钥是url形式返回的,如果其它人得到这个链接会有安全问题,所以生成一个url请求一次token马上失效,提升一下安全度。

2 JsonpResult(即jsonp服务端实现)需要自己扩展。


//根据一次性密钥返回token
public
JsonpResult Token(Guid cert) { var sso = HttpContext.Cache["SSO"] == null ? new List<SSO>() : (List<SSO>)HttpContext.Cache["SSO"]; sso.RemoveAll(p => p.Time < DateTime.Now); var single = sso.SingleOrDefault(p => p.Cert == cert); sso.Remove(single); HttpContext.Cache["SSO"] = sso; if (null != single) return Jsonp(new { State = 1, Token = single.Token }); return Jsonp(new { State = 0 }); }
//根据token查询用户信息
public JsonpResult Identity(Guid token) { var user = UserInfo.SingleOrDefault(token); return Jsonp(new { State = 1, Result = user }); }

 

下面上分站代码:

后台代码:

1 将分站的token返回给页面供js调用。

2 一个ajax接口存储token。

public ActionResult Index()
{
    ViewBag.Token = Session["SSO_Token"];

    return View();
}

public JsonResult UpdateToken(Guid token)
{
    Session["SSO_Token"] = token;
    return Json(new { State = 1 });
}

分站页面js:

http://localhost:8001 是我本地的授权站。

<script type="text/javascript">
  /*url参数辅助方法,此处用来获取url回传的一次性密钥参数值*/ function querystring(key, val) { var s = window.location.search.substr(1).split('&'); var obj = {}; for (var k in s) { if (s[k].indexOf('=') >= 0) { var temp = s[k].split('='); obj[temp[0]] = temp[1]; } } if (arguments.length >= 2) { obj[key] = val; s = ""; for (var o in obj) { s = s + '&' + o + '=' + obj[o]; } return s.replace('&', '?'); } else { return obj[key]; } }   
  
  /*用token跨域获取用户信息*/
function identityByToken(token) { $.getJSON('http://localhost:8001/SSO/Identity?callback=?', { token: token }, function (d) { alert(d.Result.Name + "检测到SSO登录");
       //...todo }); }   
  
  /*用密钥获取用户信息(先跨域获取token,然后调用token获取)*/
function identityByCert(cert) { $.getJSON("http://localhost:8001/SSO/Token?callback=?", { cert: cert }, function (d) { if (d.State == 1) { $.post("/Home/UpdateToken?token=" + d.Token);//成功获取token,ajax提交给分站后台存储,下次访问直接可获取到token identityByToken(d.Token); } else {//失败则跳转至授权登录页获取一次性密钥 window.location = 'http://localhost:8001/Account/Login?returnurl=' + window.location; } }); } if (!!'@(ViewBag.Token)') {//有令牌 identityByToken('@(ViewBag.Token)'); } else { if (!!querystring('cert')) {//有一次性key identityByCert(querystring('cert')); } else {//失败则跳转至授权登陆页(注意将本页url作为returnurl参数传递给了授权页,授权登录页经过处理会把密钥作为参数回传重定向到本页面) window.location = 'http://localhost:8001/Account/Login?returnurl=' + window.location; } } </script>

 

完成。下面是一大篇渣渣截图,可略过。


 

看看效果:

此时主站未登录

 

访问本地分站:192.168.2.31:8010

直接跳转到了登录页,注意returnurl参数:

 

1 登录成功

2 跳转回分站并带回密钥

3 通过密钥跨域获取token

4 ajax提交token给分站后台存储

4 通过token跨域获取user(弹窗提示)

 

ps:这个带cert参数的url经过这次请求以后就失效了,所以别人得到这个url访问也不会得到对应的token。

 

再重新访问一次分站点:

可以看到由于分站已经有了token,所以只有一个请求了。

PS:实际应用中会把登陆user一些常用信息存储在分站点,直到过期才会去主站请求,不用每次都用token去请求。

 

此时主站已经登录,我懒得再弄一个分站B来测试了,这里模拟一下:

最后模拟一下用户已经登录,但是分站未登录的情况。

先清空分站session,然后登录主站,再访问分站192.168.2.31:8010

结果直接弹出了登录信息。

 

梳理一下:

1 主站的功能就是通过token提供用户信息给分站,只要分站有token就提供(注意用户的token虽然是可变的,但仍有一些安全隐患,解决方案前面提过用分发的方式,我这里没有具体实现)。

2 主站的接口可以通过设置分站列表,验证访问来源来过滤一些非法请求,提高安全度。

3 分站如何获得token。只能通过主站给的密钥获得,密钥都是一次性且有时限的。

4 分站如何获得密钥。通过访问主站的登录页面,主站重定向将密钥用url方式返回。

 

 

最后的最后,关于注销

思路:

1 在主站设一个注销接口,任何分站注销都去访问主站的注销。

2 主站的注销清空所有分站的登录信息(这里为session)。

3 如何跨域清空?百度了一下,有个方式就是实现一个主站的注销页面,页面上动态创建多个iframe(有多少个分站就创建多少个)来注销分站登录信息。

 

我这没具体实现的原因是:

项目不大,只是要集成一下原来的项目。

本来就不需要很严谨的一个应用,我把分站上的session时间设置的很短,分站不会有很长的退出延时。

最重要的原因是懒......

发表评论
用户名: 匿名