如何用IIdentityServerBuilder的AddApiAuthorization配置Identity Server与NSwag API并获取用户声明
问题背景
使用Identity Server搭建认证/授权流程,通过NSwag中间件让用户在Swagger API中完成授权,访问标记有[Authorize]特性的端点。当前流程可正常跳转至Identity Server登录页,登录后返回Swagger API,但请求头中的JWT Bearer令牌仅包含用户的sub(GUID)信息,无法获取用户名、邮箱、角色等声明,无法基于声明配置访问策略。
已完成的核心配置如下:
1. NSwag OAuth2安全方案配置
services.AddMvcCore().AddApiExplorer(); services.AddOpenApiDocument(settings => { settings.Title = "MyProject Services"; settings.Version = "1.0"; settings.AddSecurity("oauth2", new NSwag.OpenApiSecurityScheme { Type = NSwag.OpenApiSecuritySchemeType.OAuth2, Flow = NSwag.OpenApiOAuth2Flow.AccessCode, AuthorizationUrl = "/connect/authorize", TokenUrl = "/connect/token", Scopes = new Dictionary<string, string> { { "MyProjectServicesAPI", "API Access" } } }); settings.OperationProcessors.Add(new AspNetCoreOperationSecurityScopeProcessor("oauth2")); });
2. Startup.Configure中的OAuth2客户端设置
app.UseOpenApi(); app.UseSwaggerUi3(options => { options.OAuth2Client = new NSwag.AspNetCore.OAuth2ClientSettings { ClientId = "MyProjectAPI", ClientSecret = "mysecret", UsePkceWithAuthorizationCodeGrant = true }; });
3. Identity Server核心配置(Startup.ConfigureServices)
services.AddIdentityServer() .AddDeveloperSigningCredential() .AddApiAuthorization<ApplicationUser, ApplicationDbContext>(options => { options.IdentityResources = new IdentityResourceCollection { new IdentityResources.OpenId(), new IdentityResources.Profile(), new IdentityResources.Email(), new IdentityResource { Name = "roles", DisplayName = "roles", UserClaims = new List<string> { JwtClaimTypes.Role } }, new IdentityResource { Name = "basicInfo", DisplayName = "basic info", UserClaims = new List<string> { JwtClaimTypes.PreferredUserName } } }; options.Clients = new ClientCollection { new Client { ClientId = "MyProjectAPI", ClientName = "My Project Services API", ClientSecrets = { new Secret("mysecret".Sha256()) }, AllowedGrantTypes = GrantTypes.Code, AllowAccessTokensViaBrowser = true, RedirectUris = { "https://localhost:44319/swagger/oauth2-redirect.html" }, PostLogoutRedirectUris = { "https://localhost:44319/Identity/Account/Logout" }, AllowedScopes = { "basicInfo", "roles", "MyProjectServicesAPI", IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile, IdentityServerConstants.StandardScopes.Email, RequirePkce = true, RequireConsent = false } } }; }); services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddIdentityServerJwt() .AddJwtBearer(options => { options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters() { ValidateIssuer = true }; });
4. 管道配置
app.UseIdentityServer(); app.UseAuthentication(); app.UseAuthorization();
当前OIDC发现文档已包含所有配置的范围,但令牌仅显示MyProjectServicesAPI范围,疑问:是否需要手动将声明添加到Bearer令牌中?为何已配置范围,声明仍未显示?
1. 修正Swagger的OAuth2范围配置
当前NSwag仅请求了MyProjectServicesAPI范围,需将所需的身份范围加入Swagger的Scopes配置,确保授权时向Identity Server请求完整的范围:
settings.AddSecurity("oauth2", new NSwag.OpenApiSecurityScheme { // 保留原有Type、Flow、AuthorizationUrl、TokenUrl配置 Scopes = new Dictionary<string, string> { { "MyProjectServicesAPI", "API Access" }, { "openid", "OpenID Connect" }, { "profile", "User Profile" }, { "email", "User Email" }, { "roles", "User Roles" }, { "basicInfo", "Basic User Info" } } });
2. 修正Identity Server客户端配置的语法错误
客户端配置中AllowedScopes列表混入了RequirePkce和RequireConsent属性,需将这两个属性移到Client的根层级:
new Client { // 保留原有ClientId、ClientName、ClientSecrets等配置 AllowedScopes = { "basicInfo", "roles", "MyProjectServicesAPI", IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile, IdentityServerConstants.StandardScopes.Email }, RequirePkce = true, RequireConsent = false }
3. 确保Identity Server将身份声明注入访问令牌
默认情况下,Identity Server不会自动将身份资源的声明放入访问令牌,可通过两种方式实现:
方式一:在ApiResource中声明需要包含的用户声明
在AddApiAuthorization配置中添加ApiResources,指定需要注入访问令牌的用户声明:
options.ApiResources = new ApiResourceCollection { new ApiResource("MyProjectServicesAPI") { UserClaims = { JwtClaimTypes.Subject, JwtClaimTypes.PreferredUserName, JwtClaimTypes.Email, JwtClaimTypes.Role } } };
方式二:自定义ProfileService手动添加声明
创建自定义ProfileService,从用户存储中获取并注入所需声明:
public class CustomProfileService : IProfileService { private readonly UserManager<ApplicationUser> _userManager; public CustomProfileService(UserManager<ApplicationUser> userManager) { _userManager = userManager; } public async Task GetProfileDataAsync(ProfileDataRequestContext context) { var user = await _userManager.GetUserAsync(context.Subject); if (user == null) throw new ArgumentException("用户不存在"); var claims = new List<Claim> { new Claim(JwtClaimTypes.PreferredUserName, user.UserName), new Claim(JwtClaimTypes.Email, user.Email) }; var roles = await _userManager.GetRolesAsync(user); claims.AddRange(roles.Select(r => new Claim(JwtClaimTypes.Role, r))); context.IssuedClaims.AddRange(claims); } public async Task IsActiveAsync(IsActiveContext context) { var user = await _userManager.GetUserAsync(context.Subject); context.IsActive = user != null; } }
在Startup.ConfigureServices中注册该服务:
services.AddScoped<IProfileService, CustomProfileService>();
4. 简化认证中间件配置
移除多余的AddJwtBearer配置,AddIdentityServerJwt()已包含完整的JWT Bearer认证逻辑,重复配置可能导致冲突:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddIdentityServerJwt();
验证步骤
- 重启Identity Server和API服务
- 在Swagger中重新发起授权,确认授权弹窗显示所有添加的范围
- 调用带
[Authorize]的端点,通过jwt.io解析请求头中的JWT令牌,确认包含所需的用户名、邮箱、角色等声明
内容的提问来源于stack exchange,提问作者SigmaScout_12

