Asp.Net Core 中IdentityServer4 實戰之角色授權詳解

  • 2020 年 3 月 30 日
  • 筆記

一、前言

前幾篇文章分享了IdentityServer4密碼模式的基本授權及自定義授權等方式,最近由於改造一個網關服務,用到了IdentityServer4的授權,改造過程中發現比較適合基於Role角色的授權,通過不同的角色來限制用戶訪問不同的Api資源,這裡我就來分享IdentityServer4基於角色的授權詳解。

IdentityServer4 歷史文章目錄

沒有看過之前的幾篇文章,我建議先回過頭看看上面那幾篇文章再來看本篇文章,不過對於大牛來說就可以跳過了。。。。

二、模擬場景

還是按照我的文章風格套路,實戰之前先來模擬下應用場景,無場景的實戰都是耍流氓,模擬場景更能讓大家投入,同時也是自我學習、思考、總結的結晶之處!!!

對於角色授權大家也不陌生,大家比較熟悉的應該是RBAC的設計,這裡就不闡述RBAC,有興趣的可以百度。我們這裡簡單模擬下角色場景
假如有這麼一個數據網關服務服務(下面我統稱為數據網關),客戶端有三種帳號角色(普通用戶、管理員用戶、超級管理員用戶),數據網關針對這三種角色用戶分配不同的數據訪問許可權,場景圖如下:

那麼這種場景我們會怎麼去設計呢?這個場景還算比較簡單,角色比較單一,比較固定,對於這種場景很多人可能會考慮到通過Filter過濾器等方式來實現,這當然可以。不過正對這種場景IdentityServer4中本身就支援角色授權,下面我來給大家分享IdentityServer4的角色授權.

三、角色授權實戰

授權流程

擼程式碼之前我們先整理下IdentityServer4的 角色授權流程圖,我簡單概括畫了下,流程圖如下:

場景圖概括如下:

  • 客戶端分為三種核心角色(普通用戶、管理員用戶、超級管理-老闆)用戶,三種用戶訪問同一個數據網關(API資源)
  • 數據網關(API資源)對這三種用戶角色做了訪問限制。

角色授權流程解釋如下:

  • 第一步: 不同的用戶攜帶用戶密碼等資訊訪問授權中心(ids4)嘗試授權
  • 第二步: 授權中心對用戶授權通過返回access_token給用戶同時聲明用戶的RoleClaim中。。
  • 第三步: 客戶端攜帶拿到的access_token嘗試請求數據網關(API資源)。
  • 第四步:數據網關收到客戶端的第一次請求會到授權中心請求獲得驗證公鑰。
  • 第五步:授權中心返回驗證公鑰數據網關並且快取起來,後面不再到授權中心再次獲得驗證公鑰(只會請求一次,除非重啟服務)。
  • 第六步:數據網關(ids4)通過驗證網關驗證access_token是否驗證通過,並且驗證請求的客戶端用戶聲明的Role是否和請求的API資源約定的的角色一致。如果一致則通過第步返回給用戶端,否則直接拒絕請求.

擼程式碼

程式碼繼續上面幾篇文章的例子的續集,你懂的,就不從零開始擼程式碼啦(強烈建議沒看過上面幾篇的先看下上面的目錄中的幾篇,要不然會一頭霧水,大佬跳過)
要使IdentityServer4實現的授權中心支援角色驗證的支援,我們需要在定義的API資源中添加角色的引入,程式碼如下:
上幾篇文章的授權中心(Jlion.NetCore.Identity.Service)的
程式碼如下:

 /// <summary>   /// 資源   /// </summary>   /// <returns></returns>   public static IEnumerable<ApiResource> GetApiResources()   {       return new List<ApiResource>       {           new ApiResource(OAuthConfig.UserApi.ApiName,OAuthConfig.UserApi.ApiName),       };   }  

加入角色的支援程式碼改造如下:

 /// <summary>   /// 資源   /// </summary>   /// <returns></returns>   public static IEnumerable<ApiResource> GetApiResources()   {        return new List<ApiResource>        {            new ApiResource(                OAuthConfig.UserApi.ApiName,                OAuthConfig.UserApi.ApiName,                new List<string>(){JwtClaimTypes.Role }                ),        };   }  

