偷懶小工具 - SSO單點登陸通用類(可跨域)

寫在前面的話html

上次發佈過一篇一樣標題的文章。可是由於跨域方面作得不太理想。我進行了修改,並從新分享給你們。前端

若是這篇文章對您有所幫助,請您點擊一下推薦。以便有動力分享出更多的「偷懶小工具」git

目的 

目的很明確,就是搭建單點登陸的幫助類,而且是一向的極簡風格(調用方法保持5行之內)。github

而且與其餘類庫,關聯性下降。因此,不使用WebAPI或者WebService等。web

思路

由於上次有朋友說,光看見一堆代碼,看不見具體思路。因此,此次分享,我把思路先寫出來。json

懶得看實現代碼的朋友,可直接查看「思路」這個子標題。後端

同時若是有好的想法,請修改後在github上推給我。Talk is cheap,Show me the code跨域

同域cookie

同域須要考慮的問題比較少。只須要考慮,MVC和WebForm的Request如何獲取便可。app

實現流程圖以下

1. 由於是使用一樣的Cookie因此名稱和加密方式必須一致。

2. 須要設置登陸成功後,回跳的網址。由於Forms身份認證的ReturnURL不能得到請求原網址。

3. 剩下的就如圖所示了。不明白的能夠追問,我就不細說了。

跨域

跨域除了須要考慮同域的問題外,還須要考慮狀態共享。由於同源策略問題,故此使用JSONP

1. 由於不是Cookie共享,因此只須要設置相同的加密方法便可。

2. 須要在認證網站,添加可登陸的其餘網站集合,使用逗號分隔。

3. 須要在其餘網站,建立一個Login頁面並調用幫助類的驗證方法。配置認證網站URL。

4. 當認證網站登陸成功後,會根據配置的其餘網站,給他們發送JSONP請求,讓他們自動登陸。

5. 註銷同理。JSONP請求方式,可參考這篇文章:jsonp詳解。使用的就是添加js標籤的方式。

至此,思路說明結束。不明白的能夠追問。


詳細設計

簡介

整個類庫格式以下,我儘可能進行了重構,讓各位看着方便一些。由於懶因此只是儘可能重構。

SSO.js:跨域單點登陸,須要在登陸頁面引用的Javascript腳本。

SSOCrossDomain:跨域幫助類

SSOSameDomain:同域幫助類

App.config:跨域幫助類,涉及到的配置示例

須要在認證網站和其餘網站中,同時引用這個類。並根據本身的需求,看調用哪一個幫助類。

使用方法

首先,咱們建立以下結構的解決方案來進行演示。

Authorize:是WebForm的認證網站,使用MVP的PV模式搭建。其餘的均爲須要共享的網站。

MVC1:是MVC的認證網站。認證網站均實現了,最簡單的登陸功能。

同域

首先說一下同域如何使用。

1. 咱們須要配置相同的身份驗證。那麼咱們在Web.Config中,寫上以下代碼。

<system.web>
    <compilation debug="true" targetFramework="4.6.1" />
    <authentication mode="Forms">
      <forms loginUrl="~/Login.aspx" name="CookiesTest" cookieless="UseCookies"></forms>
    </authentication>
    <authorization>
      <deny users="?" />
    </authorization>
    <machineKey validationKey="5029E82E1779497186D46F83D78FAD3211D46F83D78FAD" validation="SHA1" decryptionKey="5029E82E1779497186D46F83D78FAD3211D46F83D78FAD" decryption="DES" />
  </system.web>
Authorize
<system.web>
    <compilation debug="true" targetFramework="4.6.1" />
    <authentication mode="Forms">
      <forms loginUrl="http://localhost:51666/Login.aspx?link=http://localhost:56757/WebForm1.aspx" name="CookiesTest" cookieless="UseCookies"></forms>
      <!--<forms loginUrl="~/Login.aspx" name="CookieWeb1" cookieless="UseCookies"></forms>-->
    </authentication>
    <authorization>
      <deny users="?" />
    </authorization>
    <machineKey validationKey="5029E82E1779497186D46F83D78FAD3211D46F83D78FAD" validation="SHA1" decryptionKey="5029E82E1779497186D46F83D78FAD3211D46F83D78FAD" decryption="DES" />
  </system.web>
Web1

