zoukankan      html  css  js  c++  java
  • .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(30);
                       options.LoginPath = "/Account/Login";
                       options.LogoutPath = "/Account/Logout";
                       options.SlidingExpiration = true;
                       //options.DataProtectionProvider = DataProtectionProvider.Create(new DirectoryInfo(@"D:ssokey"));
                       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(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();
            }

    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);
            }
    View Code

    从源码我们可以分析出流程:

    根据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:ssokey"));

    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的实现。如果对本篇有任何建议与疑问,可以在下方评论反馈给我。

  • 相关阅读:
    LintCode "Maximum Gap"
    LintCode "Wood Cut"
    LintCode "Expression Evaluation"
    LintCode "Find Peak Element II"
    LintCode "Remove Node in Binary Search Tree"
    LintCode "Delete Digits"
    LintCode "Binary Representation"
    LeetCode "Game of Life"
    LintCode "Coins in a Line"
    LintCode "Word Break"
  • 原文地址:https://www.cnblogs.com/skychen1218/p/9773466.html
Copyright © 2011-2022 走看看