API資源中添加了角色驗證的支援後,需要在用戶登錄授權成功後聲明Claim用戶的Role資訊,程式碼如下:
改造前程式碼:

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}"),            new Claim(EnumUserClaim.DisplayName.ToString(),"測試用戶"),            new Claim(EnumUserClaim.UserId.ToString(),"10001"),            new Claim(EnumUserClaim.MerchantId.ToString(),"000100001"),        };     }     #endregion   }  

為了保留之前文章的源程式碼,好讓之前的文章源程式碼可追溯,我這裡不在源程式碼上改造升級,我直接新增一個用戶密碼驗證器類,
命名為RoleTestResourceOwnerPasswordValidator,程式碼改造如下:

 /// <summary>   /// 角色授權用戶名密碼驗證器demo   /// </summary>   public class RoleTestResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator   {       public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context)       {           try           {               var userName = context.UserName;               var password = context.Password;                 //驗證用戶,這麼可以到資料庫裡面驗證用戶名和密碼是否正確               var claimList = await ValidateUserByRoleAsync(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>       /// 驗證用戶(角色Demo 專用方法)       /// 這裡和之前區分,主要是為了保留和部落格同步源程式碼       /// </summary>       /// <param name="loginName"></param>       /// <param name="password"></param>       /// <returns></returns>       private async Task<List<Claim>> ValidateUserByRoleAsync(string loginName, string password)       {           //TODO 這裡可以通過用戶名和密碼到資料庫中去驗證是否存在,           // 以及角色相關資訊,我這裡還是使用記憶體中已經存在的用戶和密碼           var user = OAuthMemoryData.GetUserByUserName(loginName);             if (user == null)              throw new Exception("登錄失敗,用戶名和密碼不正確");             //下面的Claim 聲明我為了演示,硬編碼了,           //實際生產環境需要通過讀取資料庫的資訊並且來聲明             return new List<Claim>()           {                 new Claim(ClaimTypes.Name, $"{user.UserName}"),               new Claim(EnumUserClaim.DisplayName.ToString(),user.DisplayName),               new Claim(EnumUserClaim.UserId.ToString(),user.UserId.ToString()),               new Claim(EnumUserClaim.MerchantId.ToString(),user.MerchantId.ToString()),               new Claim(JwtClaimTypes.Role.ToString(),user.Role.ToString())           };       }       #endregion  }  

為了方便演示,我直接把Role定義成了一個公共枚舉EnumUserRole,程式碼如下:

/// <summary>  /// 角色枚舉  /// </summary>  public enum EnumUserRole  {      Normal,      Manage,      SupperManage  }  

GetUserByUserName中硬編碼創建了三個角色的用戶,程式碼如下:

 /// <summary>   /// 為了演示,硬編碼了,   /// 這個方法可以通過DDD設計到底層資料庫去查詢資料庫   /// </summary>   /// <param name="userName"></param>   /// <returns></returns>   public static UserModel GetUserByUserName(string userName)   {        var normalUser = new UserModel()        {           DisplayName = "張三",           MerchantId = 10001,           Password = "123456",           Role = Enums.EnumUserRole.Normal,           SubjectId = "1",           UserId = 20001,           UserName = "testNormal"       };       var manageUser = new UserModel()       {           DisplayName = "李四",           MerchantId = 10001,           Password = "123456",           Role = Enums.EnumUserRole.Manage,           SubjectId = "1",           UserId = 20001,           UserName = "testManage"       };       var supperManageUser = new UserModel()       {           DisplayName = "dotNET博士",           MerchantId = 10001,           Password = "123456",           Role = Enums.EnumUserRole.SupperManage,           SubjectId = "1",           UserId = 20001,           UserName = "testSupperManage"       };       var list = new List<UserModel>() {           normalUser,           manageUser,           supperManageUser       };       return list?.Where(item => item.UserName.Equals(userName))?.FirstOrDefault();   }  

