IdentityServer4实战 - 基于角色的权限控制及Claim详解
一.前言
大家好,許久沒有更新博客了,最近從重慶來到了成都,換了個工作環境,前面都比較忙沒有什么時間,這次趁著清明假期有時間,又可以分享一些知識給大家。在QQ群里有許多人都問過IdentityServer4怎么用Role(角色)來控制權限呢?還有關于Claim這個是什么呢?下面我帶大家一起來揭開它的神秘面紗!
二.Claim詳解
我們用過IdentityServer4或者熟悉ASP.NET Core認證的都應該知道有Claim這個東西,Claim我們通過在線翻譯有以下解釋:
(1)百度翻譯
(2)谷歌翻譯
這里我理解為聲明,我們每個用戶都有多個Claim,每個Claim聲明了用戶的某個信息比如:Role=Admin,UserID=1000等等,這里Role,UserID每個都是用戶的Claim,都是表示用戶信息的單元?,我們不妨把它稱為用戶信息單元?。
建議閱讀楊總的Claim相關的解析?http://www.cnblogs.com/savorboard/p/aspnetcore-identity.html
三.測試環境中添加角色Claim
這里我們使用IdentityServer4的QuickStart中的第二個Demo:ResourceOwnerPassword來進行演示(代碼地址放在文末),所以項目的創建配置就不在這里演示了。
這里我們需要自定義IdentityServer4(后文簡稱id4)的驗證邏輯,然后在驗證完畢之后,將我們自己需要的Claim加入驗證結果。便可以向API資源服務進行傳遞。id4定義了IResourceOwnerPasswordValidator接口,我們實現這個接口就行了。
Id4為我們提供了非常方便的In-Memory測試支持,那我們在In-Memory測試中是否可以實現自定義添加角色Claim呢,答案當時是可以的。
1.首先我們需要在定義TestUser測試用戶時,定義用戶Claims屬性,意思就是為我們的測試用戶添加額外的身份信息單元,這里我們添加角色身份信息單元:
new TestUser {SubjectId = "1",Username = "alice",Password = "password",Claims = new List<Claim>(){new Claim(JwtClaimTypes.Role,"superadmin") } },new TestUser {SubjectId = "2",Username = "bob",Password = "password",Claims = new List<Claim>(){new Claim(JwtClaimTypes.Role,"admin") } }JwtClaimTypes是一個靜態類在IdentityModel程序集下,里面定義了我們的jwt token的一些常用的Claim,JwtClaimTypes.Role是一個常量字符串public const string Role = "role";如果JwtClaimTypes定義的Claim類型沒有我們需要的,那我們直接寫字符串即可。
2.分別啟動 QuickstartIdentityServer、Api、ResourceOwnerClient 查看 運行結果:
可以看見我們定義的API資源通過HttpContext.User.Claims并沒有獲取到我們為測試用戶添加的Role Claim,那是因為我們為API資源做配置。
3.配置API資源需要的Claim
在QuickstartIdentityServer項目下的Config類的GetApiResources做出如下修改:
public static IEnumerable<ApiResource> GetApiResources(){ ??return new List<ApiResource>{// ? ? ? ? ? ? ? ?new ApiResource("api1", "My API")new ApiResource("api1", "My API",new List<string>(){JwtClaimTypes.Role})}; }
我們添加了一個Role Claim,現在再次運行(需要重新QuickstartIdentityServer方可生效)查看結果。
可以看到,我們的API服務已經成功獲取到了Role Claim。
這里有個疑問,為什么需要為APIResource配置Role Claim,我們的API Resource才能獲取到呢,我們查看ApiResource的源碼:
public ApiResource(string name, string displayName, IEnumerable<string> claimTypes){ ? ?if (name.IsMissing()) throw new ArgumentNullException(nameof(name));Name = name;DisplayName = displayName;Scopes.Add(new Scope(name, displayName)); ? ?if (!claimTypes.IsNullOrEmpty()){ ? ? ? ?foreach (var type in claimTypes){UserClaims.Add(type);}} }從上面的代碼可以分析出,我們自定義的Claim添加到了一個名為UserClaims的屬性中,查看這個屬性:
/// <summary>/// List of accociated user claims that should be included when this resource is requested./// </summary>public ICollection<string> UserClaims { get; set; } = new HashSet<string>();根據注釋我們便知道了原因:請求此資源時應包含的相關用戶身份單元信息列表。
四.通過角色控制API訪問權限
我們在API項目下的IdentityController做出如下更改
[Route("[controller]")] ? ?public class IdentityController : ControllerBase{[Authorize(Roles = "superadmin")][HttpGet] ? ?public IActionResult Get() ? ?{ ? ? ? ?return new JsonResult(from c in HttpContext.User.Claims select new { c.Type, c.Value });}[Authorize(Roles = "admin")][Route("{id}")][HttpGet] ? ?public string Get(int id) ? ?{ ? ? ? ?return id.ToString();} }我們定義了兩個API通過Authorize特性賦予了不同的權限(我們的測試用戶只添加了一個角色,通過訪問具有不同角色的API來驗證是否能通過角色來控制)
我們在ResourceOwnerClient項目下,Program類最后添加如下代碼:
response = await client.GetAsync("http://localhost:5001/identity/1");if (!response.IsSuccessStatusCode) {Console.WriteLine(response.StatusCode);Console.WriteLine("沒有權限訪問 http://localhost:5001/identity/1"); }else{ ? ?var content = response.Content.ReadAsStringAsync().Result;Console.WriteLine(content); }這里我們請求第二個API的代碼,正常情況應該會沒有權限訪問的(我們使用的用戶只具有superadmin角色,而第二個API需要admin角色),運行一下:
可以看到提示我們第二個,無權訪問,正常。
五.如何使用已有用戶數據自定義Claim
我們前面的過程都是使用的TestUser來進行測試的,那么我們正式使用時肯定是使用自己定義的用戶(從數據庫中獲取),這里我們可以實現IResourceOwnerPasswordValidator接口,來定義我們自己的驗證邏輯。
/// <summary>/// 自定義 Resource owner password 驗證器/// </summary>public class CustomResourceOwnerPasswordValidator: IResourceOwnerPasswordValidator{ ? ?/// <summary>/// 這里為了演示我們還是使用TestUser作為數據源,/// 正常使用此處應當傳入一個 用戶倉儲 等可以從/// 數據庫或其他介質獲取我們用戶數據的對象/// </summary>private readonly TestUserStore _users; ? ?private readonly ISystemClock _clock; ? ?public CustomResourceOwnerPasswordValidator(TestUserStore users, ISystemClock clock) ? ?{_users = users;_clock = clock;} ? ?/// <summary>/// 驗證/// </summary>/// <param name="context"></param>/// <returns></returns>public Task ValidateAsync(ResourceOwnerPasswordValidationContext context) ? ?{ ? ? ? ?//此處使用context.UserName, context.Password 用戶名和密碼來與數據庫的數據做校驗if (_users.ValidateCredentials(context.UserName, context.Password)){ ? ? ? ? ? ?var user = _users.FindByUsername(context.UserName); ? ? ? ? ? ?//驗證通過返回結果 //subjectId 為用戶唯一標識 一般為用戶id//authenticationMethod 描述自定義授權類型的認證方法 //authTime 授權時間//claims 需要返回的用戶身份信息單元 此處應該根據我們從數據庫讀取到的用戶信息 添加Claims 如果是從數據庫中讀取角色信息,那么我們應該在此處添加 此處只返回必要的Claimcontext.Result = new GrantValidationResult(user.SubjectId ?? throw new ArgumentException("Subject ID not set", nameof(user.SubjectId)),OidcConstants.AuthenticationMethods.Password, _clock.UtcNow.UtcDateTime,user.Claims);} ? ? ? ?else{ ? ? ? ? ? ?//驗證失敗context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "invalid custom credential");} ? ? ? ?return Task.CompletedTask;}在Startup類里配置一下我們自定義的驗證器:
實現了IResourceOwnerPasswordValidator還不夠,我們還需要實現IProfileService接口,他是專門用來裝載我們需要的Claim信息的,比如在token創建期間和請求用戶信息終結點是會調用它的GetProfileDataAsync方法來根據請求需要的Claim類型,來為我們裝載信息,下面是一個簡單實現:
public class CustomProfileService: IProfileService{/// <summary>/// The logger/// </summary>protected readonly ILogger Logger;/// <summary>/// The users/// </summary>protected readonly TestUserStore Users;/// <summary>/// Initializes a new instance of the <see cref="TestUserProfileService"/> class./// </summary>/// <param name="users">The users.</param>/// <param name="logger">The logger.</param>public CustomProfileService(TestUserStore users, ILogger<TestUserProfileService> logger){Users = users;Logger = logger; }/// <summary>/// 只要有關用戶的身份信息單元被請求(例如在令牌創建期間或通過用戶信息終點),就會調用此方法/// </summary>/// <param name="context">The context.</param>/// <returns></returns>public virtual Task GetProfileDataAsync(ProfileDataRequestContext context){context.LogProfileRequest(Logger); ? ?//判斷是否有請求Claim信息if (context.RequestedClaimTypes.Any()){ ? ? ? ?//根據用戶唯一標識查找用戶信息var user = Users.FindBySubjectId(context.Subject.GetSubjectId()); ? ? ? ?if (user != null){ ? ? ? ? ? ?//調用此方法以后內部會進行過濾,只將用戶請求的Claim加入到 context.IssuedClaims 集合中 這樣我們的請求方便能正常獲取到所需Claimcontext.AddRequestedClaims(user.Claims);}}context.LogIssuedClaims(Logger); ? ?return Task.CompletedTask; }/// <summary>/// 驗證用戶是否有效 例如:token創建或者驗證/// </summary>/// <param name="context">The context.</param>/// <returns></returns>public virtual Task IsActiveAsync(IsActiveContext context){Logger.LogDebug("IsActive called from: {caller}", context.Caller); ? ?var user = Users.FindBySubjectId(context.Subject.GetSubjectId());context.IsActive = user?.IsActive == true; ? ?return Task.CompletedTask; }同樣在Startup類里啟用我們自定義的ProfileService?:AddProfileService<CustomProfileService>()
值得注意的是如果我們直接將用戶的所有Claim加入?context.IssuedClaims集合,那么用戶所有的Claim都將會無差別返回給請求方。比如默認情況下請求用戶終結點(http://Identityserver4地址/connect/userinfo)只會返回sub(用戶唯一標識)信息,如果我們在此處直接?context.IssuedClaims=User.Claims,那么所有Claim都將被返回,而不會根據請求的Claim來進行篩選,這樣做雖然省事,但是損失了我們精確控制的能力,所以不推薦。
上述說明配圖:
如果直接?context.IssuedClaims=User.Claims,那么返回結果如下:
? ? ? ? /// <summary>/// 只要有關用戶的身份信息單元被請求(例如在令牌創建期間或通過用戶信息終點),就會調用此方法/// </summary>/// <param name="context">The context.</param>/// <returns></returns>public virtual Task GetProfileDataAsync(ProfileDataRequestContext context) ? ? ? ?{ ? ? ? ? ? ?var user = Users.FindBySubjectId(context.Subject.GetSubjectId()); ? ? ? ? ? ?if (user != null)context.IssuedClaims .AddRange(user.Claims); ? ? ? ? ? ?return Task.CompletedTask;}用戶的所有Claim都將被返回。這樣降低了我們控制的能力,我們可以通過下面的方法來實現同樣的效果,但卻不會丟失控制的能力。
(1).自定義身份資源資源
身份資源的說明:身份資源也是數據,如用戶ID,姓名或用戶的電子郵件地址。 身份資源具有唯一的名稱,您可以為其分配任意身份信息單元(比如姓名、性別、身份證號和有效期等都是身份證的身份信息單元)類型。 這些身份信息單元將被包含在用戶的身份標識(Id Token)中。 客戶端將使用scope參數來請求訪問身份資源。
public static IEnumerable<IdentityResource> GetIdentityResourceResources(){ ? ?var customProfile = new IdentityResource(name: "custom.profile",displayName: "Custom profile",claimTypes: new[] { "role"}); ? ?return new List<IdentityResource>{ ? ? ? ?new IdentityResources.OpenId(), new IdentityResources.Profile(),customProfile}; }(2).配置Scope
通過上面的代碼,我們自定義了一個名為“customProfile“的身份資源,他包含了"role" Claim(可以包含多個Claim),然后我們還需要配置Scope,我們才能訪問到:
我們在Client對象的AllowedScopes屬性里加入了我們剛剛定義的身份資源,下載訪問用戶信息終結點將會得到和上面一樣的結果。
六.總結
寫這篇文章,簡單分析了一下相關的源碼,如果因為有本文描述不清楚或者不明白的地方建議閱讀一下源碼,或者加下方QQ群在群內提問。如果我們的根據角色的權限認證沒有生效,請檢查是否正確獲取到了角色的用戶信息單元。我們需要接入已有用戶體系,只需實現IProfileService和IResourceOwnerPasswordValidator接口即可,并且在Startup配置Service時不再需要AddTestUsers,因為將使用我們自己的用戶信息。
Demo地址:https://github.com/stulzq/BlogDemos/tree/master/Id4RoleAndClaim
相關文章:
學習Identity Server 4的預備知識
?使用Identity Server 4建立Authorization Server (1)
使用Identity Server 4建立Authorization Server (2)
使用Identity Server 4建立Authorization Server (3)
使用Identity Server 4建立Authorization Server (4)
使用Identity Server 4建立Authorization Server (5)
Identity Service - 解析微軟微服務架構eShopOnContainers(二)
IdentityServer4(OAuth2.0服務)折騰筆記
IdentityServer4 指定角色授權(Authorize(Roles="admin"))
IdentityServer4 ASP.NET Core的OpenID Connect OAuth 2.0框架學習保護API
IdentityServer4 SigningCredential(RSA 證書加密)
IdentityServer4 實現自定義 GrantType 授權模式
IdentityServer4 配置負載均衡
IdentityServer4使用教程1--搭建ID4網站
IdentityServer4(10)- 添加對外部認證的支持之QQ登錄
IdentityServer4 第三方快速入門和示例
原文:https://www.cnblogs.com/stulzq/p/8726002.html
.NET社區新聞,深度好文,歡迎訪問公眾號文章匯總 http://www.csharpkit.com
總結
以上是生活随笔為你收集整理的IdentityServer4实战 - 基于角色的权限控制及Claim详解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: RabbitMQ教程C#版 - 工作队列
- 下一篇: ASP.NET Core 集成测试