单点登录(后文简称:sso)的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统(摘自百度百科)。整个流程中涉及到的角色有:

  • 用户。
  • 应用服务器,即业务系统。
  • 单点登录服务器,所有业务系统登录的核心枢纽,后文简称用户中心。

 

关于token同步的思考

从其定义中不难发现,核心功能点:一处登录处处登录,注销亦然。那么如何实现一处登录处处登录,先抛开网上各种解决思路回到问题本身。用户中心登录成功后产生的token(或者说“票据”,后文统一称token)如果能够同步到各个业务系统,而各个业务系统能成功解析token后即可认为达到了一处登录处处登录。所以关键问题在于:

  1. 如何在用户中心登录成功后将token同步到各个业务系统。
  2. 各业务系统如何能够成功解析token。

 

其中各业务系统解析token很好解决,和用户中心约定一套公用的加密/解密方式即可。那么问题一,由于token的存储一般在于浏览器,而从用户中心服务器发起请求到各个业务系统是在浏览器端写不了token的。那么换种思路,在登录成功后从浏览器端向各个业务系统发起请求写入token。

 

关于登录功能使用的思考

而由于用户中心被许多业务系统所使用,各系统所使用的开发语言未必能完全统一,于是有功能点二:登录服务的调用应该是易用且与平台语言无关的。这个问题可按两种不同的思路来解决:

  1. 业务系统没有登录页面,直接跳转用户中心登录并将token同步至所有业务系统。
  2. 业务系统有登录页面,直接引用用户中心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的刷新是跟随正常请求的,我们就不能再使用像登录那样依靠浏览器去通知所有业务系统了,关于这个问题,有三种解决思路:

  1. 各系统定时刷新token并通知各个业务系统。
  2. token只存于用户中心,向各个业务系统发放该token的key,各业务系统根据key向用户中心获取token并缓存,缓存的过期时间为是token下次应该刷新的时间。
  3. 共享一个分布式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);