鼎鼎知识库
您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

19IdentityServer4给API自定义claim.md 6.9KB

IdentityServer4有自己的验证逻辑,需要将需要的Claim放入验证结果,然后向API传递。实现IResourceOwernerPasswordValidator这个接口就可以。

在In-Memory中添加Claim

添加用户

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);
}

在生产环境下添加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<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接口
  • IdentiyResourceClientAllowedScopes配合
  • IdentiyResourceClientClaims配合