1. 程式人生 > >eShopOnContainers 知多少[3]:Identity microservice

eShopOnContainers 知多少[3]:Identity microservice

首先感謝曉晨Master和EdisonChou的審稿!也感謝正在閱讀的您!

引言

通常,服務所公開的資源和 API 必須僅限受信任的特定使用者和客戶端訪問。那進行 API 級別信任決策的第一步就是身份認證——確定使用者身份是否可靠。

在微服務場景中,身份認證通常統一處理。一般有兩種實現形式:

  1. 基於API 閘道器中心化認證:要求客戶端必須都通過閘道器訪問微服務。(這就要求提供一種安全機制來認證請求是來自於閘道器。)
    基於API 閘道器中心化認證

  2. 基於安全令牌服務(STS)認證:所有的客戶端先從STS獲取令牌,然後請求時攜帶令牌完成認證。
    基於安全令牌服務(STS)認證

而本節所講的Identity microservice就是使用第二種身份認證方式。

服務簡介

Identity microservice 主要用於統一的身份認證和授權,為其他服務提供支撐。

提到認證,大家最熟悉不過的當屬Cookie認證了,它也是目前使用最多的認證方式。但Cookie認證也有其侷限性:不支援跨域、移動端不友好等。而從當前的架構來看,需要支援移動端、Web端、微服務間的交叉認證授權,所以傳統的基於Cookie的本地認證方案就行不通了。我們就需要使用遠端認證的方式來提供統一的認證授權機制。
而遠端認證方式當屬:OAuth2.0和OpenID Connect了。藉助OAuth2.0和OpenID Connect即可實現類似下圖的認證體系:

而如何實現呢,藉助:

  1. ASP.NET Core Identity
  2. IdentityServer4

基於Cookie的認證和基於Token的認證的差別如下所示:

Cookie-Based Auth VS Token-Based Auth

架構模式

該微服務作為支撐服務,並沒有選擇複雜的架構模式,使用了MVC單層架構,使用EF Core ORM框架用於資料持久化,SQL Server資料庫。使用Autofac IOC框架替換了預設依賴注入框架。

專案結構如下所示:
Identity.API 專案結構

核心技術選型:

PS:對ASP.NET Core Identity、IdentityServer4以及OAuth2.0不瞭解的,請先行閱讀文末參考資料補課!!!

下面就著重講解ASP.NET Core Identity和IdentityServer4在本服務中的使用。

ASP.NET Core Identity && IdentityServer4簡介

ASP.NET Core Identity用於構建ASP.NET Core Web應用程式的成員資格系統,包括成員資格,登入和使用者資料(包括登入資訊、角色和宣告)。
ASP.NET Core Identity封裝了User、Role、Claim等身份資訊,便於我們快速完成登入功能的實現,並且支援第三方登入(Google、Facebook、QQ、Weixin等,支援開箱即用[第三方身份提供商列表]),以及雙重驗證,同時內建支援Bearer 認證(令牌認證)。

雖然ASP.NET Core Identity已經完成了絕大多數的功能,且支援第三方登入(第三方為其使用者頒發令牌),但若要為本地使用者頒發令牌,則需要自己實現令牌的頒發和驗證邏輯。換句話說,我們需要自行實現OpenId Connect協議。

OpenID Connect 1.0 是基於OAuth 2.0協議之上的簡單身份層,它允許客戶端根據授權伺服器的認證結果最終確認終端使用者的身份,以及獲取基本的使用者資訊。

而IdentityServer4就是為ASP.NET Core量身定製的實現了OpenId Connect和OAuth2.0協議的認證授權中介軟體。IdentityServer4在ASP.NET Core Identity的基礎上,提供令牌的頒發驗證等。

認證流程簡介

在ASP.NET Core中使用的是基於申明(Claim)的認證,而什麼是申明(Cliam)呢?

