偷懒小工具 - 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

开发过程当中,思路是最重要的。可是还须要用实际的代码来验证你的思路。毕竟语言是廉价的。

最后的话

这个偷懒小工具系列,都是我没事干写的东西,并非工做内容。我分享也只是用本身的行动,支持开源精神。

若是能帮到您,我会很高兴的。若是帮不到您,右上角就能够了。请大神们,不要拍砖哦~

相关文章
相关标签/搜索