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

一、前言

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

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

二、應用實戰

2.1 模擬場景

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

這張架構圖缺點:

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

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

身份授權從業務系統中拆分出來后,有了如下的優勢:

  • 授權服務不受業務的影響,如果業務網關宕機了,那至少不會影響代理商網關的業務授權系統的使用
  • 授權服務一旦建立,一般就很難進行升級,除非特殊情況。
  • 在敏捷開發中,業務系統可能發布頻繁,電商業務系統可能每天都是在頻繁升級更新,這樣也不至于影響了授權系統服務導致代理商業務受到影響

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

支付網關服務抽離后的優勢:

  • 支付網關服務更新不會太頻繁,可以減少整個系統的因為發布導致的一系列問題,增強穩定性
  • 支付系統出現宕機不影響整個電商系統的使用,用戶還可以瀏覽商品等等其他操作,技術和運維人員也比較好排查定位問題所在;提升用戶體驗,同時提升排查問題的效率。

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

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

由于授權網關服務之前單獨抽離出來了,這次把支付業務網關拆分出來就也比較順利,一下子就完成了電商系統的架構升級。今天這篇文章的目的架構升級也就完成了,想要深入后續電商系統架構升級的同學可以關注后續給大家帶來的微服務的相關教程,到時繼續以這個例子來進行微服務架構上的演變升級,敬請大家關注。好了下面我們來回歸該升級的和核心主題授權網關服務 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 實現的簡單授權中心的思想也就完成了,后續繼續學習,有錯誤地方還請留言指出!感謝!!!

下一篇
posted @ 2020-03-11 08:06  Jlion  閱讀(5151)  評論(46編輯  收藏
最新chease0ldman老人