你們好,許久沒有更新博客了,最近從重慶來到了成都,換了個工做環境,前面都比較忙沒有什麼時間,此次趁着清明假期有時間,又能夠分享一些知識給你們。在QQ羣裏有許多人都問過IdentityServer4怎麼用Role(角色)來控制權限呢?還有關於Claim這個是什麼呢?下面我帶你們一塊兒來揭開它的神祕面紗!html
咱們用過IdentityServer4或者熟悉ASP.NET Core認證的都應該知道有Claim這個東西,Claim咱們經過在線翻譯有如下解釋:git
(1)百度翻譯github
(2)谷歌翻譯數據庫
這裏我理解爲聲明
,咱們每一個用戶都有多個Claim,每一個Claim聲明瞭用戶的某個信息好比:Role=Admin,UserID=1000等等,這裏Role,UserID每一個都是用戶的Claim,都是表示用戶信息的單元
,咱們不妨把它稱爲用戶信息單元
。api
建議閱讀楊總的Claim相關的解析 http://www.cnblogs.com/savorboard/p/aspnetcore-identity.htmlide
這裏咱們使用IdentityServer4的QuickStart中的第二個Demo:
ResourceOwnerPassword
來進行演示(代碼地址放在文末),因此項目的建立配置就不在這裏演示了。測試
這裏咱們須要自定義IdentityServer4(後文簡稱id4)的驗證邏輯,而後在驗證完畢以後,將咱們本身須要的Claim加入驗證結果。即可以向API資源服務進行傳遞。id4定義了IResourceOwnerPasswordValidator
接口,咱們實現這個接口就好了。ui
Id4爲咱們提供了很是方便的In-Memory測試支持,那咱們在In-Memory測試中是否能夠實現自定義添加角色Claim呢,答案當時是能夠的。this
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項目下的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角色),運行一下:
能夠看到提示咱們第二個,無權訪問,正常。
咱們前面的過程都是使用的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 若是是從數據庫中讀取角色信息,那麼咱們應該在此處添加 此處只返回必要的Claim context.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類型,來爲咱們裝載信息,下面是一個簡單實現:
這裏特別說明一下:本節講的是「如何使用已有用戶數據自定義Claim」,實現 IResourceOwnerPasswordValidator 是爲了對接已有的用戶數據,而後纔是實現 IProfileService 以添加自定義 claim,這兩步共同完成的是 「使用已有用戶數據自定義Claim」,並非自定義 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 集合中 這樣咱們的請求方便能正常獲取到所需Claim context.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,咱們才能訪問到:
new Client { ClientId = "ro.client", AllowedGrantTypes = GrantTypes.ResourceOwnerPassword, ClientSecrets = { new Secret("secret".Sha256()) }, AllowedScopes = { "api1" ,IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile,"custom.profile"} }
咱們在Client對象的AllowedScopes屬性里加入了咱們剛剛定義的身份資源,下載訪問用戶信息終結點將會獲得和上面同樣的結果。
新增於2018.12.14
在定義 Client 資源的時候發現,Client也有一個Claims屬性,根據註釋得知,在此屬性上設置的值將會被直接添加到AccessToken,代碼以下:
new Client { ClientId = "client", AllowedGrantTypes = GrantTypes.ClientCredentials, ClientSecrets = { new Secret("secret".Sha256()) }, AllowedScopes = { "api1", IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile }, Claims = new List<Claim> { new Claim(JwtClaimTypes.Role, "admin") } };
只用在客戶端資源這裏設置就行,其餘地方不用設置,而後請求AccessToken就會被帶入。
值得注意的是Client這裏設置的Claims默認都會被帶一個client_前綴。若是像前文同樣使用 [Authorize(Roles ="admin")] 是行的,由於 [Authorize(Roles ="admin")] 使用的Claim是role而不是client_role
寫這篇文章,簡單分析了一下相關的源碼,若是由於有本文描述不清楚或者不明白的地方建議閱讀一下源碼,或者加下方QQ羣在羣內提問。若是咱們的根據角色的權限認證沒有生效,請檢查是否正確獲取到了角色的用戶信息單元。咱們須要接入已有用戶體系,只需實現IProfileService
和IResourceOwnerPasswordValidator
接口便可,而且在Startup配置Service時再也不須要AddTestUsers
,由於將使用咱們本身的用戶信息。
Demo地址:https://github.com/stulzq/IdentityServer4.Samples/tree/master/Practice/01_RoleAndClaim