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

多租户应用中基于TenantId动态配置AddJwtBearer的Authority

多租户KeyCloak身份验证动态Authority配置方案

问题背景

我开发了一个以KeyCloak作为身份代理的多租户应用,Token中包含TenantId字段,可据此识别用户所属租户。原本验证Token时使用AddJwtBearer并配置固定的Authority,现在需要根据Token中的TenantId从配置中动态获取对应的Authority进行验证。

已将原有的JwtConfiguration改为存储多租户信息列表,期望在每次请求的Token验证环节,根据Token中的TenantId获取对应租户信息,设置对应的Authority及验证参数。

尝试通过自定义中间件在认证管道前修改JwtBearerOptions时,出现错误:

"Unable to validate issuer. validationParameters.ValidIssuer is null or whitespace AND validationParameters.ValidIssuers is null or empty."

最终通过在TokenValidationParameters中添加ValidIssuer和IssuerSigningKeys解决了该问题。

原认证配置代码

public static class KeycloakAuthenticationExtensions
{
    public static void AddKeycloakAuthentication(this IServiceCollection services, IConfiguration configuration)
    {
        var jwtConfiguration = configuration.GetSection(Core.Authentication.Constants.JwtConfigurationSection).Get<JwtConfiguration>();

        services.AddOptions<JwtConfiguration>().Bind(configuration.GetSection(Authentication.Constants.JwtConfigurationSection));
        services.AddAuthentication(options =>
        {
            options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
            options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;

        }).AddJwtBearer(options =>
        {
            options.Authority = jwtConfiguration!.Authority;
            options.SaveToken = false;
            options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
            {
                ValidAudiences = jwtConfiguration.Audiences,
                ValidateAudience = true,
                ValidateIssuer = true,
                ValidateLifetime = true,
                ValidateIssuerSigningKey = true,
                // Name claim和role claim映射
                NameClaimType = ApiConstants.PreferredUserNameClaim
            };

            options.Events = new JwtBearerEvents()
            {
                OnMessageReceived = context =>
                {
                    return Task.CompletedTask;
                },
                OnAuthenticationFailed = context =>
                {
                    context.Response.StatusCode = StatusCodes.Status401Unauthorized;
                    return context.Response.WriteAsync(context.Request.Headers[ApiConstants.AuthorizationHeader].ToString());
                },
                OnTokenValidated = context =>
                {
                    return Task.CompletedTask;
                }
            };

        });
        services.AddSingleton<ITokenProvider, KeyCloakJwtTokenProvider>();
        services.AddSingleton<ITokenStore, InMemoryCachedTokenStore>();
        services.AddTransient<AuthenticationHttpMessageHandler>();
        services.AddHttpClient<ITokenProvider, KeyCloakJwtTokenProvider>(client =>
        {
            client.BaseAddress = new Uri(jwtConfiguration!.Authority + ApiConstants.TokenEndpoint);
        });
    }
}

修改后的多租户配置尝试

services.AddOptions<TenantConfiguration>().Bind(configuration.GetSection(Authentication.Constants.TenantConfiguration));
    services.AddAuthentication(options =>
    {
        options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
        options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
    }).AddJwtBearer(options =>
    {
        var tenant1 = jwtConfiguration!.TenantInfo.FirstOrDefault(x => x.TenantId.Equals("**tenantId**", StringComparison.OrdinalIgnoreCase));
        options.Authority = royHillTenant?.Authority;
        options.SaveToken = false;
        options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
        {
            ValidAudiences = royHillTenant?.Audiences,
            ValidateAudience = true,
            ValidateIssuer = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            // Name claim和role claim映射
            NameClaimType = ApiConstants.PreferredUserNameClaim
        };

        options.Events = new JwtBearerEvents()
        {
            OnMessageReceived = context =>
            {
                return Task.CompletedTask;
            },
            OnAuthenticationFailed = context =>
            {
                context.Response.StatusCode = StatusCodes.Status401Unauthorized;
                return context.Response.WriteAsync(context.Request.Headers[ApiConstants.AuthorizationHeader].ToString());
            },
            OnTokenValidated = context =>
            {
                return Task.CompletedTask;
            }
        };

    });
    services.AddSingleton<ITokenProvider, KeyCloakJwtTokenProvider>();
    services.AddSingleton<ITokenStore, InMemoryCachedTokenStore>();
    services.AddTransient<AuthenticationHttpMessageHandler>();
    services.AddHttpClient<ITokenProvider, KeyCloakJwtTokenProvider>();// client =>
    //{
    //    client.BaseAddress = new Uri(jwtConfiguration!.Authority + ApiConstants.TokenEndpoint);
    //});
}

