单点登录(后文简称: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);