单点登录(一):思考
单点登录(后文简称:sso)的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统(摘自百度百科)。整个流程中涉及到的角色有:
- 用户。
- 应用服务器,即业务系统。
- 单点登录服务器,所有业务系统登录的核心枢纽,后文简称用户中心。
关于token同步的思考
从其定义中不难发现,核心功能点:一处登录处处登录,注销亦然。那么如何实现一处登录处处登录,先抛开网上各种解决思路回到问题本身。用户中心登录成功后产生的token(或者说“票据”,后文统一称token)如果能够同步到各个业务系统,而各个业务系统能成功解析token后即可认为达到了一处登录处处登录。所以关键问题在于:
- 如何在用户中心登录成功后将token同步到各个业务系统。
- 各业务系统如何能够成功解析token。
其中各业务系统解析token很好解决,和用户中心约定一套公用的加密/解密方式即可。那么问题一,由于token的存储一般在于浏览器,而从用户中心服务器发起请求到各个业务系统是在浏览器端写不了token的。那么换种思路,在登录成功后从浏览器端向各个业务系统发起请求写入token。
关于登录功能使用的思考
而由于用户中心被许多业务系统所使用,各系统所使用的开发语言未必能完全统一,于是有功能点二:登录服务的调用应该是易用且与平台语言无关的。这个问题可按两种不同的思路来解决:
- 业务系统没有登录页面,直接跳转用户中心登录并将token同步至所有业务系统。
- 业务系统有登录页面,直接引用用户中心sso.js调用登录并将token同步至所有业务系统。
关于登录用户权限的思考
假定有业务系统A、B、C、D。用户1可登录系统A、B,用户2可登录系统B、C、D,于是有功能点三:用户中心应该可以控制用户所能登录的业务系统。在登录生成token时,加入能够登录的业务系统信息,在登录成功后,只向能够登录的业务系统发起同步token的请求,并且各业务系统在token解析后需要验证token是否具有当前系统的登录权限。
关于token刷新策略的思考
关于token的刷新策略,token应该什么时候刷新,在sso系统中,token刷新后又该如何通知到其他业务系统。第一个问题参考owin的cookie登录,在请求中,判断token是否超过有效期的一半,超过则刷新。第二个问题就麻烦了,因为token的刷新是跟随正常请求的,我们就不能再使用像登录那样依靠浏览器去通知所有业务系统了,关于这个问题,有三种解决思路:
- 各系统定时刷新token并通知各个业务系统。
- token只存于用户中心,向各个业务系统发放该token的key,各业务系统根据key向用户中心获取token并缓存,缓存的过期时间为是token下次应该刷新的时间。
- 共享一个分布式token存储系统,可使用redis,向各个业务系统发放token的key,需要刷新时直接使用key刷新redis中的token。
巴拉巴拉讲了一堆,也不知道大伙们能理解多少,权当记录我在开发过程中的一些思考吧,当然少不了大家喜闻乐见的GitHub地址:https://github.com/liuxx001/sso.git,下篇讲具体实现,最后先放个sso.js压压惊。
var sso = sso || {}; (function ($) { sso.host = "http://localhost:58806/"; sso.utils = { isEmpty: function(str) { if (typeof (str) === "undefined") return true; if (str.replace(/(^s*)|(s*$)/g, "").length === 0) return true; return false; } }; /** * 登录 * @param {signInfo}登录信息 * { userName:"", password:"", rememberMe:false, returnUrl:"" } */ sso.login = function(signInfo) { if (sso.utils.isEmpty(signInfo.userName)) { alert("用户名不能为空"); return; } if (sso.utils.isEmpty(signInfo.password)) { alert("登录密码不能为空"); return; } $.ajax({ url: sso.host + "Account/SignIn", dataType: 'jsonp', type: 'GET', contentType: 'application/json', data: signInfo }); }; /** * 三方登录 * @param {signInfo}登录信息 * { loginProvider:"", providerKey:"", rememberMe:false, returnUrl:"" } */ sso.externalLogin = function(signInfo) { if (sso.utils.isEmpty(signInfo.loginProvider)) { alert("三方登录来源不能为空"); return; } if (sso.utils.isEmpty(signInfo.providerKey)) { alert("三方登录唯一Key不能为空"); return; } $.ajax({ url: sso.host + "Account/ExternalSignIn", dataType: 'jsonp', type: 'GET', contentType: 'application/json', data: signInfo }); }; /** * 注销 */ sso.logOut = function() { $.ajax({ url: sso.host + "Account/SignOut", dataType: 'jsonp', type: 'GET', contentType: 'application/json', data: {} }); }; /** * sso服务器登录成功后jsonp回调 * @param {string[]}需要通知的Url集合 */ sso.notify = function () { var createScript = function (src) { $("<script><//script>").attr("src", src).appendTo("body"); }; var urlList = arguments; for (var i = 1; i < urlList.length; i++) { createScript(urlList[i]); } //延时执行,避免跳转时cookie还未写入成功 setTimeout(function () { if (urlList[0] === "refresh") { window.location.reload(); } else { window.location.href = urlList[0]; } }, 1000); }; /** * sso服务器登录失败后jsonp回调 * @param {code}错误码 * @param {msg}错误消息 */ sso.error= function(code, msg) { alert(msg); } })(jQuery);