偷懒小工具 - SSO单点登录通用类(可跨域)

邮差的信 提交于 2019-12-29 03:45:48

写在前面的话

上次发布过一篇同样标题的文章。但是因为跨域方面做得不太理想。我进行了修改,并重新分享给大家。

如果这篇文章对您有所帮助,请您点击一下推荐。以便有动力分享出更多的“偷懒小工具”

目的 

目的很明确,就是搭建单点登录的帮助类,并且是一贯的极简风格(调用方法保持5行以内)。

并且与其他类库,关联性降低。所以,不使用WebAPI或者WebService等。

思路

因为上次有朋友说,光看见一堆代码,看不见具体思路。所以,这次分享,我把思路先写出来。

懒得看实现代码的朋友,可直接查看“思路”这个子标题。

同时如果有好的想法,请修改后在github上推给我。Talk is cheap,Show me the code

同域

同域需要考虑的问题比较少。只需要考虑,MVC和WebForm的Request如何获取即可。

实现流程图如下

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

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

最后的话

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

如果能帮到您,我会很高兴的。如果帮不到您,右上角就可以了。请大神们,不要拍砖哦~

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!