多租户应用中基于TenantId动态配置AddJwtBearer的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
相关产品推荐
相关产品推荐

