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

  • 相关阅读:
    realsense d435i qt 测试
    realsense d435i 数据 测试
    realsense d435i测试
    ubuntu torch GPU yolov5
    IfcLayeredItem
    ubuntu大服务器 pytorch环境配置
    condarc内容
    realsense point cloud
    yolov5 环境配置
    pip error
  • 原文地址:https://www.cnblogs.com/skychen1218/p/9773466.html
Copyright © 2011-2022 走看看