配置東西分別爲:Forms認證,禁止匿名用戶訪問,配置單點登陸加密方式。 

其中Web1的Forms認證,指向的就是Authorize,而且使用link當作後綴,進行成功後跳轉。

2. 須要在Authorize網站中,添加登陸頁面,並添加登陸後的調用方法。

/// <summary>
        /// 用戶登陸方法
        /// </summary>
        private void LoginView_Submit(object sender, AuthorizeEventArgs e)
        {
            string userName = LoginView.UserName;
            string password = LoginView.Password;
            if (ValidationUserInfo(userName, password))
            {
                //同域單點登陸
                SSOSameDomain sso = new SSOSameDomain(e.Page);
                sso.LogIn("CookiesTest", new TimeSpan(0, 1, 0), userName);

                ////跨域單點登陸
                //SSOCrossDomain cross = new SSOCrossDomain(e.Page);
                //cross.LogIn("CookiesTest", new TimeSpan(0, 1, 0), userName);
            }
        }
Authorize

SSOSameDomain,分別能夠接受Page和HttpContextBase,做爲讀取Request的媒介。

因此各位若是不用MVP,可實例化時直接this。

LogIn登陸方法,須要傳遞配置的Cookie名稱、過時時間和須要保存的內容。

3. 配置註銷功能,在點擊註銷後,執行以下方法。

protected void SignOut_Click(object sender, EventArgs e)
        {
            new SSOSameDomain(this).LogOut();
            //new SSOCrossDomain(this).LogOut();
        }
註銷

4. 獲取用戶內容,能夠調用幫助類的GetUserData方法。傳遞Cookie名稱,便可獲取對應內容。

protected void Page_Load(object sender, EventArgs e)
        {
            if (!IsPostBack)
            {
                if (User.Identity.IsAuthenticated)
                {
                    var result = new SSOSameDomain(this).GetUserData("CookiesTest");
                    txtUserData.Text = result;
                    //SSOCrossDomain cross = new SSOCrossDomain(this);
                    //txtUserData.Text = cross.GetUserData("CookieWeb1");
                }
            }
        }
獲取用戶內容 

至此,咱們已經完成了同域的單點登陸。

跨域

跨域由於須要驗證,因此會比同域操做多幾步。注意:每一個網站都必須有相似Login.aspx頁面用做登陸存儲。

1. 首先配置相同的加密方式,由於咱們的JSONP傳遞的是密文,因此解密方式必須一致。

<system.web>
    <compilation debug="true" targetFramework="4.6.1" />
    <authentication mode="Forms">
      <forms loginUrl="~/Login.aspx" name="CookiesTest" cookieless="UseCookies"></forms>
    </authentication>
    <authorization>
      <deny users="?" />
    </authorization>
    <machineKey validationKey="5029E82E1779497186D46F83D78FAD3211D46F83D78FAD" validation="SHA1" decryptionKey="5029E82E1779497186D46F83D78FAD3211D46F83D78FAD" decryption="DES" />
  </system.web>
Authorize

其餘網站的Forms認證頁面,都指向本地的Login.aspx。注意加密方式必須一致,否則沒法解密。

2. 認證網站設置可登陸的網址集合,在配置文件中添加集合,使用逗號分隔。

<appSettings>
    <add key="LoginUrl" value="http://localhost:56757/Login.aspx,http://localhost/Web2/Login.aspx" />
  </appSettings>
LoginUrl

3. 其餘網站設置統一認證的網址,並添加成功後跳轉的地址。

<appSettings>
    <add key="AuthorizeUrl" value="http://localhost:51666/Login.aspx?link=http://localhost:56757/WebForm1.aspx" />
  </appSettings>
AuthorizeUrl

至此,配置結束,咱們接下來講一下如何調用。

4. 認證網站,添加驗證登陸和登陸方法。

public void Initialize(Page page)
        {
            SSOCrossDomain cross = new SSOCrossDomain(page);
            cross.ValidationLogIn("CookiesTest", new TimeSpan(0, 1, 0));
        }

        /// <summary>
        /// 用戶登陸方法
        /// </summary>
        private void LoginView_Submit(object sender, AuthorizeEventArgs e)
        {
            string userName = LoginView.UserName;
            string password = LoginView.Password;
            if (ValidationUserInfo(userName, password))
            {
                ////同域單點登陸
                //SSOSameDomain sso = new SSOSameDomain(e.Page);
                //sso.LogIn("CookiesTest", new TimeSpan(0, 1, 0), userName);

                //跨域單點登陸
                SSOCrossDomain cross = new SSOCrossDomain(e.Page);
                cross.LogIn("CookiesTest", new TimeSpan(0, 1, 0), userName);
            }
        }
