1. 程式人生 > >程序員的自我救贖---3.2:SSO及應用案例

程序員的自我救贖---3.2:SSO及應用案例

contain urn 前端框架 顯示 分開 十分 域名 酒店 刷新

《前言》

(一) 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及應用案例