(十三)总结 《SSO 及应用案例》
先说说 SSO(单点登录)这种产物是怎么来的? 场景是这样的假设一个大型应用平台(web)下面有几个模块比如:商城,机票,酒店。
我上商城时候我没有登录,则登录一下,而又从商城跳转到酒店,发现酒店没登录则又需要登录一下酒店网站。
这里人们就会想有没有,我在我当前域名下一处登录,就可以在当前域名(子域名)随处浏览以及操作,所以这时候就诞生了 SSO。SSO 不单单解决了我们 "一处登录,到处操作" 的问题以外,还省去了我们每个项目开发登录模块的时间。SSO 的基本原理如下:
SSO 虽然叫单点登录,但是我们更愿意叫他 "统一登录中心",因为的他的职责就是承担了所有的登录工作,虽然 SSO 在 Web 时代很盛行,但是在 APP 时代
就又有很大的不同了,这个我后面会讲到。这里先讲一下上面一张图中 SSO 以及客户端分别是做了哪些事情
这里画的还是比较抽象,有几点可以说一下:
1,上图中应用里用 Session 存储用户信息,有的做法是用 Cookie。这里用 Session 或 Cookie 都可以,但是 Cookie 本身存在客户端浏览器中,
所以从安全性上来说不如 Session,Cookie 的优点是不会随浏览器的关闭而销毁,下次访问网站时可以无需登录。这里各取所需,我们从安全性
上考虑说选择了用 Session。
2,关于创建 Ticket 后将 Ticket 传给子站。Ticket 叫做 "令牌",本身包含用户的基础信息(比如账号、用户名)还有子站要访问的页面地址,以及过期时间等等。
Ticket 要回传给子站有的是直接往 SSO 站的 Cookie 里面存,然后子站通过设置 domain 参数共享 cookie 读写。这个本身没有问题但是还是个第一点一样。
把握好安全性就行。
3,图一我画的是用 cookie 的方式,尽管本地有用户信息(Ticket)但是为是安全还是要上 SSO 上请验证一下,登录是否过期。这种做法有的甚至每个页面都去
请求 SSO 看是否有过期,过期了则退出登录,这个是根据业务需求的不同做的。比如邮箱,没操作一次授权时间加长 10 分钟,如果十分钟没有任何操作
再操作的时候就被退出了。这个看具体应用,用法不同而已。
4,如果客户端(浏览器)禁用 Cookie 那 Session 是拿不到的。这个其实都知道每个浏览器的 Session_Id 不同,Session 本身是键值对,但是唯一性标识
不是 Session 的 key,是 Session_id,而 session_id 是保存在浏览器的 Cookie 中的,其实就等于禁用了 Cookie,Session 也废了。============================ 华丽的分割线 =================================== 接下来要说重点了,其实在上一篇 《理解 Oauth2.0》 中就讲到了很多和 SSO 类似的概念,其实两者本质是一样的。但是我们也可以
分开来看。我就更习惯分开来看,我的理解是这样的,我认为 OAuth 更关注的是 "授权",SSO 则侧重是 "登录"。
所以从概念上来说,OAuth 的设计天生就不用去关注比如跨域这样的问题,SSO 则更多是本平台下一站登录,随处操作。前期我们 Winner 框架中是 SSO 来扩展 OAuth,今年 Jason 重构了一个版本则是 OAuth 来兼任 SSO。这里没有好坏技术高低之分,只是场景不同。
现在基本是一个 APP 的时代,所以 SSO 的功能被弱化了,更多时候我们使用 APP 就没有一个所谓的 "一处登录,随处操作" 的说法,就一个登录。我们来看看 Winner 中的核心代码:
首先,我在前面讲《 Winner.FrameWork.MVC 》 的时候有说到,以前我们使用基类去验证用户是否登录,而现在我们使用更灵活的特性类去处理
我们 Winner 中特性类的验证最常用的是 [AuthLogin] 和 [AuthRight] 两者的不同在于 [AuthLogin] 只验证是否有登录,没有登录就去登录。
意思就是说该页面所有人都有权限访问,前提是有注册。而 [AuthRight] 则不单单是验证了是否登录,还验证了是否有权限访问本页面。
关于权限那一块,在后面的文章中我再单独讲权限系统时再细讲。我们重点来看一下 [AuthLogin] 的核心代码:
我们看到一开始我们有 base(ignore);这个我在前面的篇章中有讲到过,这个可以通过配置文件配置,目的是省去我们每个项目开发的时候都要去登录。
- using System;
- using System.Collections.Generic;
- using System.Dynamic;
- using System.Linq;
- using System.Net;
- using System.Web;
- using System.Web.Mvc;
- using System.Xml;
- using Winner.Framework.MVC.Attribute;
- using Winner.Framework.MVC.GlobalContext;
- using Winner.Framework.MVC.Models;
- using Winner.Framework.MVC.Models.Account;
- using Winner.Framework.Utils;
- namespace Winner.Framework.MVC
- {
- /// <summary>
- /// PC Web用户登陆检查
- /// </summary>
- public class AuthLoginAttribute : AuthorizationFilterAttribute
- {
- /// <summary>
- /// 实例化一个新的验证对象
- /// </summary>
- /// <param name="ignore">是否忽略检查</param>
- public AuthLoginAttribute(bool ignore = false)
- : base(ignore)
- {
- }
- /// <summary>
- /// 登陆验证
- /// </summary>
- /// <param name="context">当前上下文</param>
- protected override bool OnAuthorizationing(AuthorizationContext context)
- {
- //Ajax请求但又未登录时则返回信息
- if (!ApplicationContext.Current.IsLogined && base.ContextProvider.IsAjaxRequest)
- {
- OutputResult("未登录或者会话已过期,请重新登录!", 401);
- return false;
- }
- if (context.HttpContext.Session == null)
- {
- throw new Exception("服务器Session不可用!");
- }
- try
- {
- //调用提供者进行登陆
- ProviderManager.LoginProvider.Login();
- }
- catch (Exception ex)
- {
- Log.Error(ex);
- if (ex.InnerException != null)
- {
- Log.Error(ex.InnerException);
- }
- OutputResult("登陆时出现系统繁忙,请稍后再试!", 401);
- return false;
- }
- //如果没有登陆则返回
- if (!ApplicationContext.Current.IsLogined)
- {
- OutputResult("未登录", 401);
- return false;
- }
- return true;
- }
- }
- }
在配置文件中默认一个登录账号,这样调试时候能省很多时间。
我们判断的步骤是这样的:
第一步:如果用户是 ajax 请求,并且用户信息不存在的话直接返回 false。这里是应对用户登录之后 用户长时间未操作造成用户信息过期失效
因为我们的 Winner 框架基本都是 Ajax 请求,所以当两个条件都存在的时候就直接返回 401 错误。如果界面显示 401 则重新刷新一下,
因为刷新就不是 Ajax 了,所以就会跳到登录页去登录。
第二步:判断 Session 是否可用,不可用就直接抛异常了,就是我上面说的禁用 Session 这种情况。
第三步:在 ProviderManager.LoginProvider.Login(),我们才是做了具体的操作,我们看一下 Login() 代码:
- public void Login()
- {
- //检查是否有SSO站点POST过来的用户退出数据
- string str = HttpContext.Current.Request.Url.Query;
- if (str.Contains("logout"))
- {
- //TODO:退出本地登陆
- Logout();
- return;
- }
- //检查本地系统是否已登陆
- if (ApplicationContext.Current.IsLogined)
- return;
- //判断是否有配置自动登陆
- if (GlobalConfig.IsAutoLogin)
- {
- //代理登陆配置文件所配置的用户
- var autoResult = ApplicationContext.UserLogin(GlobalConfig.DefaultAutoLoginUserId, true);
- if (!autoResult.Success)
- {
- throw new Exception(autoResult.Message);
- }
- HttpCookie cookie = new HttpCookie("ticket");
- cookie.Value = GlobalConfig.DefaultAutoLoginToken;
- HttpContext.Current.Response.AppendCookie(cookie);
- return;
- }
- //如果没有Ticket直接跳转到SSO进行检查
- int userId;
- if (!ApplicationContext.GetNodeIdByTicket(out userId))
- {
- SSOLogin();
- return;
- }
- Log.Debug("user_id={0}", userId);
- //登陆到本地系统
- var result = ApplicationContext.UserLogin(userId, false);
- if (!result.Success)
- {
- throw new Exception(result.Message);
- }
- }
这里 ApplicationContext.Current.IsLogined 为 True 的话,就是用户已经登录过了,登录过了就返回,IsLogined 属性里面是判断了用户信息是否存在。
- private void SSOLogin()
- {
- string service = HttpContext.Current.Request.Url.AbsoluteUri;
- service = Regex.Replace(service, @"\?ticket[^&]*.", "");
- string url = string.Concat(GlobalConfig.SSO_LoginURL, "?service=", HttpContext.Current.Server.UrlEncode(service));
- HttpContext.Current.Response.Redirect(url);
- }
如果配置了自动登录,则装载自动登录的用户信息,从配置文件中读取。最后,上面判断都 False 的话,就跳到 SSO 系统去登录获取 ticket。=================================== 华丽的分割线 =========================== 下面就是 SSO 系统做的事情,SSO 最基本的职责就是登录,首先就是登录界面。根据用户填写的账号密码判断用户是否注册,没有注册则注册。
说白了就是登录注册流程。用户在 SSO 登录成功之后则创建 Session 保存用户账号,然后生成一个 ticket 字符串。每个团队对于 Ticket 字符串的内容
都不太相同,但是大抵就是要请求界面的 url,账户号,授权码这些。当然子系统判断 URL 中有 ticket 值的时候,就将 Ticket 写入子项目的 Session,其实我们会有一个 UserInfo 的基础对象,这个 Userinfo 是一个用户信息的 model。
这个是根据 Ticket 带过来用户账户再到数据库查了一次的。Jason 重构一次 SSO,方式上有点变动,更多的是采用 Oauth2.0 的方式。不清楚 Oauth 的可以看我上篇文章 《理解 Oauth2.0》 。
这里我公开一下我们 SSO 项目的源码,所以我就不一一的贴出来了。有兴趣的朋友可以自己看代码不懂的可以在 QQ 群里咨询。SSO 登录中心 GitHub 下载地址: https://github.com/demon28/OAuth2.git 就写到这里。有兴趣一起探讨 Winner 框架的可以加我们 QQ 群:261083244。或者扫描左侧二维码加群。
来源: http://www.cnblogs.com/demon28/p/8058662.html