1. 程式人生 > >Asp.Net Core 中IdentityServer4 實戰之角色授權詳解

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

## 一、前言 前幾篇文章分享了`IdentityServer4`密碼模式的基本授權及自定義授權等方式,最近由於改造一個閘道器服務,用到了`IdentityServer4`的授權,改造過程中發現比較適合基於`Role`角色的授權,通過不同的角色來限制使用者訪問不同的`Api資源`,這裡我就來分享`IdentityServer4`基於角色的授權詳解。 #### IdentityServer4 歷史文章目錄 - [Asp.Net Core IdentityServer4 中的基本概念](https://www.cnblogs.com/jlion/p/12437441.html) - [Asp.Net Core 中IdentityServer4 授權中心之應用實戰](https://www.cnblogs.com/jlion/p/12447081.html) - [Asp.Net Core 中IdentityServer4 授權中心之自定義授權模式](https://www.cnblogs.com/jlion/p/12468365.html) - [Asp.Net Core 中IdentityServer4 授權原理及重新整理Token的應用](https://www.cnblogs.com/jlion/p/12501195.html) - [Asp.Net Core 中IdentityServer4 實戰之 Claim詳解](https:////www.cnblogs.com/jlion/p/12543486.html) 沒有看過之前的幾篇文章,我建議先回過頭看看上面那幾篇文章再來看本篇文章,不過對於大牛來說就可以跳過了。。。。 ## 二、模擬場景 還是按照我的文章風格套路,實戰之前先來模擬下應用場景,無場景的實戰都是耍流氓,模擬場景更能讓大家投入,同時也是自我學習、思考、總結的結晶之處!!! 對於角色授權大家也不陌生,大家比較熟悉的應該是`RBAC`的設計,這裡就不闡述`RBAC`,有興趣的可以百度。我們這裡簡單模擬下角色場景 假如有這麼一個`資料閘道器服務`服務(下面我統稱為`資料閘道器`),客戶端有三種賬號角色(普通使用者、管理員使用者、超級管理員使用者),資料閘道器針對這三種角色使用者分配不同的資料訪問許可權,場景圖如下: ![](https://img2020.cnblogs.com/blog/824291/202003/824291-20200328175216974-1045194802.jpg) 那麼這種場景我們會怎麼去設計呢?這個場景還算比較簡單,角色比較單一,比較固定,對於這種場景很多人可能會考慮到通過`Filter`過濾器等方式來實現,這當然可以。不過正對這種場景`IdentityServer4`中本身就支援角色授權,下面我來給大家分享`IdentityServer4`的角色授權. ## 三、角色授權實戰 #### 授權流程 擼程式碼之前我們先整理下`IdentityServer4`的 角色授權流程圖,我簡單概括畫了下,流程圖如下: ![](https://img2020.cnblogs.com/blog/824291/202003/824291-20200328175347833-226144972.jpg) 場景圖概括如下: - 客戶端分為三種核心角色(普通使用者、管理員使用者、超級管理-老闆)使用者,三種使用者訪問同一個`資料閘道器`(API資源) - `資料閘道器`(API資源)對這三種使用者角色做了訪問限制。 角色授權流程解釋如下: - 第一步: 不同的使用者攜帶使用者密碼等資訊訪問`授權中心`(ids4)嘗試授權 - 第二步: `授權中心`對使用者授權通過返回`access_token`給使用者同時宣告使用者的`Role`到`Claim`中。。 - 第三步: 客戶端攜帶拿到的`access_token`嘗試請求`資料閘道器`(API資源)。 - 第四步:`資料閘道器`收到客戶端的第一次請求會到`授權中心`請求獲得驗證公鑰。 - 第五步:`授權中心`返回`驗證公鑰`給`資料閘道器`並且快取起來,後面不再到`授權中心`再次獲得驗證公鑰(只會請求一次,除非重啟服務)。 - 第六步:`資料閘道器`(ids4)通過驗證閘道器驗證`access_token`是否驗證通過,並且驗證請求的客戶端使用者宣告的`Role`是否和請求的`API資源`約定的的角色一致。如果一致則通過第步返回給使用者端,否則直接拒絕請求. #### 擼程式碼 程式碼繼續上面幾篇文章的例子的續集,你懂的,就不從零開始擼程式碼啦(強烈建議沒看過上面幾篇的先看下上面的目錄中的幾篇,要不然會一頭霧水,大佬跳過) 要使`IdentityServer4`實現的`授權中心`支援角色驗證的支援,我們需要在定義的`API資源`中新增`角色`的引入,程式碼如下: 上幾篇文章的`授權中心`(Jlion.NetCore.Identity.Service)的 程式碼如下: ``` /// /// 資源 /// /// public static IEnumerable GetApiResources() { return new List { new ApiResource(OAuthConfig.UserApi.ApiName,OAuthConfig.UserApi.ApiName), }; } ``` 加入角色的支援程式碼改造如下: ``` /// /// 資源 /// /// public static IEnumerable GetApiResources() { return new List { new ApiResource( OAuthConfig.UserApi.ApiName, OAuthConfig.UserApi.ApiName, new List(){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 /// /// 驗證使用者 ///
/// /// /// private async Task> ValidateUserAsync(string loginName, string password) { //TODO 這裡可以通過使用者名稱和密碼到資料庫中去驗證是否存在, // 以及角色相關資訊,我這裡還是使用記憶體中已經存在的使用者和密碼 var user = OAuthMemoryData.GetTestUsers(); if (user == null) throw new Exception("登入失敗,使用者名稱和密碼不正確"); return new List() { new Claim(ClaimTypes.Name, $"{loginName}"), new Claim(EnumUserClaim.DisplayName.ToString(),"測試使用者"), new Claim(EnumUserClaim.UserId.ToString(),"10001"), new Claim(EnumUserClaim.MerchantId.ToString(),"000100001"), }; } #endregion } ``` 為了保留之前文章的原始碼,好讓之前的文章原始碼可追溯,我這裡不在原始碼上改造升級,我直接新增一個使用者密碼驗證器類, 命名為`RoleTestResourceOwnerPasswordValidator`,程式碼改造如下: ``` /// /// 角色授權使用者名稱密碼驗證器demo ///
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 /// /// 驗證使用者(角色Demo 專用方法) /// 這裡和之前區分,主要是為了保留和部落格同步原始碼 ///
/// /// /// private async Task> ValidateUserByRoleAsync(string loginName, string password) { //TODO 這裡可以通過使用者名稱和密碼到資料庫中去驗證是否存在, // 以及角色相關資訊,我這裡還是使用記憶體中已經存在的使用者和密碼 var user = OAuthMemoryData.GetUserByUserName(loginName); if (user == null) throw new Exception("登入失敗,使用者名稱和密碼不正確"); //下面的Claim 宣告我為了演示,硬編碼了, //實際生產環境需要通過讀取資料庫的資訊並且來宣告 return new List() { 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`,程式碼如下: ``` /// /// 角色列舉 /// public enum EnumUserRole { Normal, Manage, SupperManage } ``` `GetUserByUserName`中硬編碼建立了三個角色的使用者,程式碼如下: ``` /// /// 為了演示,硬編碼了, /// 這個方法可以通過DDD設計到底層資料庫去查詢資料庫 /// /// /// 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() { 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() //.AddResourceOwnerValidator() .AddResourceOwnerValidator() .AddExtensionGrantValidator() .AddProfileService();//新增微信端自定義方式的驗證 #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詳解](https://www.cnblogs.com/jlion/p/12543486.html) 文章中在`資料閘道器`服務中新增了`UserController`控制器,並添加了一個訪問使用者基本的`Claim`資訊介面,之前的程式碼如下: ``` [ApiController] [Route("[controller]")] public class UserController : ControllerBase { private readonly ILogger _logger; public UserController(ILogger logger) { _logger = logger; } [Authorize] [HttpGet] public async Task 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 Get(int id) { var userId = User.UserId(); return new { name = User.Name(), userId = userId, displayName = User.DisplayName(), merchantId = User.MerchantId(), roleName=User.Role()//獲得當前登入使用者的角色 }; } ``` 到這裡`資料閘道器`程式碼也已經改造完了,我們接下來就是執行結果看看是否正確。 #### 執行 我們分別通過命令列執行我們的`授權閘道器`服務和`資料閘道器`服務,分別如下圖: `授權閘道器`還是指定5000 埠,如下圖: ![](https://img2020.cnblogs.com/blog/824291/202003/824291-20200328121418204-1439174025.png) `資料閘道器`跟之前幾篇文章一樣指定 5001 埠,如下圖: ![](https://img2020.cnblogs.com/blog/824291/202003/824291-20200328121519324-1379482600.png) 現在`授權閘道器`和`資料閘道器`都已經完美執行起來了,接下來我們通過`postman`模擬請求。 先來通過普通使用者(testNormal)請求`授權中心`獲得`access_token`,如下圖: ![](https://img2020.cnblogs.com/blog/824291/202003/824291-20200328121804409-786368335.png) 請求驗證通過, 再來通過獲取到的`access_token` 獲取普通介面: ![](https://img2020.cnblogs.com/blog/824291/202003/824291-20200328121950290-500508800.png) 也完美獲取到資料 再來訪問下標註了`supperManage`超級管理員的角色介面,如下圖: ![](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博士"關注,或者微信掃下面的二維碼進行關注,同時大家也可以積極的分享或點個右下角的**推薦**,讓更多人的關注到我的