.net core实践系列之SSO-同域实现
前言
SSO的系列还是以.Net Core作为实践例子与大家分享,SSO在Web方面复杂度分同域与跨域。本篇先分享同域的设计与实现,跨域将在下篇与大家分享。
如有需要调试demo的,可把SSO项目部署为域名http://sso.cg.com/,Web1项目部署为http://web1.cg.com,http://web2.cg.com,可以减少配置修改量
源码地址:https://github.com/SkyChenSky/Core.SSO
效果图

SSO简介
单点登录,全称为Single Sign On,在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。
它是一个解决方案,目的是为了整合企业内多个应用系统,仅由一组账号只需进行一次登录,就可被授权访问多个应用系统。

流程描述
未登录状态访问业务Web应用会引导到认证中心。
用户在认证中心输入账号信息通过登录后,认证中心会根据用户信息生成一个具有安全性的token,将以任何方式持久化在浏览器。
此后访问其他Web应用的时候,必须携带此token进行访问,业务Web应用会通过本地认证或者转发认证而对token进行校验。
从上图可以简单的分析出三个关键点:
- Token的生成
- Token的共享
- Token校验
Token的生成
方式有多种:
可以通过Web框架对用户信息加密成Token。
Token编码方式也可以为JSON WEB TOKEN(JWT)
也可以是一段MD5,通过字典匹配保存在服务器用户信息与MD5值
Token的共享
浏览器存储有三种方式:
- Cookie
- 容量4KB限制
- 过期时间
- localStorage
- 容量5MB限制
- 生命周期永久
- sessionStorage
- 容量5MB限制
- 生命周期当前会话,关闭浏览器则失效
- 无法与服务端交互
作为拥有会失效的会话状态,更因选择Cookie存储。那么Cookie的使用是可以在同域共享的,因此在实现SSO的时候复杂度又分为同域与跨域。
同域的共享比较简单,在应用设置Cookie的Domain属性进行设置,就可以完美的解决。
Token校验
校验分两种情况:
- 转发给认证中心认证
- 由谁授权,就由谁进行身份认证。授权与认证是成对的。如果是以Cookie认证,那就是服务端对token进行解密。如果是服务端保存用户信息,则匹配token值。
- 业务应用自身认证
- 不需要转发,那就意味着业务应用认证规则与认证中心的认证规则必须是一致的。
设计要点
原则上来讲,只要统一Token的产生和校验方式,无论授权与认证的在哪(认证系统或业务系统),也无论用户信息存储在哪(浏览器、服务器),其实都可以实现单点登录的效果。
此次使用.NET Core MVC框架,以Cookie认证通过业务应用自身认证的方式进行同父域的SSO实现。
为什么要使用Cookie认证方式?
1.会话状态分布在客户浏览器,避免大量用户同时在线对服务端内存容量的压力。
2.横向扩展良好性,可按需增减节点。
统一应用授权认证
将以Core的Cookie认证进行实现,那么意味着每个应用对用户信息的加解密方式需要一致。
因此对AddCookie的设置属性DataProtectionProvider或者TicketDataFormat的加密方式进行重写实现。
.NET Core的SSO实现
Cookie认证
认证中心AddCookie的设置
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.Cookie.Name = "Token";
options.Cookie.Domain = ".cg.com";
options.Cookie.HttpOnly = true;
options.ExpireTimeSpan = TimeSpan.FromMinutes();
options.LoginPath = "/Account/Login";
options.LogoutPath = "/Account/Logout";
options.SlidingExpiration = true;
//options.DataProtectionProvider = DataProtectionProvider.Create(new DirectoryInfo(@"D:\sso\key"));
options.TicketDataFormat = new TicketDataFormat(new AesDataProtector());
});
}
业务应用AddCookie的设置
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.Cookie.Name = "Token";
options.Cookie.Domain = ".cg.com";
options.Events.OnRedirectToLogin = BuildRedirectToLogin;
options.Events.OnSigningOut = BuildSigningOut;
options.Cookie.HttpOnly = true;
options.ExpireTimeSpan = TimeSpan.FromMinutes();
options.LoginPath = "/Account/Login";
options.LogoutPath = "/Account/Logout";
options.SlidingExpiration = true;
options.TicketDataFormat = new TicketDataFormat(new AesDataProtector());
});
}
基于设计要点的“统一应用授权认证”这一点,两者的区别不大,ticket的加密方式统一使用了AES,都指定Cookie.Domain = ".cg.com",保证了Cookie同域共享,设置了HttpOnly避免XSS攻击。
两者区别在于:
options.Events.OnRedirectToLogin = BuildRedirectToLogin;
options.Events.OnSigningOut = BuildSigningOut;
这是为了让业务应用引导跳转到认证中心登录页面。OnRedirectToLogin是认证失败跳转。OnSigningOut是注销跳转。
/// <summary>
/// 未登录下,引导跳转认证中心登录页面
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
private static Task BuildRedirectToLogin(RedirectContext<CookieAuthenticationOptions> context)
{
var currentUrl = new UriBuilder(context.RedirectUri);
var returnUrl = new UriBuilder
{
Host = currentUrl.Host,
Port = currentUrl.Port,
Path = context.Request.Path
};
var redirectUrl = new UriBuilder
{
Host = "sso.cg.com",
Path = currentUrl.Path,
Query = QueryString.Create(context.Options.ReturnUrlParameter, returnUrl.Uri.ToString()).Value
};
context.Response.Redirect(redirectUrl.Uri.ToString());
return Task.CompletedTask;
} /// <summary>
/// 注销,引导跳转认证中心登录页面
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
private static Task BuildSigningOut(CookieSigningOutContext context)
{
var returnUrl = new UriBuilder
{
Host = context.Request.Host.Host,
Port = context.Request.Host.Port ?? ,
};
var redirectUrl = new UriBuilder
{
Host = "sso.cg.com",
Path = context.Options.LoginPath,
Query = QueryString.Create(context.Options.ReturnUrlParameter, returnUrl.Uri.ToString()).Value
};
context.Response.Redirect(redirectUrl.Uri.ToString());
return Task.CompletedTask;
}
}
登录注销
认证中心与业务应用两者的登录注册基本一致。
private async Task<IActionResult> SignIn(User user)
{
var claims = new List<Claim>
{
new Claim(JwtClaimTypes.Id,user.UserId),
new Claim(JwtClaimTypes.Name,user.UserName),
new Claim(JwtClaimTypes.NickName,user.RealName),
}; var userPrincipal = new ClaimsPrincipal(new ClaimsIdentity(claims, "Basic")); var returnUrl = HttpContext.Request.Cookies[ReturnUrlKey];
await HttpContext.SignInAsync(userPrincipal,
new AuthenticationProperties
{
IsPersistent = true,
RedirectUri = returnUrl
}); HttpContext.Response.Cookies.Delete(ReturnUrlKey); return Redirect(returnUrl ?? "/");
} private async Task SignOut()
{
await HttpContext.SignOutAsync();
}
HttpContext.SignInAsync的原理
使用的是Cookie认证那么就是通过Microsoft.AspNetCore.Authentication.Cookies库的CookieAuthenticationHandler类的HandleSignInAsync方法进行处理的。
源码地址:https://github.com/aspnet/Security/blob/master/src/Microsoft.AspNetCore.Authentication.Cookies/CookieAuthenticationHandler.cs
protected async override Task HandleSignInAsync(ClaimsPrincipal user, AuthenticationProperties properties)
{
if (user == null)
{
throw new ArgumentNullException(nameof(user));
} properties = properties ?? new AuthenticationProperties(); _signInCalled = true; // Process the request cookie to initialize members like _sessionKey.
await EnsureCookieTicket();
var cookieOptions = BuildCookieOptions(); var signInContext = new CookieSigningInContext(
Context,
Scheme,
Options,
user,
properties,
cookieOptions); DateTimeOffset issuedUtc;
if (signInContext.Properties.IssuedUtc.HasValue)
{
issuedUtc = signInContext.Properties.IssuedUtc.Value;
}
else
{
issuedUtc = Clock.UtcNow;
signInContext.Properties.IssuedUtc = issuedUtc;
} if (!signInContext.Properties.ExpiresUtc.HasValue)
{
signInContext.Properties.ExpiresUtc = issuedUtc.Add(Options.ExpireTimeSpan);
} await Events.SigningIn(signInContext); if (signInContext.Properties.IsPersistent)
{
var expiresUtc = signInContext.Properties.ExpiresUtc ?? issuedUtc.Add(Options.ExpireTimeSpan);
signInContext.CookieOptions.Expires = expiresUtc.ToUniversalTime();
} var ticket = new AuthenticationTicket(signInContext.Principal, signInContext.Properties, signInContext.Scheme.Name); if (Options.SessionStore != null)
{
if (_sessionKey != null)
{
await Options.SessionStore.RemoveAsync(_sessionKey);
}
_sessionKey = await Options.SessionStore.StoreAsync(ticket);
var principal = new ClaimsPrincipal(
new ClaimsIdentity(
new[] { new Claim(SessionIdClaim, _sessionKey, ClaimValueTypes.String, Options.ClaimsIssuer) },
Options.ClaimsIssuer));
ticket = new AuthenticationTicket(principal, null, Scheme.Name);
} var cookieValue = Options.TicketDataFormat.Protect(ticket, GetTlsTokenBinding()); Options.CookieManager.AppendResponseCookie(
Context,
Options.Cookie.Name,
cookieValue,
signInContext.CookieOptions); var signedInContext = new CookieSignedInContext(
Context,
Scheme,
signInContext.Principal,
signInContext.Properties,
Options); await Events.SignedIn(signedInContext); // Only redirect on the login path
var shouldRedirect = Options.LoginPath.HasValue && OriginalPath == Options.LoginPath;
await ApplyHeaders(shouldRedirect, signedInContext.Properties); Logger.SignedIn(Scheme.Name);
}
从源码我们可以分析出流程:
根据ClaimsPrincipal的用户信息序列化后通过加密方式进行加密获得ticket。(默认加密方式是的KeyRingBasedDataProtecto。源码地址:https://github.com/aspnet/DataProtection)
再通过之前的初始化好的CookieOption再AppendResponseCookie方法进行设置Cookie
最后通过Events.RedirectToReturnUrl进行重定向到ReturnUrl。
Ticket加密
两种设置方式
- CookieAuthenticationOptions.DataProtectionProvider
- CookieAuthenticationOptions.TicketDataFormat
DataProtectionProvider
如果做了集群可以设置到共享文件夹,在第一个启动的应用则会创建如下图的文件
options.DataProtectionProvider = DataProtectionProvider.Create(new DirectoryInfo(@"D:\sso\key"));

TicketDataFormat
重写数据加密方式,本次demo使用了是AES.
options.TicketDataFormat = new TicketDataFormat(new AesDataProtector());
internal class AesDataProtector : IDataProtector
{
private const string Key = "!@#13487"; public IDataProtector CreateProtector(string purpose)
{
return this;
} public byte[] Protect(byte[] plaintext)
{
return AESHelper.Encrypt(plaintext, Key);
} public byte[] Unprotect(byte[] protectedData)
{
return AESHelper.Decrypt(protectedData, Key);
}
}
结尾
以上为.NET Core MVC的同域SSO实现思路与细节 。因编写demo的原因代码复用率并不好,冗余代码比较多,大家可以根据情况进行抽离封装。下篇会继续分享跨域SSO的实现。如果对本篇有任何建议与疑问,可以在下方评论反馈给我。
.net core实践系列之SSO-同域实现的更多相关文章
- .net core实践系列之SSO-跨域实现
前言 接着上篇的<.net core实践系列之SSO-同域实现>,这次来聊聊SSO跨域的实现方式.这次虽说是.net core实践,但是核心点使用jquery居多. 建议看这篇文章的朋友可 ...
- .net core实践系列之短信服务-目录
前言 经过两周多的业余时间,终于把该系列的文章写完了.第一次写系列,可能部分关键点并没有覆盖到,如果有疑问的朋友可以随时反馈给我.另外也感谢在我发布文章时给予我方案建议与反馈源码BUG的朋友们.下面是 ...
- .net core实践系列之短信服务-架构设计
前言 上篇<.net core实践系列之短信服务-为什么选择.net core(开篇)>简单的介绍了(水了一篇).net core.这次针对短信服务的架构设计和技术栈的简析. 源码地址:h ...
- .net core实践系列之短信服务-Sikiro.SMS.Api服务的实现
前言 上篇<.net core实践系列之短信服务-架构设计>介绍了我对短信服务的架构设计,同时针对场景解析了我的设计理念.本篇继续讲解Api服务的实现过程. 源码地址:https://gi ...
- .net core实践系列之短信服务-Api的SDK的实现与测试
前言 上一篇<.net core实践系列之短信服务-Sikiro.SMS.Api服务的实现>讲解了API的设计与实现,本篇主要讲解编写接口的SDK编写还有API的测试. 或许有些人会认为, ...
- .net core实践系列之短信服务-Sikiro.SMS.Bus服务的实现
前言 前两篇<.net core实践系列之短信服务-Sikiro.SMS.Api服务的实现>.<.net core实践系列之短信服务-Api的SDK的实现与测试>分别讲解了AP ...
- .net core实践系列之短信服务-为什么选择.net core(开篇)
前言 从今天我将会写.net core实战系列,以我最近完成的短信服务作为例子.该系列将会尽量以最短的时间全部发布出来.源码也将优先开源出来给大家. 源码地址:https://github.com/S ...
- .net core实践系列之短信服务-架构优化
前言 通过前面的几篇文章,讲解了一个短信服务的架构设计与实现.然而初始方案并非100%完美的,我们仍可以对该架构做一些优化与调整. 同时我也希望通过这篇文章与大家分享一下,我的架构设计理念. 源码地址 ...
- .net core实践系列之短信服务-Sikiro.SMS.Job服务的实现
前言 本篇会继续讲解Sikiro.SMS.Job服务的实现,在我写第一篇的时候,我就发现我当时设计的架构里Sikiro.SMS.Job这个可以选择不需要,而使用MQ代替.但是为了说明调度任务使用实现也 ...
随机推荐
- (后台)jxl.read.biff.BiffException: Unable to recognize OLE stream
在excel中打开,另存成xls就可以.
- 标注的SQL拼接语句
方案一 exec sp_executesql N' SELECT T0.[WtmCode], T3.[opCode], T3.[opValue], T3.[CondId] FROM [dbo].[OW ...
- sqlServer 获取最新的一条数据
SQL语句获取最新一条的数据 * from tra.dbo.订单 order by 下单时间 desc --时间倒序排列取第一条 select * from tra.dbo.订单 where 下单时间 ...
- mssql sqlserver存储过程专题
MSSQL存储过程简介及创建方式 mssql_DB_存储过程类型简介 MSSQL sql存储过程创建简介及应用举例 MSSQL SQl server 2008 CLR 存储过程创建举例 MSSQL ...
- EOS之eosio.token合约的部署和发放token
eosio.token智能合约 在eos目录中自带的合约中,有一个eosio.token智能合约,这个智能合约的功能是为账户发放token,token可以用来转账操作. 操作步骤 在eos私有节点操作 ...
- asp.net 调用 WNetAddConnection2 window api 访问被拒绝
通过Asp.net 程序调用局域网另外一台机器文件,显示拒绝访问,单独编写控制台程序正常. 修改iis 应用程序池标识,为管理员用户即可!!
- c/c++ 线性表之双向链表
c/c++ 线性表之双向链表 线性表之双向链表 不是存放在连续的内存空间,链表中的每个节点的next都指向下一个节点,每个节点的before都指向前一个节点,最后一个节点的下一个节点是NULL. 真实 ...
- [UGUI]游戏中的Tips贴图标边缘显示(贴边)
Tips贴图标边缘显示 图左:当左边宽度不足于容纳Tips时,Tips放在右侧显示,顶和图标对齐 图右:当左边宽度足够容纳Tips时,Tips放在左侧显示,顶和图标对齐 适应需求:当图标和Tips是在 ...
- February 13th, 2018 Week 7th Tuesday
You are your greatest asset. 你就是你自己最大的资本. For most of us, there are few things that we can count on ...
- [福大软工] Z班 第2次成绩排行榜
作业链接 http://www.cnblogs.com/easteast/p/7469291.html 评分细则 本次个人项目分数由三部分组成,分别是 (1)博客 - 20分,分数组成如下: 在文章开 ...