写在前面的话:上次发布过一篇同样标题的文章。但是因为跨域方面做得不太理想。
我进行了修改,并重新分享给大家。想看原来的文,可点击上方的超链接。
目的很明确,就是搭建单点登录的帮助类,并且是一贯的极简风格(调用方法保持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中,写上如下代码。
class="code_img_closed" src="/Upload/Images/2016102605/0015B68B3C38AA5B.gif" alt=""><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>logs_code_collapse">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用来处理跟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
开发过程中,思路是最重要的。但是还需要用实际的代码来验证你的思路。毕竟语言是廉价的。
这个偷懒小工具系列,都是我没事干写的东西,并不是工作内容。我分享也只是用自己的行动,支持开源精神。
如果能帮到您,我会很高兴的。如果帮不到您,右上角就可以了。请大神们,不要拍砖哦~