認證網站

Initialize:是Login.aspx頁面初始化執行的方法,咱們調用幫助類的ValidationLogin,驗證是否登陸。

Login:調用幫助類的Login方法,能夠保存登陸狀態,並向其餘網站進行發送狀態。

5. 其餘網站,添加驗證登陸方法。

protected void Page_Load(object sender, EventArgs e)
        {
            if (!IsPostBack)
            {
                SSOCrossDomain cross = new SSOCrossDomain(this);
                cross.ValidationLogIn("CookieWeb1", new TimeSpan(0, 2, 0));
            }
        }
其餘網站

ValidationLogIn :驗證登陸方法,傳遞參數:本地存儲的Cookie名稱,過時時間。

6. 其餘網站,添加註銷方法和獲取登陸內容。

protected void Page_Load(object sender, EventArgs e)
        {
            if (!IsPostBack)
            {
                if (User.Identity.IsAuthenticated)
                {
                    var result = new SSOSameDomain(this).GetUserData("CookiesTest");
                    txtUserData.Text = result;
                    //SSOCrossDomain cross = new SSOCrossDomain(this);
                    //txtUserData.Text = cross.GetUserData("CookieWeb1");
                }
            }
        }

        protected void SignOut_Click(object sender, EventArgs e)
        {
            //new SSOSameDomain(this).LogOut();
            new SSOCrossDomain(this).LogOut();
        }
註銷和獲取

至此,咱們已經完成了跨域的單點登陸。每一個調用,不超過5行代碼,極簡風格。

MVC方法相似,能夠參考下方源碼。

代碼實現

Operation

Operation用來處理跟Request和Response掛鉤的操做。我目前沒有找到WebForm和MVC公用的類。

故此使用抽象工廠來實現此類操做。此處,我一直不是很滿意,但願有其餘想法的能夠告知。

1. 定義抽象類。

/// <summary>
    /// 單點登陸操做工廠
    /// </summary>
    public abstract class Operation
    {
        /// <summary>
        /// 執行受權的腳本
        /// </summary>
        public string PerformJavascript { get; set; }

        /// <summary>
        /// 獲取參數
        /// </summary>
        /// <param name="request">參數名</param>
        /// <returns>參數值</returns>
        public abstract string GetRequest(string request);

        /// <summary>
        /// 設置Cookie
        /// </summary>
        /// <param name="cookie">Cookie實體</param>
        public abstract void SetCookie(HttpCookie cookie);

        /// <summary>
        /// 獲取Cookie值
        /// </summary>
        /// <param name="cookieName">Cookie名稱</param>
        public abstract HttpCookie GetCookie(string cookieName);

        /// <summary>
        /// 重定向制定頁面
        /// </summary>
        /// <param name="url">目標URL</param>
        public abstract void Redirect(string url);

        /// <summary>
        /// 輸出指定內容
        /// </summary>
        /// <param name="text">內容</param>
        public abstract void PerformJs(string text);

        /// <summary>
        /// 獲取當前URL
        /// </summary>
        /// <returns></returns>
        public abstract Uri Uri();
    }
Operation

2. 定義WebForm的操做類。

/// <summary>
    /// WebForm操做方法
    /// </summary>
    public class OperationPage : Operation
    {
        public Page Page { get; set; }

        public OperationPage(Page page)
        {
            Page = page;
        }

        public override string GetRequest(string request)
        {
            string result = Page.Request[request];
            return result ?? "";
        }

        public override void SetCookie(HttpCookie cookie)
        {
            Page.Response.Cookies.Add(cookie);
        }

        public override HttpCookie GetCookie(string cookieName)
        {
            return Page.Request.Cookies[cookieName];
        }

        public override void Redirect(string url)
        {
            Page.Response.Redirect(url);
        }

        public override void PerformJs(string text)
        {
            Page.ClientScript.RegisterStartupScript(Page.ClientScript.GetType(), "LogIn", text);
        }

        public override Uri Uri()
        {
            return new Uri(Page.Request.Url.ToString());
        }
    }
