你们好,许久没有更新博客了,最近从重庆来到了成都,换了个工做环境,前面都比较忙没有什么时间,此次趁着清明假期有时间,又能够分享一些知识给你们。在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