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

  • 2020 年 3 月 11 日
  • 筆記

一、前言

查閱了大多數相關資料,查閱到的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 實現的簡單授權中心的思想也就完成了,後續繼續學習,有錯誤地方還請留言指出!感謝!!!