IdentityServer4有自己的验证逻辑,需要将需要的Claim放入验证结果,然后向API传递。实现IResourceOwernerPasswordValidator
这个接口就可以。
添加用户
new TestUser{
SubjectId="",
Username="",
Password="",
Claims = new List<Claim>{new Claim(JwtClaimTypes.Role, "superadmin")}
},
new TestUser{
SubjectId = "",
Username="",
Password="",
Claims = new List<Claim>{new Claim(JwtClaimTypes.Role, "admin")}
}
此时查看HttpContext.User.Claims
却没有刚才Type是role的claim。为什么呢?
--因为在验证服务器上管理的ApiResource
中没有把自定义的claim设置进去。如何设置呢?
public static IEnumerable<ApiResource> GetApiResources()
{
return new List<ApiResource>{
new ApiResource("","",new List<string>(){JwtClaimTypes.Role})
};
}
为什么在ApiResource
的构造函数把claim能传递出去呢? 再看ApiResource
的源代码。
public ApiResouce(string name, string displayName, IEnumerable<string> 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<string> UserClaims{get;set;} = new HashSet<string>();
在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);
}
需要实现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<CustomResourceOwnerPasswordValidator>();
}
}
以上已有用户的claim信息已经放到了验证逻辑中。接下来还需要IProfileService
接口的帮忙。
public class CustomProfileService : IProfileService
{
protected readonly ILogger Logger;
protected readoly TestUserStore Users;
public CustomProfileService(TestUserStore users, ILogger<TestUserProfilerService> 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<CustomProfileService>();
以上所有的claim返回就少了控制,不见得是好事。
如果想控制发出的claims,一种方式是通过身份资源。身份资源会放到token的Scope参数中。
public static IEnumerable<IdentiyResource> GetIdentityResourceResources()
{
var customProfile = new IdentityResource{
"custom.profile","",new []{"role"}
};
return new List<IdentityResource>{
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<Claim>{
new Claim(JwtCliamTypes.Role, "admin");
}
}
放在这里的claim会在token中以client_role
出现,但在使用的时候还是Authorize(Roles="Admin")
.
总结时刻
为API提供自定义claim,可以尝试如下切入点:
IResourceOwnerPasswordValidator
接口IProfileService
接口IdentiyResource
和Client
的AllowedScopes
配合IdentiyResource
和Client
的Claims
配合