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

来源:https://www.cnblogs.com/jlion/archive/2020/03/30/12571620.html
-Advertisement-
Play Games

假如有這麼一個數據網關服務服務,客戶端有三種賬號角色(普通用戶、管理員用戶、超級管理員用戶),數據網關針對這三種角色用戶分配不同的數據訪問許可權,那怎麼樣通過IdentityServer4 來實現角色的授權呢?它又是怎樣的一個過程? ...


一、前言

前幾篇文章分享了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博士"關註,或者微信掃下麵的二維碼進行關註,同時大家也可以積極的分享或點個右下角的推薦,讓更多人的關註到我的文章。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 前言 AutoWrapper是一個簡單可自定義全局異常處理程式和ASP.NET Core API響應的包裝。他使用ASP.NET Core middleware攔截傳入的HTTP請求,並將最後的結果使用統一的格式來自動包裝起來.目的主要是讓我們更多的關註業務特定的代碼要求,並讓包裝器自動處理HTTP ...
  • ViewComponent的一種使用方法 最近在一個自己新建的Core項目中想使用Html.Action(),突然發現這個方法已經沒了,下麵我按照官網(https://docs.microsoft.com/zh cn/aspnet/core/mvc/views/view components?vie ...
  • 一、什麼是ThreadPool 線程池(源碼) 1.線程池顧名思義,有我們的系統創建一個容器裝載著我們的線程,由CLR控制的所有AppDomain共用。線程池可用於執行任務、發送工作項、處理非同步 I/O、代表其他線程等待以及處理計時器。所以使用線程池不需要自己創建線程,而是通過線程池來創建和執行和管 ...
  • 配置規則 命令行配置提供程式 環境變數配置提供程式 文件配置提供程式 配置的讀取 自定義配置數據源 ASP.NET Core中的配置項可以通過命令行、環境變數、json/xml/ini配置文件來提供。 Web應用在生成主機時會調用CreateDefaultBuilder方法,這個方法按照下麵的順序添 ...
  • 下麵2個service中,方法體均一樣。只是其中service名,參數和url路徑不一樣。 原因是曾經對項目升級或是重構留下來的結果。 由於程式中N多處地方有引用,又不能合併它,刪除更不用說了。 因為,Insus.NET為了讓其更好維護,因此寫了另外一個Service來進行重構它們: 好了,博文中最 ...
  • 1、創建一個Asp.Net Core Web應用程式 1.1、打開VS2019 新建項目 1.2、選好項目位置後進入選擇界面,選擇Web應用程式 1.3、進去的頁面結構如下 Pages 文件夾:包含 Razor 頁面和支持文件。 每個 Razor 頁面都是一對文件: 一個 .cshtml 文件,其中 ...
  • 相關主頁 Prometheus https://prometheus.io/ grafana https://grafana.com/ 安裝Prometheus Linux 可以參考https://www.cnblogs.com/linkanyway/p/Configure-a-Prometheus ...
  • 我從一萬二千年前開始寫XAML,這麼多年用了很多各式各樣的工具,現在留在電腦里的、現在還在用的、在寫WPF時用的也就那麼幾個。這篇文章總結了這些工具,希望這些工具可以讓WPF開發者事半功倍。 1. Visual Studio Visual Studio應該無需介紹,它是“面向任何開發者的同類最佳工具 ...
一周排行
    -Advertisement-
    Play Games
  • 前言 在我們開發過程中基本上不可或缺的用到一些敏感機密數據,比如SQL伺服器的連接串或者是OAuth2的Secret等,這些敏感數據在代碼中是不太安全的,我們不應該在源代碼中存儲密碼和其他的敏感數據,一種推薦的方式是通過Asp.Net Core的機密管理器。 機密管理器 在 ASP.NET Core ...
  • 新改進提供的Taurus Rpc 功能,可以簡化微服務間的調用,同時可以不用再手動輸出模塊名稱,或調用路徑,包括負載均衡,這一切,由框架實現並提供了。新的Taurus Rpc 功能,將使得服務間的調用,更加輕鬆、簡約、高效。 ...
  • 順序棧的介面程式 目錄順序棧的介面程式頭文件創建順序棧入棧出棧利用棧將10進位轉16進位數驗證 頭文件 #include <stdio.h> #include <stdbool.h> #include <stdlib.h> 創建順序棧 // 指的是順序棧中的元素的數據類型,用戶可以根據需要進行修改 ...
  • 前言 整理這個官方翻譯的系列,原因是網上大部分的 tomcat 版本比較舊,此版本為 v11 最新的版本。 開源項目 從零手寫實現 tomcat minicat 別稱【嗅虎】心有猛虎,輕嗅薔薇。 系列文章 web server apache tomcat11-01-官方文檔入門介紹 web serv ...
  • C總結與剖析:關鍵字篇 -- <<C語言深度解剖>> 目錄C總結與剖析:關鍵字篇 -- <<C語言深度解剖>>程式的本質:二進位文件變數1.變數:記憶體上的某個位置開闢的空間2.變數的初始化3.為什麼要有變數4.局部變數與全局變數5.變數的大小由類型決定6.任何一個變數,記憶體賦值都是從低地址開始往高地 ...
  • 如果讓你來做一個有狀態流式應用的故障恢復,你會如何來做呢? 單機和多機會遇到什麼不同的問題? Flink Checkpoint 是做什麼用的?原理是什麼? ...
  • C++ 多級繼承 多級繼承是一種面向對象編程(OOP)特性,允許一個類從多個基類繼承屬性和方法。它使代碼更易於組織和維護,並促進代碼重用。 多級繼承的語法 在 C++ 中,使用 : 符號來指定繼承關係。多級繼承的語法如下: class DerivedClass : public BaseClass1 ...
  • 前言 什麼是SpringCloud? Spring Cloud 是一系列框架的有序集合,它利用 Spring Boot 的開發便利性簡化了分散式系統的開發,比如服務註冊、服務發現、網關、路由、鏈路追蹤等。Spring Cloud 並不是重覆造輪子,而是將市面上開發得比較好的模塊集成進去,進行封裝,從 ...
  • class_template 類模板和函數模板的定義和使用類似,我們已經進行了介紹。有時,有兩個或多個類,其功能是相同的,僅僅是數據類型不同。類模板用於實現類所需數據的類型參數化 template<class NameType, class AgeType> class Person { publi ...
  • 目錄system v IPC簡介共用記憶體需要用到的函數介面shmget函數--獲取對象IDshmat函數--獲得映射空間shmctl函數--釋放資源共用記憶體實現思路註意 system v IPC簡介 消息隊列、共用記憶體和信號量統稱為system v IPC(進程間通信機制),V是羅馬數字5,是UNI ...