Asp.Net Core 中IdentityServer4 受權中心之應用實戰

1、前言

查閱了大多數相關資料,查閱到的IdentityServer4 的相關文章大可能是比較簡單而且可能是翻譯官網的文檔編寫的,我這裏在
Asp.Net Core 中IdentityServer4 的應用分析中會以一個電商系統架構升級過程當中廣泛會遇到的場景進行實戰性講述分析,同時最後會把個人實戰性的代碼放到github 上,敬請你們關注!html

這裏就直接開始擼代碼,概念性東西就已經不概述了,想要了解概念推薦你們查看我以前的文章和官方文檔:git

2、應用實戰

2.1 模擬場景

最初小團隊的電商系統場景以下圖:
github

這張架構圖缺點:數據庫

  • 發佈頻繁,發佈影響整個電商系統
  • 很難作到敏捷開發
  • 維護性可能會存在必定的弊端,主要看內部架構狀況。

大多數小電商團隊對於多客戶端登陸受權來講可能已經實現了Oauth 2.0 的身份受權驗證,可是是和電商業務集成在一個網關裏面,這樣不是很好的方式;因爲公司業務橫向擴大,產品經理調研了代理商業務,最終讓技術開發代理商業務系統。架構師出於後續發展的各方面考慮,把代理商業務單獨創建了一個獨立的網關,而且把受權服務一併給獨立出來,調整後的電商系統架構圖以下:
api

身份受權從業務系統中拆分出來後,有了以下的優點:架構

  • 受權服務不受業務的影響,若是業務網關宕機了,那至少不會影響代理商網關的業務受權系統的使用
  • 受權服務一旦創建,通常就很難進行升級,除非特殊狀況。
  • 在敏捷開發中,業務系統可能發佈頻繁,電商業務系統可能天天都是在頻繁升級更新,這樣也不至於影響了受權系統服務致使代理商業務受到影響

代理商業務引入進來後,同時又增長了秒殺活動,發現成交量大大增大,支付訂單集中在某一時刻翻了十幾倍,這時候整個電商業務API網關已經扛不住了,負載了幾臺可能也有點吃力;開發人員通過跟架構師一塊兒討論,得出了扛不住的緣由:主要是秒殺活動高併發的支付,以致於整個電商業務系統受到影響,故準備把支付系統從業務系統中拆分出成獨立的支付網關,並作了必定的負載,成功解決了以上問題,這時候整個電商系統架構圖就演變成以下:
併發

支付網關服務抽離後的優點:app

  • 支付網關服務更新不會太頻繁,能夠減小整個系統的由於發佈致使的一系列問題,加強穩定性
  • 支付系統出現宕機不影響整個電商系統的使用,用戶還能夠瀏覽商品等等其餘操做,技術和運維人員也比較好排查定位問題所在;提高用戶體驗,同時提高排查問題的效率。

受權中心:單獨一個服務網關,訪問支付業務網關電商業務網關代理商業務網關都須要先經過受權中心得到受權拿到訪問令牌AccessToken 才能正常的訪問這些網關,這樣受權模塊就不會受任何的業務影響,同時各個業務網關也不須要寫一樣的受權業務的代碼;業務網關僅僅只需關注自己的業務便可,受權中心僅僅只須要關注維護受權;通過這樣升級改造後整個系統維護性獲得很大的提升,相關的業務也能夠針對具體狀況進行選擇性的擴容。運維

上面的電商網關演變架構圖中我這裏沒有畫出具體的請求流向,偷了個賴,這裏仍是先把OAuth2.0 的受權大致的流程圖單獨貼出來:
dom

因爲受權網關服務以前單獨抽離出來了,此次把支付業務網關拆分出來就也比較順利,一會兒就完成了電商系統的架構升級。今天這篇文章的目的架構升級也就完成了,想要深刻後續電商系統架構升級的同窗能夠關注後續給你們帶來的微服務的相關教程,到時繼續以這個例子來進行微服務架構上的演變升級,敬請你們關注。好了下面咱們來回歸該升級的和核心主題受權網關服務 IdentityServer4 的應用。

2.2 IdentityServer4 密碼受權模式

受權網關服務

靜態內存配置方式

定義資源

分資源分爲身份資源(Identity resources)和API資源(API resources)。
咱們先建立Jlion.NetCore.Identity.Service 網關服務項目,在網關服務中添加受保護的API資源,建立OAuthMemoryData 類代碼以下:

/// <summary>
/// Api資源 靜態方式定義
/// </summary>
/// <returns></returns>
public static IEnumerable<ApiResource> GetApiResources()
{
       return new List<ApiResource>
       {
            new ApiResource(OAuthConfig.UserApi.ApiName,OAuthConfig.UserApi.ApiName),
       };
}

定義客戶端Client

接下來OAuthMemoryData 類中定義一個客戶端應用程序的Client,咱們將使用它來訪問咱們的API資源代碼以下:

