IdentityServer4:如何仅返回当前Scope对应的Claims
嘿,这个问题我之前帮好几个开发者捋清楚过——IdentityServer默认不会自动帮你按Scope和资源的关联关系过滤声明,所以才会出现所有Claims都返回给每个资源的情况。结合你提到的配置场景,我给你分步拆解解决方案:
1. 先把资源与Scope的绑定关系搞扎实
首先要确保你定义ApiResource和ApiScope时,关联关系是专属绑定的,不能交叉混放。举个配置示例:
services.AddIdentityServer() .AddInMemoryApiResources(new List<ApiResource> { new ApiResource("resource1", "资源1") { // 只绑定属于这个资源的Scope Scopes = { "scope1" }, // 明确该资源能拿到的专属声明 UserClaims = { "claim1", "claim2" } }, new ApiResource("resource2", "资源2") { Scopes = { "scope2" }, UserClaims = { "claim3", "claim4" } } }) .AddInMemoryApiScopes(new List<ApiScope> { new ApiScope("scope1", "权限范围1") { // 这里的声明要和对应资源的UserClaims完全匹配 UserClaims = { "claim1", "claim2" } }, new ApiScope("scope2", "权限范围2") { UserClaims = { "claim3", "claim4" } } });
核心就是:每个资源只认领自己的Scope,每个Scope只维护自己的声明集合,别把其他Scope的声明混进来。
2. 开启IdentityServer的严格资源验证
在IdentityServer的全局配置里,打开严格资源验证和受众声明,让系统自动按资源过滤声明:
services.AddIdentityServer(options => { // 强制验证资源与Scope的关联,避免跨资源泄露声明 options.ResourceValidation = ResourceValidationMode.Strict; // 确保令牌里包含正确的受众(aud)标识,方便后续资源验证 options.EmitStaticAudienceClaim = true; }) // ... 后续的AddInMemoryClients等配置
ResourceValidationMode.Strict是关键,它会让IdentityServer在颁发令牌时,只把当前资源关联的Scope声明放进去,不会把其他Scope的声明塞进来。
3. 客户端请求要精准指定Scope
客户端配置时,虽然可以把多个Scope加到AllowedScopes里,但实际请求令牌时,要根据访问的资源精准请求对应的Scope——比如访问resource1就只请求scope1,访问resource2就只请求scope2。如果客户端一次性请求所有Scope,IdentityServer会按资源拆分令牌,每个令牌对应一个资源的声明。
举个客户端配置的例子:
.AddInMemoryClients(new List<Client> { new Client { ClientId = "my_client", AllowedGrantTypes = GrantTypes.ClientCredentials, ClientSecrets = { new Secret("your_secret_here".Sha256()) }, // 允许访问的所有Scope,但请求时要按需指定 AllowedScopes = { "scope1", "scope2" } } })
4. 自定义ProfileService做细粒度控制(可选)
如果上述配置还满足不了你的特殊需求(比如某些声明需要动态判断是否返回),可以实现IProfileService手动过滤声明:
public class CustomProfileService : IProfileService { private readonly UserManager<IdentityUser> _userManager; private readonly IUserClaimsPrincipalFactory<IdentityUser> _claimsFactory; public CustomProfileService(UserManager<IdentityUser> userManager, IUserClaimsPrincipalFactory<IdentityUser> claimsFactory) { _userManager = userManager; _claimsFactory = claimsFactory; } public async Task GetProfileDataAsync(ProfileDataRequestContext context) { var user = await _userManager.GetUserAsync(context.Subject); var principal = await _claimsFactory.CreateAsync(user); var allClaims = principal.Claims.ToList(); // 根据当前请求的Scope过滤出允许的声明 var requestedScopes = context.RequestedResources.ApiResources.SelectMany(r => r.Scopes); var allowedClaims = new List<Claim>(); foreach (var scope in requestedScopes) { switch (scope.Name) { case "scope1": allowedClaims.AddRange(allClaims.Where(c => c.Type is "claim1" or "claim2")); break; case "scope2": allowedClaims.AddRange(allClaims.Where(c => c.Type is "claim3" or "claim4")); break; } } // 去重后返回 context.IssuedClaims = allowedClaims.DistinctBy(c => c.Type).ToList(); } public async Task IsActiveAsync(IsActiveContext context) { var user = await _userManager.GetUserAsync(context.Subject); context.IsActive = user != null; } }
然后在Startup里注册这个服务:
services.AddScoped<IProfileService, CustomProfileService>();
最后验证一下
配置完后,你可以用IdentityServer的令牌端点获取对应Scope的令牌,然后用JWT解析工具查看令牌内容,确认aud字段是对应的资源ID,且claims里只有该Scope的声明。
内容的提问来源于stack exchange,提问作者ArdAtak