自定义中间件代码

namespace Optym.RMX.Core.Hosting.WebAPI
{
    public class JwtBearerOptionMiddleware
    {
        private readonly RequestDelegate _next;
        private readonly IRailMaxLogger _logger;
        private readonly IServiceProvider serviceProvider;
        private readonly IHttpContextAccessor _contextAccessor;
        private readonly TenantConfiguration tenantConfiguration;
        public JwtBearerOptionMiddleware(RequestDelegate next, IRailMaxLogger logger, IServiceProvider serviceProvider
            , IHttpContextAccessor contextAccessor, IOptions<TenantConfiguration> tenantConfiguration)
        {
            this._logger = logger;
            this._next = next;
            this.serviceProvider = serviceProvider;
            this._contextAccessor = contextAccessor;
            this.tenantConfiguration = tenantConfiguration.Value;
        }
        public async Task InvokeAsync(HttpContext httpContext)
        {
           // string token = this._contextAccessor.HttpContext?.Request.Headers["Authorization"];
            var accessToken = this._contextAccessor.HttpContext?.Request.Headers.Authorization.FirstOrDefault()?.Split(" ").Last();
            var handler = new JwtSecurityTokenHandler();
            var jwtDecoded = handler.ReadToken(accessToken) as JwtSecurityToken;
            string tenantId = jwtDecoded?.Claims.First(claim => claim.Type == "TenantId").Value;

            var tenantInfo = this.tenantConfiguration.TenantInfo.FirstOrDefault(x => x.TenantId.Equals(tenantId, StringComparison.OrdinalIgnoreCase));
            if (tenantInfo == null)
            {
                throw new RmxException($"No tenant information found for Tenant Id {tenantId}");
            }

            //var claims = this._contextAccessor.HttpContext?.User.Identities.First().Claims;
            // string tenantId = claims?.First(claim => claim.Type == "TenantId").Value;

            var jwtoptionsMonitor = serviceProvider.GetService<IOptionsMonitor<JwtBearerOptions>>();
            var jwtoptions = jwtoptionsMonitor.Get(JwtBearerDefaults.AuthenticationScheme);

            // 动态修改jwtoptions
            jwtoptions.Authority = tenantInfo.Authority;
            jwtoptions.TokenValidationParameters.ValidAudiences = tenantInfo.Audiences;

            jwtoptions.SaveToken = false;
            jwtoptions.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
            {
                ValidAudiences = tenantInfo.Audiences,
                ValidateAudience = true,
                ValidateIssuer = true,
                ValidateLifetime = true,
                ValidateIssuerSigningKey = true,
                // Name claim和role claim映射
                NameClaimType = ApiConstants.PreferredUserNameClaim
            };
            jwtoptions.Events = new JwtBearerEvents()
            {
                OnMessageReceived = context =>
                {
                    return Task.CompletedTask;
                },
                OnAuthenticationFailed = context =>
                {
                    context.Response.StatusCode = StatusCodes.Status401Unauthorized;
                    return context.Response.WriteAsync(context.Request.Headers[ApiConstants.AuthorizationHeader].ToString());
                },
                OnTokenValidated = context =>
                {
                    return Task.CompletedTask;
                }
            };
            await this._next(httpContext);
        }
    }
}

问题解决

出现上述错误的核心原因是:动态修改JwtBearerOptions时,未正确配置TokenValidationParameters中的有效签发方(ValidIssuer/ValidIssuers)和签发签名密钥(IssuerSigningKeys)。

解决方法是在动态配置时,从租户的Authority端点获取OpenID配置,提取对应签发方和签名密钥:

// 从租户Authority获取OpenID配置及签名密钥
var configurationManager = new ConfigurationManager<OpenIdConnectConfiguration>(
    $"{tenantInfo.Authority}/.well-known/openid-configuration",
    new OpenIdConnectConfigurationRetriever());
var openIdConfig = await configurationManager.GetConfigurationAsync(CancellationToken.None);

// 重新设置TokenValidationParameters
jwtoptions.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
    ValidAudiences = tenantInfo.Audiences,
    ValidIssuer = openIdConfig.Issuer,
    IssuerSigningKeys = openIdConfig.SigningKeys,
    ValidateAudience = true,
    ValidateIssuer = true,
    ValidateLifetime = true,
    ValidateIssuerSigningKey = true,
    NameClaimType = ApiConstants.PreferredUserNameClaim
};

内容的提问来源于stack exchange,提问作者RashmiMs

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.24 07:45:00