偷懒小工具 - SSO单点登录通用类(可跨域)
写在前面的话
上次发布过一篇同样标题的文章。但是因为跨域方面做得不太理想。我进行了修改,并重新分享给大家。
如果这篇文章对您有所帮助,请您点击一下推荐。以便有动力分享出更多的“偷懒小工具”
目的
目的很明确,就是搭建单点登录的帮助类,并且是一贯的极简风格(调用方法保持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(, , ), 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(, , ));
} /// <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(, , ), 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(, , ));
}
}
其他网站
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(, 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(, 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 - , );
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
开发过程中,思路是最重要的。但是还需要用实际的代码来验证你的思路。毕竟语言是廉价的。
最后的话
这个偷懒小工具系列,都是我没事干写的东西,并不是工作内容。我分享也只是用自己的行动,支持开源精神。
如果能帮到您,我会很高兴的。如果帮不到您,右上角就可以了。请大神们,不要拍砖哦~
偷懒小工具 - SSO单点登录通用类(可跨域)的更多相关文章
- 偷懒小工具 - SSO单点登录通用类(可跨域)(上)
目的 目的很明确,就是搭建单点登录的帮助类,并且是一贯的极简风格(调用方法保持5行以内). 并且与其他类库,关联性降低.所以,不使用WebAPI或者WebService等. 思路 因为上次有朋友 ...
- [精华][推荐]CAS SSO 单点登录框架学习 环境搭建
1.了解单点登录 SSO 主要特点是: SSO 应用之间使用 Web 协议(如 HTTPS) ,并且只有一个登录入口. SSO 的体系中有下面三种角色: 1) User(多个) 2) Web 应用( ...
- CAS SSO单点登录框架学习
1.了解单点登录 SSO 主要特点是: SSO 应用之间使用 Web 协议(如 HTTPS) ,并且只有一个登录入口. SSO 的体系中有下面三种角色: 1) User(多个) 2) Web 应用( ...
- CAS SSO单点登录框架介绍
1.了解单点登录 SSO 主要特点是: SSO 应用之间使用 Web 协议(如 HTTPS) ,并且只有一个登录入口. SSO 的体系中有下面三种角色: 1) User(多个) 2) Web 应用( ...
- [sso] 单点登录认证流程
一.流程说明 第一步:访问cas过滤链接ssoLogin,拼凑定向到 CAS_SERVER 获取ticket的URL 第二步:CAS_SERVER校验用户信息,生成Ticket 第三步:重新定向到访问 ...
- 170810、spring+springmvc+Interceptor+jwt+redis实现sso单点登录
在分布式环境中,如何支持PC.APP(ios.android)等多端的会话共享,这也是所有公司都需要的解决方案,用传统的session方式来解决,我想已经out了,我们是否可以找一个通用的方案,比如用 ...
- SSO单点登录思路
SSO (Single Sign On) 什么是单点登录: 在大型的互联网公司中会有多个系统, 多个项目, 虽然这些项目都属于同一家公司, 但是项目本身其实都是独立的, 那多个系统可不可以实现共享同一 ...
- 【SpringSecurityOAuth2】源码分析@EnableOAuth2Sso在Spring Security OAuth2 SSO单点登录场景下的作用
目录 一.从Spring Security OAuth2官方文档了解@EnableOAuth2Sso作用 二.源码分析@EnableOAuth2Sso作用 @EnableOAuth2Client OA ...
- 开发SSO单点登录需要注意的问题
一.单点登录系统开发需要注意的问题 1.单点登录系统需要支持jsonp请求? 单点登录系统主要是向其他系统提供用户身份验证服务,因此需要提供对外接口,而外部系统通过接口访问时,必然涉 ...
随机推荐
- js数组转换问题
一维数组转多维数组 var arr=[1,2,3,4,5,6,7,8,9,10]; function splitArray(arr,size){ var result = []; var tempAr ...
- 压缩html
最近研究程序的优化,压缩html可以减小html的体积,有利于提高页面的相应速度.在webform的basepage中添加如下代码: /// <summary> /// 重写Render方 ...
- QQ互联登录以及非官方正版应用报100044错误
QQ第三方登录的时候,显示非官方正版应用,报100044错误:坑1:我们在QQ互联注册成功后需要设置包名和签名,签名是通过官方提供的工具生成的.注意一点:这里的签名是需要由打包签名之后APK生成,我们 ...
- SQL删除语句同时向备份表插入数据
从这里摘抄下来的,觉得很不错,http://www.cnblogs.com/ljhdo/p/5792886.html#3503524 ,以后就用这种方式删除,再也不用担心删除错数据啦!!!
- 使用InstallAnywhere工具打包Java_Web程序
一.准备工作 1. 下载InstallAnywhere,并安装: 2. 下载解压版jdk1.7(示例为jdk1.7.0_75): 3. 下载解压版tomcat7.0以上版本(示例 ...
- WPF 自定义搜索框
控件中的搜索图标下载地址:http://www.easyicon.net/1183666-Search_icon.html 搜索框设计过程比较简单: 1.先定义一个Rectangle作为背景 2. ...
- 关于对For循环嵌套优化的问题
1.案例描述 由于一次Java面试的笔试题,当时没有写出很好的解决方案,特此专门撰写一篇博客来加以记录,方便日后的查看 面试题目如下:从性能上优化如下代码并说明优化理由? for (int i = 0 ...
- 任意半径局部直方图类算法在PC中快速实现的框架。
在图像处理中,局部算法一般来说,在很大程度上会获得比全局算法更为好的效果,因为他考虑到了图像领域像素的信息,而很多局部算法可以借助于直方图获得加速.同时,一些常规的算法,比如中值滤波.最大值滤波.最小 ...
- hdu5651 xiaoxin juju needs help (多重集的全排列+逆元)
xiaoxin juju needs help 题意:给你一个字符串,求打乱字符后,有多少种回文串. (题于文末) 知识点: n个元素,其中a1,a2,··· ...
- C#递归遍历子目录与子目录中的文件
[转载]作者:weixingstudio 采用C#,通过指定一个路径,来递归的遍历所有的子目录以及子目录中的文件,建一个类似资源管理器的目录树 先递归的遍历所有的子目录,如果没有子目录以后,则遍历所有 ...