身份认证:如何将Claims传递至下游服务
我正在使用Duende-IdentityServer搭建分布式系统,架构中采用BFF(Backend For Front)作为客户端的API网关。当用户通过BFF登录后,我希望请求能从BFF传递至下游服务(我使用了GraphQL的Schema拼接与联邦,但这可能与问题无关)。由于下游服务需自主控制数据授权,我希望将BFF收到的Claims转发给下游服务,设想通过附加包含Claims的JWT Bearer实现,且下游服务无需联系身份服务器即可验证Claims。
我已进行了一些尝试,但在OAuth2和OIDC的配置中遇到困惑。以下是我的操作:
BFF端 Program.cs 配置
//program.cs builder.Services.AddHttpClient(GraphQLSchemas.Identity, c => c.BaseAddress = new Uri("https://localhost:7500/graphql")).AddUserAccessTokenHandler(); builder.Services.AddGraphQLServer() .AddRemoteSchemasFromRedis("GraphQL", sp => sp.GetRequiredService<ConnectionMultiplexer>()) .ModifyOptions(x => x.RemoveUnreachableTypes = true); services.AddBff(); services.AddAuthentication(options => { options.DefaultScheme = "Bff-Cookie"; options.DefaultChallengeScheme = "oidc"; options.DefaultSignOutScheme = "oidc"; }) .AddCookie("Bff-Cookie", options => { // set session lifetime options.ExpireTimeSpan = TimeSpan.FromHours(8); // sliding or absolute options.SlidingExpiration = true; // host prefixed cookie name options.Cookie.Name = bffOptions.Cookie.Name ; options.Cookie.Domain = bffOptions.Cookie.Domain; // strict SameSite handling options.Cookie.SameSite = SameSiteMode.Lax; options.Cookie.HttpOnly = true; options.Cookie.SecurePolicy = CookieSecurePolicy.Always; }) .AddOpenIdConnect("oidc", options => { options.Authority = bffOptions.IdentityServer.Host; // confidential client using code flow + PKCE options.ClientId = bffOptions.IdentityServer.ClientId; options.ResponseType = OpenIdConnectResponseType.Code; options.ResponseMode = "query"; options.MapInboundClaims = false; options.GetClaimsFromUserInfoEndpoint = false; options.SaveTokens = true; // request scopes +refresh tokens options.Scope.Clear(); options.Scope.Add("openid"); options.Scope.Add("profile"); options.Scope.Add("email"); //options.Scope.Add("Administrator"); options.Scope.Add("roles"); options.Scope.Add("offline_access"); options.ClaimActions.MapJsonKey("role", "role", "role"); options.TokenValidationParameters.RoleClaimType = JwtClaimTypes.Role; }); /// code omitted for brevity app.UseBff();
登录BFF后,我能获取到包含角色等的Claims,但access_token中并未包含这些内容。当HttpClient使用.AddUserAccessTokenHandler();时,仅access_token被传递至下游服务。
下游服务端 Program.cs 配置
//program.cs services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options => { options.Authority = "https://localhost:7500"; options.MapInboundClaims = false; options.TokenValidationParameters = new TokenValidationParameters() { ValidateAudience = false, ValidTypes = new[] { "at+jwt" }, NameClaimType = "name", RoleClaimType = "role" }; }); //code omitted for brevity app.MapGraphQL().RequireAuthorization(new AuthorizeAttribute { AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme }).AllowAnonymous();
可以看到下游服务未收到Role等Claims。
请问如何让下游服务接收到这些Claims?优先无需联系IdentityServer的方案(同时希望下游能验证收到的JWT)。
另外,我尝试过实现ProfileService,但将其注册到DI容器后,BFF登录失败,且断点未触发,尚未找到原因。
方案一:让IdentityServer在Access Token中包含所需Claims
要实现下游离线验证JWT,核心是让IdentityServer颁发的自包含Access Token本身包含Role等目标Claims,下游直接验证JWT签名即可,无需联系IdentityServer。
修正IdentityServer的Scope与客户端配置
- 确保
rolesApiScope被正确定义,关联roleclaim:// IdentityServer配置代码 new ApiScope("roles", "用户角色", new[] { "role" }) - 给BFF客户端授权
rolesscope,并确保客户端配置开启相关Claim传递(若需要):// IdentityServer客户端配置 new Client { ClientId = "your-bff-client-id", // 其他配置... AllowedScopes = { "openid", "profile", "email", "roles", "offline_access" }, AlwaysSendClientClaims = true // 按需开启 }
- 确保
修复ProfileService注册问题
之前ProfileService未生效是因为注册位置错误,需在IdentityServer的DI链中替换默认实现:// IdentityServer的Program.cs builder.Services.AddIdentityServer() .AddProfileService<CustomProfileService>();自定义ProfileService需实现
IProfileService,重点在GetProfileDataAsync中把用户Claims添加到Access Token: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) return; var claims = new List<Claim> { new Claim(JwtClaimTypes.Name, user.UserName), new Claim(JwtClaimTypes.Email, user.Email) }; // 添加角色Claims到Token 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; } }注意:BFF的OIDC配置中
GetClaimsFromUserInfoEndpoint = false时,必须确保IdentityServer直接把Claims写入Access Token,否则BFF Cookie中的Claims仅来自ID Token,Access Token仍为空。
方案二:BFF自行签发下游专用JWT
若不想修改IdentityServer配置,可让BFF基于当前用户的Claims,签发仅用于下游服务的JWT,下游用预共享密钥验证,完全脱离IdentityServer依赖。
在BFF中配置JWT签发服务
// BFF的Program.cs builder.Services.AddSingleton(new JwtIssuerOptions { SigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("your-strong-shared-secret-key")), Issuer = "BFF-Gateway", Audience = "Downstream-Services", Expiration = TimeSpan.FromMinutes(15) }); // 替换AddUserAccessTokenHandler为自定义Handler builder.Services.AddHttpClient(GraphQLSchemas.Identity, c => c.BaseAddress = new Uri("https://localhost:7500/graphql")) .AddHttpMessageHandler<DownstreamJwtHandler>(); builder.Services.AddScoped<DownstreamJwtHandler>();实现下游JWT转发Handler
从当前用户Claims生成JWT并附加到请求头:public class DownstreamJwtHandler : DelegatingHandler { private readonly IHttpContextAccessor _httpContextAccessor; private readonly JwtIssuerOptions _jwtOptions; public DownstreamJwtHandler(IHttpContextAccessor httpContextAccessor, JwtIssuerOptions jwtOptions) { _httpContextAccessor = httpContextAccessor; _jwtOptions = jwtOptions; } protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { var user = _httpContextAccessor.HttpContext?.User; if (user?.Identity?.IsAuthenticated == true) { // 提取当前用户需转发的Claims var claims = user.Claims.ToList(); claims.Add(new Claim("iss", _jwtOptions.Issuer)); var tokenHandler = new JwtSecurityTokenHandler(); var tokenDescriptor = new SecurityTokenDescriptor { Subject = new ClaimsIdentity(claims), Expires = DateTime.UtcNow.Add(_jwtOptions.Expiration), SigningCredentials = new SigningCredentials(_jwtOptions.SigningKey, SecurityAlgorithms.HmacSha256Signature), Issuer = _jwtOptions.Issuer, Audience = _jwtOptions.Audience }; var token = tokenHandler.CreateToken(tokenDescriptor); request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokenHandler.WriteToken(token)); } return await base.SendAsync(request, cancellationToken); } }下游服务配置JWT验证
修改下游服务用预共享密钥验证,无需联系IdentityServer:// 下游服务Program.cs services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options => { options.MapInboundClaims = false; options.TokenValidationParameters = new TokenValidationParameters() { ValidateIssuer = true, ValidIssuer = "BFF-Gateway", ValidateAudience = true, ValidAudience = "Downstream-Services", ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("your-strong-shared-secret-key")), NameClaimType = "name", RoleClaimType = "role" }; });
快速排查点
- 用JWT解码工具解析IdentityServer颁发的Access Token,确认是否包含role等Claims。若没有,问题出在IdentityServer配置,而非BFF或下游。
- 确认IdentityServer颁发的是自包含JWT(默认配置),而非引用令牌(Reference Token),引用令牌必须联系IdentityServer验证,不符合离线需求。
- BFF的
options.SaveTokens = true确保Access Token被保存,但仅当Access Token本身包含Claims时,转发才有意义。
内容的提问来源于stack exchange,提问作者Dylan Snel