Claim 是關於一個人或組織的某個主題的陳述,比如:一個人的名稱,角色,個人喜好,種族,特權,社團,能力等等。它本質上就是一個鍵值對,是一種非常通用的儲存使用者資訊的方式,可以很容易的將認證和授權分離開來,前者用來表示使用者是/不是什麼,後者用來表示使用者能/不能做什麼。在認證階段我們通過使用者資訊獲取到使用者的Claims,而授權便是對這些的Claims的驗證,如:是否擁有Admin的角色,姓名是否叫XXX等等。

認證主要與以下幾個核心物件打交道:

  1. Claim(身份資訊)
  2. ClaimsIdentity(身份證)
  3. ClaimsPrincipal (身份證持有者)
  4. AuthorizationToken (授權令牌)
  5. IAuthenticationScheme(認證方案)
  6. IAuthenticationHandler(與認證方案對應的認證處理器)
  7. IAuthenticationService (向外提供統一的認證服務介面)

那其認證流程是怎樣的呢?

使用者開啟登入介面,輸入使用者名稱密碼先行登入,服務端先行校驗使用者名稱密碼是否有效,有效則返回使用者例項(User),這時進入認證準備階段,根據使用者例項攜帶的身份資訊(Claim),建立身份證(ClaimsIdentity),然後將身份證交給身份證持有者(ClaimsPrincipal)持有。接下來進入真正的認證階段,根據配置的認證方案(IAuthenticationScheme),使用相對應的認證處理器(IAuthenticationHandler)進行認證 。認證成功後發放授權令牌(AuthorizationToken)。該授權令牌包含後續授權階段需要的全部資訊。

授權流程簡介

授權就是對於使用者身份資訊(Claims)的驗證,,授權又分以下幾種種:

  1. 基於Role的授權
  2. 基於Scheme的授權
  3. 基於Policy的授權

授權主要與以下幾個核心物件打交道:

  1. IAuthorizationRequirement(授權條件)
  2. IAuthorizationService(授權服務)
  3. AuthorizationPolicy(授權策略)
  4. IAuthorizationHandler (授權處理器)
  5. AuthorizationResult(授權結果)

那授權流程是怎樣的呢?

當收到授權請求後,由授權服務(IAuthorizationService)根據資源上指定的授權策略(AuthorizationPolicy)中包含的授權條件(IAuthorizationRequirement),找到相對應的授權處理器(IAuthorizationHandler )來判斷授權令牌中包含的身份資訊是否滿足授權條件,並返回授權結果。

核心物件

中介軟體整合

簡單瞭解了下認證和授權流程後,我們來了解Identity microservice是如何整合相關中介軟體的。

1. 首先是對映自定義擴充套件的User和Role

 // 對映自定義的User,Role
services.AddIdentity<ApplicationUser, IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>()//配置使用EF持久化儲存
    .AddDefaultTokenProviders();//配置預設的TokenProvider用於變更密碼和修改email時生成Token

2. 配置IdentityServer服務

// Adds IdentityServer
services.AddIdentityServer(x =>
{
    x.IssuerUri = "null";
    x.Authentication.CookieLifetime = TimeSpan.FromHours(2);
})
.AddSigningCredential(Certificate.Get())
.AddAspNetIdentity<ApplicationUser>()
.AddConfigurationStore(options =>
{
    options.ConfigureDbContext = builder => builder.UseSqlServer(connectionString,
     sqlServerOptionsAction: sqlOptions =>
     {
         sqlOptions.MigrationsAssembly(migrationsAssembly);
         //Configuring Connection Resiliency: https://docs.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency 
         sqlOptions.EnableRetryOnFailure(maxRetryCount: 15, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null);
     });
})
.AddOperationalStore(options =>
{
    options.ConfigureDbContext = builder => builder.UseSqlServer(connectionString,
     sqlServerOptionsAction: sqlOptions =>
     {
         sqlOptions.MigrationsAssembly(migrationsAssembly);
         //Configuring Connection Resiliency: https://docs.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency 
         sqlOptions.EnableRetryOnFailure(maxRetryCount: 15, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null);
     });
})
.Services.AddTransient<IProfileService, ProfileService>();