OperationPage

3. 定義MVC的操做類

/// <summary>
    /// MVC操做方法
    /// </summary>
    public class OperationHttpContext : Operation
    {
        public HttpContextBase Context { get; set; }

        public OperationHttpContext(HttpContextBase context)
        {
            Context = context;
        }

        public override string GetRequest(string request)
        {
            return Context.Request[request];
        }

        public override void SetCookie(HttpCookie cookie)
        {
            Context.Response.Cookies.Add(cookie);
        }

        public override HttpCookie GetCookie(string cookieName)
        {
            return Context.Request.Cookies[cookieName];
        }

        public override void Redirect(string url)
        {
            Context.Response.Redirect(url);
        }

        public override void PerformJs(string text)
        {
            text = text.Replace("<script>", "");
            text = text.Replace("</script>", "");
            PerformJavascript = text;
        }

        public override Uri Uri()
        {
            return new Uri(Context.Request.Url.ToString());
        }
    }
OperationHttpContext

咱們經過幫助類的構造函數,對Operation進行初始化。

/// <summary>
        /// HTTP狀態操做
        /// </summary>
        public Operation Operation { get; set; }

        public SSOSameDomain(HttpContextBase context)
        {
            Operation = new OperationHttpContext(context);
        }

        public SSOSameDomain(Page page)
        {
            Operation = new OperationPage(page);
        }
初始化

同域

同域幫助類,須要公開三個功能:LogIn,LogOut,GetUserData。此處若是有其餘需也可作成接口。

public class SSOSameDomain
    {
        /// <summary>
        /// HTTP狀態操做
        /// </summary>
        public Operation Operation { get; set; }

        public SSOSameDomain(HttpContextBase context)
        {
            Operation = new OperationHttpContext(context);
        }

        public SSOSameDomain(Page page)
        {
            Operation = new OperationPage(page);
        }

        /// <summary>
        /// 用戶登陸
        /// </summary>
        public void LogIn(string cookieName, TimeSpan overdue, string userData)
        {
            FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(2, cookieName, DateTime.Now, DateTime.Now.Add(overdue), true, userData);
            CreateCookie(ticket);
            RedirectPage();
        }

        /// <summary>
        /// 用戶註銷
        /// </summary>
        public void LogOut()
        {
            FormsAuthentication.SignOut();
            FormsAuthentication.RedirectToLoginPage();
        }

        /// <summary>
        /// 獲取登陸信息
        /// </summary>
        public string GetUserData(string cookieName)
        {
            string result = Operation.GetCookie(cookieName)?.Value;
            return result != null ? FormsAuthentication.Decrypt(result).UserData : "";
        }

        /// <summary>
        /// 建立Cookie
        /// </summary>
        private void CreateCookie(FormsAuthenticationTicket ticket)
        {
            HttpCookie cookie = new HttpCookie(ticket.Name, FormsAuthentication.Encrypt(ticket));
            cookie.Expires = ticket.Expiration;
            Operation.SetCookie(cookie);
        }

        /// <summary>
        /// 登陸成功跳轉
        /// </summary>
        private void RedirectPage()
        {
            if (!string.IsNullOrEmpty(Operation.GetRequest("link")))
            {
                Operation.Redirect(Operation.GetRequest("link"));
                return;
            }
            if (!string.IsNullOrEmpty(Operation.GetRequest("ReturnUrl")))
            {
                Operation.Redirect(Operation.GetRequest("ReturnUrl"));
                return;
            }
            Operation.Redirect("/");
        }

    }
同域幫助類

同域的很是簡單,我不講解什麼了。

跨域 

跨域幫助類,須要公開四個功能,除了同域的三個功能外,添加ValidationLogIn驗證功能。

1. 首先,咱們說一下如何實現的JSONP。咱們建立了一個Js方法,而後從後端調用這個方法。

function LogIn() {
    var urlList = arguments;
    for (var i = 1; i < urlList.length; i++) {
        CreateScript(urlList[i]);
    }
    window.location.href = urlList[0];
}

function CreateScript(src) {
    $("<script><//script>").attr("src", src).appendTo("body")
}
SSO

方法一目瞭然,很少說了。使用這個加載script,就能夠進行JSONP的訪問。

咱們接下來,一步一步過一下每一個方法。

