歡迎光臨
每天分享高質量文章

.NET Core實踐系列之SSO-同域實現

作者:陳珙

鏈接:https://www.cnblogs.com/skychen1218/p/9773466.html

前言


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儲存。那麼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是註銷跳轉。

///

/// 未登錄下,引導跳轉認證中心登錄頁面

///

///

///

private static Task BuildRedirectToLogin(RedirectContext 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;

}

///

/// 註銷,引導跳轉認證中心登錄頁面

///

///

///

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 SignIn(User user)

{

    var claims = new List

    {

        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:ssokey”));

TicketDataFormat

重寫資料加密方式,本次demo使用了是AES.

options.TicketDataFormat = new TicketDataFormat(new AesDataProtector());

internal class AesDataProtector : IDataProtector

{

    private const string Key = “[email protected]#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的原因代碼復用率並不好,冗餘代碼比較多,大家可以根據情況進行抽離封裝。


●編號169,輸入編號直達本文

●輸入m獲取文章目錄

推薦↓↓↓

Web開發

更多推薦18個技術類公眾微信

涵蓋:程式人生、演算法與資料結構、黑客技術與網絡安全、大資料技術、前端開發、Java、Python、Web開發、安卓開發、iOS開發、C/C++、.NET、Linux、資料庫、運維等

赞(0)

分享創造快樂