天天看點

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

假如有這麼一個資料網關服務服務,用戶端有三種賬号角色(普通使用者、管理者使用者、超級管理者使用者),資料網關針對這三種角色使用者配置設定不同的資料通路權限,那怎麼樣通過IdentityServer4 來實作角色的授權呢?它又是怎樣的一個過程?

一、前言

前幾篇文章分享了

IdentityServer4

密碼模式的基本授權及自定義授權等方式,最近由于改造一個網關服務,用到了

IdentityServer4

的授權,改造過程中發現比較适合基于

Role

角色的授權,通過不同的角色來限制使用者通路不同的

Api資源

,這裡我就來分享

IdentityServer4

基于角色的授權詳解。

IdentityServer4 曆史文章目錄

  • Asp.Net Core IdentityServer4 中的基本概念
  • Asp.Net Core 中IdentityServer4 授權中心之應用實戰
  • Asp.Net Core 中IdentityServer4 授權中心之自定義授權模式
  • Asp.Net Core 中IdentityServer4 授權原理及重新整理Token的應用
  • Asp.Net Core 中IdentityServer4 實戰之 Claim詳解

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

二、模拟場景

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

對于角色授權大家也不陌生,大家比較熟悉的應該是

RBAC

的設計,這裡就不闡述

RBAC

,有興趣的可以百度。我們這裡簡單模拟下角色場景

假如有這麼一個

資料網關服務

服務(下面我統稱為

資料網關

),用戶端有三種賬号角色(普通使用者、管理者使用者、超級管理者使用者),資料網關針對這三種角色使用者配置設定不同的資料通路權限,場景圖如下:

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

那麼這種場景我們會怎麼去設計呢?這個場景還算比較簡單,角色比較單一,比較固定,對于這種場景很多人可能會考慮到通過

Filter

過濾器等方式來實作,這當然可以。不過正對這種場景

IdentityServer4

中本身就支援角色授權,下面我來給大家分享

IdentityServer4

的角色授權.

三、角色授權實戰

授權流程

撸代碼之前我們先整理下

IdentityServer4

的 角色授權流程圖,我簡單概括畫了下,流程圖如下:

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

場景圖概括如下:

  • 用戶端分為三種核心角色(普通使用者、管理者使用者、超級管理-老闆)使用者,三種使用者通路同一個

    資料網關

    (API資源)
  • 資料網關

    (API資源)對這三種使用者角色做了通路限制。

角色授權流程解釋如下:

  • 第一步: 不同的使用者攜帶使用者密碼等資訊通路

    授權中心

    (ids4)嘗試授權
  • 第二步:

    授權中心

    對使用者授權通過傳回

    access_token

    給使用者同時聲明使用者的

    Role

    Claim

    中。。
  • 第三步: 用戶端攜帶拿到的

    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 端口,如下圖:

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

資料網關

跟之前幾篇文章一樣指定 5001 端口,如下圖:

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

現在

授權網關

資料網關

都已經完美運作起來了,接下來我們通過

postman

模拟請求。

先來通過普通使用者(testNormal)請求

授權中心

獲得

access_token

,如下圖:

請求驗證通過,

再來通過擷取到的

access_token

擷取普通接口:

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

也完美擷取到資料

再來通路下标注了

supperManage

超級管理者的角色接口,如下圖:

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

結果跟預想的一樣,傳回了

403

通路被拒絕,其他賬号運作也是一樣,我這裡就不一一去運作通路測試了,有興趣的同學可以到github 上拉起我的源代碼進行運作測試,

到這裡基于

ids4

角色授權基礎應用也完成了。

結束語:上面分享學習了

IdentityServer4

進行角色授權的實戰例子,但是從上面的例子中有一個不好的弊端,就是每個api通路都需要寫死進行指定

Role

這在生産環境中很不現實和靈活,

Role

角色這個東西都是通過背景自管理,進行靈活配置角色和資源的,那

IdentityServer4

有沒有什麼好的方式實作呢?留給大家思考,思考就有學習的目标,也是思維的進步。

部落格系列源代碼位址:https://github.com/a312586670/NetCoreDemo

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

如果您認為這篇文章還不錯或者有所收獲,您可以點選右下角的【推薦】按鈕精神支援,因為這種支援是我繼續寫作,分享的最大動力!

作者:Jlion

聲明:原創部落格請在轉載時保留原文連結或者在文章開頭加上本人部落格位址,如發現錯誤,歡迎批評指正。凡是轉載于本人的文章,不能設定打賞功能,如有特殊需求請與本人聯系!

為了更好的維護開源項目以及技術交流,特意建立了一個交流群,群号:1083147206 有興趣者可以加入交流

如果您覺的不錯,請微信掃碼關注 【dotNET 博士】公衆号,後續給您帶來更精彩的分享

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

繼續閱讀