IdentityServer4有自己的验证逻辑,需要将需要的Claim放入验证结果,然后向API传递。实现`IResourceOwernerPasswordValidator`这个接口就可以。 #在In-Memory中添加Claim 添加用户 ``` new TestUser{ SubjectId="", Username="", Password="", Claims = new List{new Claim(JwtClaimTypes.Role, "superadmin")} }, new TestUser{ SubjectId = "", Username="", Password="", Claims = new List{new Claim(JwtClaimTypes.Role, "admin")} } ``` 此时查看`HttpContext.User.Claims`却没有刚才Type是role的claim。为什么呢? --因为在验证服务器上管理的`ApiResource`中没有把自定义的claim设置进去。如何设置呢? ``` public static IEnumerable GetApiResources() { return new List{ new ApiResource("","",new List(){JwtClaimTypes.Role}) }; } ``` 为什么在`ApiResource`的构造函数把claim能传递出去呢? 再看`ApiResource`的源代码。 ``` public ApiResouce(string name, string displayName, IEnumerable claimTypes) { if(name.IsMissing()) throw new ArgumentNullException(nameof(name)); Name = name; DisplayName = displayName; //也就是IdentityServer4管理的ApiResource最终会以Scope的形式放在claim集合中 Scopes.Add(new Scope(name, displayName)); if(!claimTypes.IsNullOrEmpty()) { foreach(var type in claimTypes) { UserClaim.Add(type); } } } ``` 原来给`ApiResource`中的自定义claim给到了这里的`UserClaims`属性,一看这个属性就使用用来存放用户有关的所有claim,肯定是一个集合。 ``` public ICollection UserClaims{get;set;} = new HashSet(); ``` 在API控制器方法中定义不同的控制器方法给不同的角色。 ``` [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(); } } ``` 在一个控制器客户端使用superadmin角色访问第二个控制器方法,即需要admin角色的控制器方法。 ``` response = await client.GetAsync("http://localhost:5001/identity/1"); if(!response.IsSuccessStatusCode) { Console.WriteLine(response.StatusCode); Console.WriteLine("没有权限访问"); } else { var content = response.Content.ReadAsStringAsync().Result; Console.WriteLine(content); } ``` # 在生产环境下添加claim 需要实现`IResourceOwnerPasswordValidator`接口。 ``` public class CustomResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator { private readonly TestUserStore _users; private readonly ISystemClock _clock; public CustomResourceOwnerPasswordValidator(TestUserStore users, ISystemClock clock) { _users = users; _clock = clock; } public Task ValidateAsync(ResourceOwnerPasswordValidationContext context) { if(_users.ValidateCredentials(context.UserName, context.Password))//首先还是要验证用户名和密码 { var user= _users.FindByUsername(context.UserName); context.Result = new GrantValidationResult(user.SubjectId ?? throw new ArgumentException(), OidcConstants.AuthenticationMethods.Password, _clock.UtcNow.UtcDateTime, user.claims);//这里把所有的claim返回假如验证逻辑 } else { context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, ""); } return Task.CompletedTask; } } ``` 需要在DI容器中配置。 ``` public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddIdentityServer() .AddDeveloperSigningCredential() .AddInMemoryApiResources(Config.GetApiResources()) .AddInMemoryClients(Config.GetClients()) .AddTestUsers(Config.GetUsers()) .AddResourceOwnerValidator(); } } ``` 以上已有用户的claim信息已经放到了验证逻辑中。接下来还需要`IProfileService`接口的帮忙。 ``` public class CustomProfileService : IProfileService { protected readonly ILogger Logger; protected readoly TestUserStore Users; public CustomProfileService(TestUserStore users, ILogger logger) { Users = users; Logger = logger; } //这里的方法在创建令牌期间会被调用 public virutal Task GetProfileDataAsync(ProfileDataRequestContext context) { context.LogProfileRequest(Logger); if(context.ReqeustedClaimTypes.Any()) { var user = Users.FindBySubjectId(context.Subject.GetSubjectId()); if(user != null) { //这里只将用户请求的claim加入到context.IssuedClaims集合中去 context.AddRequestedClaims(user.Claims); } } context.LogIssuedClaims(Logger); return Task.CompletedTask; } } ``` 以上只返回客户端请求的claims,但是如果用以下写法会把所有的claims都返回给客户端。 ``` context.IssuedClaims.AddRange(user.Claims); ``` `IProfileService`也需要放到容器中。 ``` servces.AddProfileService(); ``` 以上所有的claim返回就少了控制,不见得是好事。 如果想控制发出的claims,一种方式是通过身份资源。身份资源会放到token的Scope参数中。 ``` public static IEnumerable GetIdentityResourceResources() { var customProfile = new IdentityResource{ "custom.profile","",new []{"role"} }; return new List{ new IdentityResource.OpenId(), new IdentityResource.Profile(), customProfile } } ``` 需要注意的是IdentityResource需要和Client中的AllowdScopes配合起来用。 ``` new Client { ClientId="", AllowdGrantTypes = GrantTypes.ResourceOwnerPassword, ClientSecrets = { new Secret() }, //包括APIresource, IdentityResource AllowdScopes = {"api1", IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstatns.StandardScopes.Profile, "custom.profile"} } ``` 另外在Client资源里有一个Claims属性,这里的设置会被直接添加到token中去。 ``` new Client { Claims = new List{ new Claim(JwtCliamTypes.Role, "admin"); } } ``` 放在这里的claim会在token中以`client_role`出现,但在使用的时候还是`Authorize(Roles="Admin")`. > 总结时刻 为API提供自定义claim,可以尝试如下切入点: - 实现`IResourceOwnerPasswordValidator`接口 - 实现`IProfileService`接口 - `IdentiyResource`和`Client`的`AllowedScopes`配合 - `IdentiyResource`和`Client`的`Claims`配合