.net core实践系列之SSO-同域实现

前言

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

效果图

SSO简介

单点登陆,全称为Single Sign On,在多个应用系统中,用户只须要登陆一次就能够访问全部相互信任的应用系统。跨域

它是一个解决方案,目的是为了整合企业内多个应用系统,仅由一组帐号只需进行一次登陆,就可被受权访问多个应用系统。浏览器

流程描述

未登陆状态访问业务Web应用会引导到认证中心。安全

用户在认证中心输入帐号信息经过登陆后,认证中心会根据用户信息生成一个具备安全性的token,将以任何方式持久化在浏览器。服务器

此后访问其余Web应用的时候,必须携带此token进行访问,业务Web应用会经过本地认证或者转发认证而对token进行校验。cookie

从上图能够简单的分析出三个关键点:session

  • 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:\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(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:\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的实现。若是对本篇有任何建议与疑问,能够在下方评论反馈给我。

相关文章
相关标签/搜索