IdentityServer預設直接在記憶體中儲存配置資料(客戶端和資源)和操作資料(令牌,程式碼和和使用者的授權資訊consents)。這顯然在生產環境是不合適的,如果服務所在主機宕機,那麼記憶體中的資料就會丟失,所以有必要持久化到資料庫。
其中AddConfigurationStoreAddOperationalStore擴充套件方法就是用來來指定配置資料和操作資料基於EF進行持久化。

3. 新增IdentityServer中介軟體

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
     // .....
    // Adds IdentityServer
    app.UseIdentityServer();
}

4. 預置種子資料

從已知的體系結構來說,我們需要預置Client和Resource:

  1. Client
public static IEnumerable<Client> GetClients(Dictionary<string,string> clientsUrl)
{
    return new List<Client>
    {
        // SPA OpenId Client Client(Implicit)
        new Client
        // Xamarin Client(Hybrid)
        new Client
        // MVC Client(Hybrid)
        new Client
        // MVC TEST Client(Hybrid)
        new Client
        // Locations Swagger UI(Implicit)
        new Client
        // Marketing Swagger UI(Implicit)
        new Client
        // Basket Swagger UI(Implicit)
        new Client
        // Ordering Swagger UI(Implicit)
        new Client
        // Mobile Shopping Aggregattor Swagger UI(Implicit)
        new Client
        // Web Shopping Aggregattor Swagger UI(Implicit)
        new Client
    };
}
  1. IdentityResources
public static IEnumerable<IdentityResource> GetResources()
{
    return new List<IdentityResource>
    {
        new IdentityResources.OpenId(),
        new IdentityResources.Profile()
    };
}
  1. ApiResources
public static IEnumerable<ApiResource> GetApis()
{
    return new List<ApiResource>
    {
        new ApiResource("orders", "Orders Service"),
        new ApiResource("basket", "Basket Service"),
        new ApiResource("marketing", "Marketing Service"),
        new ApiResource("locations", "Locations Service"),
        new ApiResource("mobileshoppingagg", "Mobile Shopping Aggregator"),
        new ApiResource("webshoppingagg", "Web Shopping Aggregator"),
        new ApiResource("orders.signalrhub", "Ordering Signalr Hub")
    };
}

5. 遷移資料庫上下文

下面就把提前在程式碼預置的種子資料遷移到資料庫中,我們如何做呢?IdentityServer為配置資料和操作資料分別定義了DBContext用於持久化,配置資料對應ConfigurationDbContext,操作資料對應PersistedGrantDbContext。程式碼如下所示:

public static void Main(string[] args)
{
    BuildWebHost(args)
        .MigrateDbContext<PersistedGrantDbContext>((_, __) => { })//遷移操作資料庫
        .MigrateDbContext<ApplicationDbContext>((context, services) =>
        {
            var env = services.GetService<IHostingEnvironment>();
            var logger = services.GetService<ILogger<ApplicationDbContextSeed>>();
            var settings = services.GetService<IOptions<AppSettings>>();

            new ApplicationDbContextSeed()
                .SeedAsync(context, env, logger, settings)
                .Wait();
        })//遷移使用者資料庫
        .MigrateDbContext<ConfigurationDbContext>((context,services)=> 
        {
            var configuration = services.GetService<IConfiguration>();

            new ConfigurationDbContextSeed()
                .SeedAsync(context, configuration)
                .Wait();
        })//遷移配置資料庫
        .Run();
}

至此,本服務的核心程式碼已解析完畢。

最終的生成的資料庫如下圖所示:
IdentityDb

最後

本文從業務和技術上對本服務進行剖析,介紹了其技術選型,並緊接著簡要介紹了ASP.NET Core Identity和IdentityServer4,最後分析原始碼,一步步揭開其神祕的面紗。至於客戶端和其他微服務服務如何使用Identity microservice進行認證和授權,我將在後續文章再行講解。

如果對ASP.NET Core Idenity和IdentityServer4不太瞭解,建議大家部落格園閱讀
的部落格進行系統學習後,再重讀本文,相信你對Identity microservice的實現機制豁然開朗。

參考資料