程序員的自我救贖---3.2:SSO及應用案例
《前言》
(一) Winner2.0 框架基礎分析
(二)PLSQL報表系統
(三)SSO單點登錄
(四) 短信中心與消息中心
(五)錢包系統
(六)GPU支付中心
(七)權限系統
(八)監控系統
(九)會員中心
(十) APP版本控制系統
(十一)Winner前端框架與RPC接口規範講解
(十二)上層應用案例
(十三)總結
《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] 和 [AuthRigth] 兩者的不同在於 [AuthLogin] 只驗證是否有登錄,沒有登錄就去登錄。
意思就是說該頁面所有人都有權限訪問,前提是有註冊。而[AuthRigth] 則不單單是驗證了是否登錄,還驗證了是否有權限訪問本頁面。
關於權限那一塊,在後面的文章中我再單獨講權限系統時再細講。
我們重點來看一下[AuthLogin] 的核心代碼:
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; } } }
我們看到一開始我們有base(ignore);這個我在前面的篇章中有講到過,這個可以通過配置文件配置,目的是省去我們每個項目開發的時候都要去登錄。
在配置文件中默認一個登錄賬號,這樣調試時候能省很多時間。
我們判斷的步驟是這樣的:
第一步:如果用戶是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); } }
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); }
這裏ApplicationContext.Current.IsLogined為True的話,就是用戶已經登錄過了,登錄過了就返回,IsLogined屬性裏面是判斷了用戶信息是否存在。
如果配置了自動登錄,則裝載自動登錄的用戶信息,從配置文件中讀取。最後,上面判斷都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。或者掃描左側二維碼加群。
程序員的自我救贖---3.2:SSO及應用案例