Duende IdentityServer:AddJwtBearer无法获取自定义声明,AddOpenIdConnect却正常
问题分析与解决方案
你的核心矛盾是:自定义用户声明(nickname、city、country)放在IdentityResource里时,Razor客户端能通过UserInfo端点拿到,但API的Access Token里没有;改成ApiScope后,API能拿到,但UI客户端又获取不到。本质是IdentityResource和ApiScope的声明分发逻辑不同:
- IdentityResource的声明默认只会出现在ID Token或UserInfo响应中,不会自动进入Access Token
- ApiScope的声明默认只会出现在Access Token中,不会进入ID Token或UserInfo响应
要实现UI和API同时获取到这些自定义声明,需要同时配置两种资源类型,并调整ProfileService的逻辑确保声明能分发到对应的令牌中。
具体实现步骤
1. 同时配置IdentityResource和ApiScope
在Identity Server的配置中,保留原有的myApi.Data身份资源(供UI获取),新增一个对应的API范围(供API获取):
// 身份资源:供UI客户端通过UserInfo或ID Token获取声明 IEnumerable<IdentityResource> IdentityResources = new List<IdentityResource> { new IdentityResources.OpenId(), new IdentityResources.Profile(), new IdentityResource("myApi.Data", new [] { nameof(User.NickName), nameof(User.Country), nameof(User.City) }) }; // API范围:供API从Access Token中获取声明 IEnumerable<ApiScope> ApiScopes = new List<ApiScope> { new ApiScope("api1", "My API"), new ApiScope(IdentityServerConstants.LocalApi.ScopeName), // 新增包含自定义声明的API范围 new ApiScope("api1.data", new [] { nameof(User.NickName), nameof(User.Country), nameof(User.City) }) }; // 更新客户端允许的范围,同时包含身份资源和API范围 IEnumerable<Client> Clients = new List<Client> { new Client { ClientId = "web", ClientSecrets = { new Secret("secret".Sha256()) }, AllowedGrantTypes = GrantTypes.Code, RedirectUris = { "https://localhost:7070/signin-oidc" }, PostLogoutRedirectUris = { "https://localhost:7070/signout-callback-oidc" }, AllowOfflineAccess = true, AllowedScopes = new List<string> { IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile, "api1", "myApi.Data", // UI用的身份资源 "api1.data", // API用的范围 IdentityServerConstants.LocalApi.ScopeName } } };
2. 调整CustomProfileService逻辑
修改GetProfileDataAsync方法,确保自定义声明能根据请求的资源类型,分别添加到身份令牌(供UI)和访问令牌(供API)中:
public class CustomProfileService : IProfileService { private readonly UserManager<User> _userManager; public CustomProfileService(UserManager<User> userManager) { _userManager = userManager; } public async Task GetProfileDataAsync(ProfileDataRequestContext context) { var user = await _userManager.GetUserAsync(context.Subject); if (user == null) throw new ArgumentException("用户不存在"); // 准备自定义声明集合 var customClaims = new List<Claim> { new Claim(nameof(User.NickName), user.NickName ?? string.Empty), new Claim(nameof(User.Country), user.Country ?? string.Empty), new Claim(nameof(User.City), user.City ?? string.Empty) }; // 如果请求包含身份资源myApi.Data,将声明添加到身份令牌/用户信息响应 if (context.RequestedResources.IdentityResources.Any(r => r.Name == "myApi.Data")) { context.IssuedClaims.AddRange(customClaims); } // 如果请求包含API范围api1.data,将声明添加到访问令牌 if (context.RequestedResources.ApiScopes.Any(s => s.Name == "api1.data")) { context.IssuedClaims.AddRange(customClaims); } // 补充基础用户声明(可选) var baseClaims = new List<Claim> { new Claim(JwtClaimTypes.Name, user.UserName) }; context.IssuedClaims.AddRange(baseClaims); } public async Task IsActiveAsync(IsActiveContext context) { var user = await _userManager.GetUserAsync(context.Subject); context.IsActive = user != null; } }
3. 客户端与API的配置确认
- Razor客户端:现有配置已经包含
myApi.Datascope,且开启了GetClaimsFromUserInfoEndpoint = true,无需修改,仍能通过User.Claims拿到自定义声明。 - API端:现有JwtBearer配置无需修改,只要客户端请求Access Token时包含
api1.datascope,API就能从User.Claims中解析出这些自定义声明。
原理说明
- Identity Server会根据请求的资源类型(IdentityResource/ApiScope),自动筛选ProfileService中添加的声明,分别注入到对应的令牌中。
- 这种方式既保证了UI客户端通过标准的OIDC流程获取用户身份信息,也让API能从Access Token中直接拿到所需的用户数据,避免了API额外调用UserInfo端点的开销。
内容的提问来源于stack exchange,提问作者Style
相关产品推荐
相关产品推荐