好了,現在用戶授權通過後聲明的Role也已經完成了,我上面使用的是JwtClaimTypes 默認支援的Role,你也可以不使用JwtClaimTypes類,可以自定義類來實現。
最後為了讓新關注我的部落格用戶沒看過之前幾篇文章的用戶不至於一頭霧水,我把註冊ids中間件程式碼還是貼出來,
註冊新的用戶名密碼驗證器到DI中 程式碼如下:

 public void ConfigureServices(IServiceCollection services)   {       services.AddControllers();           #region 資料庫存儲方式       services.AddIdentityServer()          .AddDeveloperSigningCredential()          .AddInMemoryApiResources(OAuthMemoryData.GetApiResources())          //.AddInMemoryClients(OAuthMemoryData.GetClients())          .AddClientStore<ClientStore>()          //.AddResourceOwnerValidator<ResourceOwnerPasswordValidator>()          .AddResourceOwnerValidator<RoleTestResourceOwnerPasswordValidator>()          .AddExtensionGrantValidator<WeiXinOpenGrantValidator>()          .AddProfileService<UserProfileService>();//添加微信端自定義方式的驗證         #endregion   }       public void Configure(IApplicationBuilder app, IWebHostEnvironment env)   {      if (env.IsDevelopment())      {         app.UseDeveloperExceptionPage();      }      //使用IdentityServer4 的中間件      app.UseIdentityServer();        app.UseRouting();      app.UseAuthorization();      app.UseEndpoints(endpoints =>      {           endpoints.MapControllers();      });  }  

授權中心的角色支援程式碼擼完了,我們來改造上幾篇文章中說到的用戶網關服務,這裡我就叫數據網關
項目:Jlion.NetCore.Identity.UserApiService
上一篇關於Asp.Net Core 中IdentityServer4 實戰之 Claim詳解
文章中在數據網關服務中新增了UserController控制器,並添加了一個訪問用戶基本的Claim資訊介面,之前的程式碼如下:

[ApiController]  [Route("[controller]")]  public class UserController : ControllerBase  {        private readonly ILogger<UserController> _logger;        public UserController(ILogger<UserController> logger)      {          _logger = logger;      }        [Authorize]      [HttpGet]      public async Task<object> Get()      {          var userId = User.UserId();          return new          {              name = User.Name(),              userId = userId,              displayName = User.DisplayName(),              merchantId = User.MerchantId(),          };      }  }  

上面的程式碼中Authorize沒有指定Role,那相當於所有的用戶都可以訪問這個介面,接下來,我們在UserController中創建一個只能是超級管理員角色才能訪問的介面,程式碼如下

 [Authorize(Roles =nameof(EnumUserRole.SupperManage))]   [HttpGet("{id}")]   public async Task<object> Get(int id)   {       var userId = User.UserId();       return new       {           name = User.Name(),           userId = userId,           displayName = User.DisplayName(),           merchantId = User.MerchantId(),           roleName=User.Role()//獲得當前登錄用戶的角色       };   }  

到這裡數據網關程式碼也已經改造完了,我們接下來就是運行結果看看是否正確。

運行

我們分別通過命令行運行我們的授權網關服務和數據網關服務,分別如下圖:
授權網關還是指定5000 埠,如下圖:

數據網關跟之前幾篇文章一樣指定 5001 埠,如下圖:

現在授權網關數據網關都已經完美運行起來了,接下來我們通過postman模擬請求。
先來通過普通用戶(testNormal)請求授權中心獲得access_token,如下圖:

請求驗證通過,
再來通過獲取到的access_token 獲取普通介面:

也完美獲取到數據
再來訪問下標註了supperManage超級管理員的角色介面,如下圖:

結果跟預想的一樣,返回了403訪問被拒絕,其他帳號運行也是一樣,我這裡就不一一去運行訪問測試了,有興趣的同學可以到github 上拉起我的源程式碼進行運行測試,
到這裡基於ids4角色授權基礎應用也完成了。

結束語:上面分享學習了IdentityServer4 進行角色授權的實戰例子,但是從上面的例子中有一個不好的弊端,就是每個api訪問都需要硬編碼進行指定Role 這在生產環境中很不現實和靈活,Role角色這個東西都是通過後台自管理,進行靈活配置角色和資源的,那IdentityServer4 有沒有什麼好的方式實現呢?留給大家思考,思考就有學習的目標,也是思維的進步。

部落格系列源程式碼地址:https://github.com/a312586670/NetCoreDemo

感謝語:三月份即將過去,三月份同時也是美好的開始,我的部落格從三月份開始整理分享,傳承著以一起學習,共同進步為目標,自我自律,開始分享相關技術。文章持續性同步至我的微信公眾號【dotNET博士】,這個月來初見成效,一個月內已經榮獲500+以上的粉絲,也感謝大家一直以來對我的關注,你的關注讓我更有動力分享更好的原創技術文章。還沒有關注微信公眾號的,搜索"dotNET博士"關注,或者微信掃下面的二維碼進行關注,同時大家也可以積極的分享或點個右下角的推薦,讓更多人的關注到我的文章。