You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

身份认证:如何将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。

  1. 修正IdentityServer的Scope与客户端配置

    • 确保roles ApiScope被正确定义,关联role claim:
      // IdentityServer配置代码
      new ApiScope("roles", "用户角色", new[] { "role" })
      
    • 给BFF客户端授权roles scope,并确保客户端配置开启相关Claim传递(若需要):
      // IdentityServer客户端配置
      new Client
      {
          ClientId = "your-bff-client-id",
          // 其他配置...
          AllowedScopes = { "openid", "profile", "email", "roles", "offline_access" },
          AlwaysSendClientClaims = true // 按需开启
      }
      
  2. 修复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依赖。

  1. 在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>();
    
  2. 实现下游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);
        }
    }
    
  3. 下游服务配置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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.17 05:35:21