public static IEnumerable<Client> GetClients()
{
       return new List<Client>
       {
           new Client()
           {
               ClientId =OAuthConfig.UserApi.ClientId,
               AllowedGrantTypes = new List<string>()
               {
                   GrantTypes.ResourceOwnerPassword.FirstOrDefault(),//Resource Owner Password模式
               },
               ClientSecrets = {new Secret(OAuthConfig.UserApi.Secret.Sha256()) },
               AllowedScopes= {OAuthConfig.UserApi.ApiName},
               AccessTokenLifetime = OAuthConfig.ExpireIn,
           },
      };
 }
  • AllowedGrantTypes :配置受權類型,能夠配置多個受權類型
  • ClientSecrets:客戶端加密方式
  • AllowedScopes:配置受權範圍,這裏指定哪些API 受此方式保護
  • AccessTokenLifetime:配置Token 失效時間
  • GrantTypes:受權類型,這裏使用的是密碼模式ResourceOwnerPassword

代碼中能夠看到有一個OAuthConfig 類,這個類是我單獨建的,是用於統一管理,方便維護,代碼以下:

public class OAuthConfig
 {
        /// <summary>
        /// 過時秒數
        /// </summary>
        public const int ExpireIn = 36000;

        /// <summary>
        /// 用戶Api相關
        /// </summary>
        public static class UserApi
        {
            public static string ApiName = "user_api";

            public static string ClientId = "user_clientid";

            public static string Secret = "user_secret";
        }
 }

若是後續架構升級,添加了其餘的網關服務,則只須要在這裏添加所須要保護的API 資源,也能夠經過讀取數據庫方式讀取受保護的Api資源。

接下來OAuthMemoryData 類添加測試用戶,代碼以下:

/// <summary>
/// 測試的帳號和密碼
/// </summary>
/// <returns></returns>
public static List<TestUser> GetTestUsers()
{
    return new List<TestUser>
    {
        new TestUser()
        {
             SubjectId = "1",
             Username = "test",
             Password = "123456"
        }
    };
}

上面受保護的資源,和客戶端以及測試帳號都已經創建好了,如今須要把IdentityServer4 註冊到DI中:
Startup 中的ConfigureServices 代碼以下:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();

    #region 內存方式
    services.AddIdentityServer()
        .AddDeveloperSigningCredential()
        .AddInMemoryApiResources(OAuthMemoryData.GetApiResources())
        .AddInMemoryClients(OAuthMemoryData.GetClients())
        .AddTestUsers(OAuthMemoryData.GetTestUsers());
    #endregion

}

代碼解讀:

  • AddDeveloperSigningCredential:添加證書加密方式,執行該方法,會先判斷tempkey.rsa證書文件是否存在,若是不存在的話,就建立一個新的tempkey.rsa證書文件,若是存在的話,就使用此證書文件。
  • AddInMemoryApiResources:把受保護的Api資源添加到內存中
  • AddInMemoryClients :客戶端配置添加到內存中
  • AddTestUsers :測試的用戶添加進來

最後經過UseIdentityServer()須要把IdentityServer4 中間件添加到Http管道中,代碼以下:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
     if (env.IsDevelopment())
     {
         app.UseDeveloperExceptionPage();
     }
   
     app.UseIdentityServer();

     app.UseRouting();

     app.UseAuthorization();

     app.UseEndpoints(endpoints =>
     {
        endpoints.MapControllers();
     });
}

好了,如今受權網關服務代碼已經完成,如今直接經過命令行方式啓動,命令行啓動以下,我指定5000端口,以下圖:

電商用戶網關Api項目

如今我來新建一個WebApi 大的用戶網關服務項目,取名爲Jlion.NetCore.Identity.UserApiService,新建後會默認有一個天氣預報的api接口,代碼以下:

[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
    private static readonly string[] Summaries = new[]
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    };

    private readonly ILogger<WeatherForecastController> _logger;

    public WeatherForecastController(ILogger<WeatherForecastController> logger)
    {
        _logger = logger;
    }

    [HttpGet]
    public IEnumerable<WeatherForecast> Get()
    {
        var rng = new Random();
        return Enumerable.Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = rng.Next(-20, 55),
            Summary = Summaries[rng.Next(Summaries.Length)]
        })
        .ToArray();
    }
}

接下來在Startup 類中添加受權網關服務的配置到DI中,代碼以下:

public void ConfigureServices(IServiceCollection services)
 {
       services.AddControllers();

       services.AddAuthorization();
       services.AddAuthentication("Bearer")
           .AddIdentityServerAuthentication(options =>
           {
               options.Authority = "http://localhost:5000";    //配置Identityserver的受權地址
               options.RequireHttpsMetadata = false;           //不須要https    
               options.ApiName = OAuthConfig.UserApi.ApiName;  //api的name,須要和config的名稱相同
           });
  }

這裏的options.ApiName 須要和網關服務中的Api 資源配置中的ApiName 一致

接下來須要把受權和認證中間件分別註冊到Http 管道中,代碼以下:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }


    app.UseRouting();

    app.UseAuthentication();
    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}

如今受權服務網關啓用已經完成,只須要在須要保護的Controller 中添加 Authorize 過濾器便可,如今我也經過命令行把須要保護的網關服務啓動,如圖:

如今我經過postman 工具來單獨訪問 用戶網關服務API,不攜帶任何信息的狀況下,如圖:

從訪問結果能夠看出返回401 Unauthorized 未受權。

咱們接下來再來訪問受權服務網關,如圖:

請求網關服務中body中攜帶了用戶名及密碼等相關信息,這是返回了access_token 及有效期等相關信息,咱們再拿access_token 來繼續上面的操做,訪問用戶業務網關的接口,如圖:

訪問結果中已經返回了咱們所須要的接口數據,你們目前已經對密碼模式的使用有了必定的瞭解,可是這時候可能會有人問我,我生產環境中可能須要經過數據庫的方式進行用戶信息的判斷,以及客戶端受權方式須要更加靈活的配置,可經過後臺來配置ClientId以及受權方式等,那應該怎麼辦呢?下面我再來給你們帶來生存環境中的實現方式。

數據庫匹配驗證方式

咱們須要經過用戶名和密碼到數據庫中驗證方式則須要實現IResourceOwnerPasswordValidator 接口,並實現ValidateAsync 驗證方法,簡單的代碼以下:

public class ResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator
{
    public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
    {
        try
        {
            var userName = context.UserName;
            var password = context.Password;

            //驗證用戶,這麼能夠到數據庫裏面驗證用戶名和密碼是否正確
            var claimList = await ValidateUserAsync(userName, password);

            // 驗證帳號
            context.Result = new GrantValidationResult
            (
                subject: userName,
                authenticationMethod: "custom",
                claims: claimList.ToArray()
             );
       }
       catch (Exception ex)
       {
            //驗證異常結果
            context.Result = new GrantValidationResult()
            {
                IsError = true,
                Error = ex.Message
             };
       }
  }

    #region Private Method
    /// <summary>
    /// 驗證用戶
    /// </summary>
    /// <param name="loginName"></param>
    /// <param name="password"></param>
    /// <returns></returns>
    private async Task<List<Claim>> ValidateUserAsync(string loginName, string password)
    {
        //TODO 這裏能夠經過用戶名和密碼到數據庫中去驗證是否存在,
        // 以及角色相關信息,我這裏仍是使用內存中已經存在的用戶和密碼
        var user = OAuthMemoryData.GetTestUsers();

        if (user == null)
            throw new Exception("登陸失敗,用戶名和密碼不正確");

        return new List<Claim>()
        {
            new Claim(ClaimTypes.Name, $"{loginName}"),
        };
    }
    #endregion
}

用戶密碼驗證器已經實現完成,如今須要把以前的經過AddTestUsers 方式改爲AddResourceOwnerValidator<ResourceOwnerPasswordValidator>() 方式,修改後的代碼以下:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();

    #region 數據庫存儲方式
    services.AddIdentityServer()
        .AddDeveloperSigningCredential()
        .AddInMemoryApiResources(OAuthMemoryData.GetApiResources())
        .AddInMemoryClients(OAuthMemoryData.GetClients())
        //.AddTestUsers(OAuthMemoryData.GetTestUsers());
        .AddResourceOwnerValidator<ResourceOwnerPasswordValidator>();
   #endregion
}

目前已經實現了用戶名和密碼數據庫驗證的方式,可是如今有人會考慮另一個場景,客戶端的受權方式等也須要經過後臺可配置的方式,這樣比較靈活,不經過代碼中靜態配置的方式,那應該這麼辦呢?
官方考慮的很周到,咱們可使用IClientStore 接口,同時須要實現FindClientByIdAsync 方法,代碼以下:

public class ClientStore : IClientStore
{
    public async Task<Client> FindClientByIdAsync(string clientId)
    {
        #region 用戶名密碼
        var memoryClients = OAuthMemoryData.GetClients();
        if (memoryClients.Any(oo => oo.ClientId == clientId))
        {
           return memoryClients.FirstOrDefault(oo => oo.ClientId == clientId);
        }
        #endregion

        #region 經過數據庫查詢Client 信息
        return GetClient(clientId);
        #endregion
    }

    private Client GetClient(string client)
    {
        //TODO 根據數據庫查詢
        return null;
    }
}

StartupConfigureServices 代碼AddInMemoryClients 改爲AddClientStore<> 代碼以下:

public void ConfigureServices(IServiceCollection services)
{
     services.AddControllers();

     #region 數據庫存儲方式
     services.AddIdentityServer()
         .AddDeveloperSigningCredential()
         .AddInMemoryApiResources(OAuthMemoryData.GetApiResources())
         //.AddInMemoryClients(OAuthMemoryData.GetClients())
         .AddClientStore<ClientStore>()
         .AddResourceOwnerValidator<ResourceOwnerPasswordValidator>();
    #endregion
 }

好了數據庫查詢匹配方式也已經改造完了,業務網關服務不須要改動如何代碼,運行結果這裏就不在運行演示了。Demo 代碼已經上傳到github 上了,github 源代碼地址https://github.com/a312586670/IdentityServerDemo

結語:經過IdentityServer4 實現的簡單受權中心的思想也就完成了,後續繼續學習,有錯誤地方還請留言指出!感謝!!!

相關文章
相關標籤/搜索