單點登錄(后文簡稱: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);