2. LogIn 用戶登陸

/// <summary>
        /// 用戶登陸受權
        /// <param name="userData">用戶信息</param>
        /// </summary>
        public void LogIn(string cookieName, TimeSpan overdue, string userData, string redirectUrl = "")
        {
            FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(2, cookieName, DateTime.Now, DateTime.Now.Add(overdue), true, userData);
            CreateCookie(ticket);
            PerformJavascript("logIn", redirectUrl, userData);
        }
Login

分別就是:建立憑證、建立Cookie、發送JSONP請求

/// <summary>
        /// 執行前端js跳轉,受權
        /// </summary>
        private void PerformJavascript(string logType, string redirectLink, string userData = "")
        {
            Uri uri = Operation.Uri();
            string redirectUrl = "";
            if (string.IsNullOrEmpty(redirectLink))
            {
                redirectUrl = GetPageUrl();
                //若是返回網址包含Http,則直接跳轉。不包含則本網址內跳轉
                if (!redirectUrl.Contains("http"))
                {
                    redirectUrl = uri.Scheme + "://" + uri.Authority + GetPageUrl();
                }
            }
            else
            {
                redirectUrl = redirectLink;
            }
            StringBuilder resultMethod = new StringBuilder("LogIn('" + redirectUrl + "',");
            foreach (string url in GetUrlList())
            {
                resultMethod.Append("'");
                resultMethod.Append(string.Format("{0}?logType={1}&userData={2}", url, logType, userData));
                resultMethod.Append("',");
            }
            resultMethod.Remove(resultMethod.Length - 1, 1);
            resultMethod.Append(")");
            Operation.PerformJs("<script>" + resultMethod + "</script>");
        }
PerformJavascript

執行前端JS方法,內容分別是:獲取成功跳轉路徑,拼接調用方法的Js,執行Js

3. LogOut 用戶註銷

/// <summary>
        /// 用戶註銷
        /// </summary>
        public void LogOut()
        {
            FormsAuthentication.SignOut();
            string loginUrl = ConfigurationManager.AppSettings["LoginUrl"];
            if (string.IsNullOrEmpty(loginUrl))
            {
                string authorizeUrl = ConfigurationManager.AppSettings["AuthorizeUrl"];
                Operation.Redirect(authorizeUrl + "&logType=logOut");
                return;
            }
            PerformJavascript("logOut", "");
        }
LogOut

分別就是:本地註銷、遠程發送註銷請求到認證網站,執行Js

4. GetUserData 與同域相似,這裏不貼代碼了。

5. ValidationLogIn 驗證登陸用戶,會判斷請求的logType,來進行登陸和註銷的操做。

public void ValidationLogIn(string cookieName, TimeSpan overdue)
        {
            string logTypeParameter = Operation.GetRequest("logType");
            string redirectLink = Operation.GetRequest("link");
            if (string.IsNullOrEmpty(logTypeParameter))
            {
                string authorizeUrl = ConfigurationManager.AppSettings["AuthorizeUrl"];
                if (string.IsNullOrEmpty(authorizeUrl))
                {
                    return;
                }
                else
                {
                    Operation.Redirect(authorizeUrl);
                    return;
                }
            }
            SSOSameDomain sameDomain = new SSOSameDomain(HttpContextType);
            switch (logTypeParameter)
            {
                case "logIn":
                    sameDomain.LogIn(cookieName, overdue, Operation.GetRequest("userData"));
                    break;

                case "logOut":
                    FormsAuthentication.SignOut();
                    if (string.IsNullOrEmpty(redirectLink))
                    {
                        FormsAuthentication.RedirectToLoginPage();
                    }
                    else
                    {
                        Operation.Redirect(redirectLink);
                    }
                    break;

                default:
                    throw new InvalidOperationException("登陸認證狀態無效");
            }
        }
ValidationLogIn

開源地址:Github   碼雲OSC

開發過程當中,思路是最重要的。可是還須要用實際的代碼來驗證你的思路。畢竟語言是廉價的。

最後的話

這個偷懶小工具系列,都是我沒事幹寫的東西,並非工做內容。我分享也只是用本身的行動,支持開源精神。

若是能幫到您,我會很高興的。若是幫不到您,右上角就能夠了。請大神們,不要拍磚哦~

相關文章
相關標籤/搜索