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

一、前言

前幾篇文章分享了IdentityServer4密碼模式的基本授權及自定義授權等方式,最近由於改造一個網關服務,也用到了IdentityServer4的授權,改造過程中發現比較適合基於Role角色的授權,通過不同的角色來限制用戶訪問不同的Api資源,當時編寫的時候我是使用了一個中間件來實現,想想可能基於Role角色的方式來實現會更佳,不過也只是其中一小部分適合,這裏我就來分享IdentityServer4基於角色的授權詳解。

IdentityServer4 歷史文章目錄

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

二、模擬場景

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

對於角色授權大家也不陌生,大家比較熟悉的應該是RBAC的設計,這裏就不闡述RBAC,有興趣的可以百度。我們這裏簡單模擬下角色場景
假如有這麼一個數據網關服務服務(下面我統稱爲數據網關),客戶端有三種賬號角色(普通用戶、管理員用戶、超級管理員用戶),數據網關針對這三種角色用戶分配不同的數據訪問權限,場景圖如下:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-VmwMOujA-1585400781825)(https://img2020.cnblogs.com/blog/824291/202003/824291-20200328175216974-1045194802.jpg)]

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

三、角色授權實戰

授權流程

擼代碼之前我們先整理下IdentityServer4的 角色授權流程圖,我簡單概括畫了下,流程圖如下:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-xAmZG5y0-1585400781826)(https://img2020.cnblogs.com/blog/824291/202003/824291-20200328175347833-226144972.jpg)]

場景圖概括如下:

  • 客戶端分爲三種核心角色(普通用戶、管理員用戶、超級管理-老闆)用戶,三種用戶訪問同一個數據網關(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 端口,如下圖:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-KWiDH3U2-1585400781827)(https://img2020.cnblogs.com/blog/824291/202003/824291-20200328121418204-1439174025.png)]
數據網關跟之前幾篇文章一樣指定 5001 端口,如下圖:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-eV70gthl-1585400781827)(https://img2020.cnblogs.com/blog/824291/202003/824291-20200328121519324-1379482600.png)]

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

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

也完美獲取到數據
再來訪問下標註了supperManage超級管理員的角色接口,如下圖:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-8aZxG01A-1585400781829)(https://img2020.cnblogs.com/blog/824291/202003/824291-20200328122111367-1105822413.png)]
結果跟預想的一樣,返回了403訪問被拒絕,其他賬號運行也是一樣,我這裏就不一一去運行訪問測試了,有興趣的同學可以到github 上拉起我的源代碼進行運行測試,
到這裏基於ids4角色授權基礎應用也完成了。

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

博客系列源代碼地址:https://github.com/a312586670/NetCoreDemo

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章