SSO的系列仍是以.Net Core做爲實踐例子與你們分享,SSO在Web方面複雜度分同域與跨域。本篇先分享同域的設計與實現,跨域將在下篇與你們分享。git
若有須要調試demo的,可把SSO項目部署爲域名http://sso.cg.com/,Web1項目部署爲http://web1.cg.com,http://web2.cg.com,能夠減小配置修改量github
源碼地址:https://github.com/SkyChenSky/Core.SSOweb
單點登陸,全稱爲Single Sign On,在多個應用系統中,用戶只須要登陸一次就能夠訪問全部相互信任的應用系統。跨域
它是一個解決方案,目的是爲了整合企業內多個應用系統,僅由一組帳號只需進行一次登陸,就可被受權訪問多個應用系統。瀏覽器
未登陸狀態訪問業務Web應用會引導到認證中心。安全
用戶在認證中心輸入帳號信息經過登陸後,認證中心會根據用戶信息生成一個具備安全性的token,將以任何方式持久化在瀏覽器。服務器
此後訪問其餘Web應用的時候,必須攜帶此token進行訪問,業務Web應用會經過本地認證或者轉發認證而對token進行校驗。cookie
從上圖能夠簡單的分析出三個關鍵點:session
方式有多種:框架
能夠經過Web框架對用戶信息加密成Token。
Token編碼方式也能夠爲JSON WEB TOKEN(JWT)
也能夠是一段MD5,經過字典匹配保存在服務器用戶信息與MD5值
瀏覽器存儲有三種方式:
做爲擁有會失效的會話狀態,更因選擇Cookie存儲。那麼Cookie的使用是能夠在同域共享的,所以在實現SSO的時候複雜度又分爲同域與跨域。
同域的共享比較簡單,在應用設置Cookie的Domain屬性進行設置,就能夠完美的解決。
校驗分兩種狀況:
原則上來說,只要統一Token的產生和校驗方式,不管受權與認證的在哪(認證系統或業務系統),也不管用戶信息存儲在哪(瀏覽器、服務器),其實均可以實現單點登陸的效果。
這次使用.NET Core MVC框架,以Cookie認證經過業務應用自身認證的方式進行同父域的SSO實現。
1.會話狀態分佈在客戶瀏覽器,避免大量用戶同時在線對服務端內存容量的壓力。
2.橫向擴展良好性,可按需增減節點。
將以Core的Cookie認證進行實現,那麼意味着每一個應用對用戶信息的加解密方式須要一致。
所以對AddCookie的設置屬性DataProtectionProvider或者TicketDataFormat的加密方式進行重寫實現。
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(30); 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()); }); }
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(30); 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 ?? 80, }; 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(); }
使用的是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。
兩種設置方式
若是作了集羣能夠設置到共享文件夾,在第一個啓動的應用則會建立以下圖的文件
options.DataProtectionProvider = DataProtectionProvider.Create(new DirectoryInfo(@"D:\sso\key"));
重寫數據加密方式,本次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的實現。若是對本篇有任何建議與疑問,能夠在下方評論反饋給我。