.NET 5多租户场景下Identity Server 4 Authority参数动态配置问题
问题根因
你当前的方案存在三个核心问题导致动态配置不生效:
- 中间件执行顺序错误:租户配置中间件放在了认证、授权中间件之后,请求到达时先执行了认证逻辑,此时用的还是启动时的默认配置,后续修改的Authority完全没有生效机会
- 运行期改配置不生效:
AddIdentityServerAuthentication封装的JWT认证逻辑会在应用启动阶段就基于初始配置初始化验证器、预拉取OIDC元数据(包括签名公钥、合法Issuer等),运行期修改IOptions的属性不会触发验证器重新初始化 - 单例配置并发风险:
IOptions<T>是全局单例对象,多租户并发请求时修改它的属性会导致配置混乱,A租户的请求可能拿到B租户的Authority配置
解决方案
我们可以弃用封装好的AddIdentityServerAuthentication,直接使用JwtBearer认证并自定义动态验证逻辑,步骤如下:
1. 调整中间件顺序
先解析租户配置,再执行认证逻辑:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { //other settings app.UseTenantConfig(); // 移到认证中间件之前 app.UseAuthentication(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers() .RequireAuthorization("ApiScope"); }); //other settings }
2. 调整ConfigureServices的认证配置
自定义Issuer验证和签名密钥动态获取逻辑:
public void ConfigureServices(IServiceCollection services) { //other settings services.AddAuthentication(option => { option.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; option.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }).AddJwtBearer(options => { options.RequireHttpsMetadata = true; options.SaveToken = true; options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, // 不设置固定的Issuer和签名密钥,走自定义逻辑 IssuerValidator = (issuer, token, parameters) => { var httpContext = parameters.Item as HttpContext; var tenantInfo = httpContext.RequestServices.GetRequiredService<TenantInfo>(); var tenantService = httpContext.RequestServices.GetRequiredService<ITenantParameterService>(); var expectedAuthority = tenantService.GetParameterByTenant(tenantInfo.Address, "Authority"); if (issuer.Equals(expectedAuthority, StringComparison.OrdinalIgnoreCase)) { return issuer; } throw new SecurityTokenInvalidIssuerException($"Invalid issuer {issuer}"); }, IssuerSigningKeyResolver = (token, securityToken, kid, parameters) => { var httpContext = parameters.Item as HttpContext; var tenantInfo = httpContext.RequestServices.GetRequiredService<TenantInfo>(); var tenantService = httpContext.RequestServices.GetRequiredService<ITenantParameterService>(); var authority = tenantService.GetParameterByTenant(tenantInfo.Address, "Authority"); // 生产环境建议对元数据做内存缓存,避免每次请求都拉取 var httpClient = new HttpClient(); var disco = httpClient.GetDiscoveryDocumentAsync(authority).GetAwaiter().GetResult(); if (disco.IsError) throw new InvalidOperationException(disco.Error); return disco.KeySet.Keys.Where(k => k.Kid == kid); } }; options.Events = new JwtBearerEvents { OnMessageReceived = context => { // 把当前请求上下文传入验证参数,供自定义逻辑使用 context.Options.TokenValidationParameters.Item = context.HttpContext; return Task.CompletedTask; } }; }); services.AddAuthorization(options => { options.AddPolicy("ApiScope", policy => { policy.RequireAuthenticatedUser(); policy.RequireClaim("scope", "any1", "any2"); }); }); //other settings }
3. 简化租户中间件逻辑
不要再修改全局IOptions的值,只要保证TenantInfo被正确存入当前请求的服务容器即可:
public async Task Invoke(HttpContext context, ITenantParameterService tenantParameterService) { var tenantInfo = context.RequestServices.GetRequiredService<TenantInfo>(); //获取租户域名 var connectionString = tenantParameterService.GetConnectionStringByTenant(tenantInfo.Address); var myContext = context.RequestServices.GetService<MyDbContext>(); myContext?.Database.SetConnectionString(connectionString); await _next(context); }
优化提示
生产环境建议对每个租户的OIDC元数据做内存缓存,缓存过期时间可设为1小时,避免每次请求都拉取元数据影响接口性能。
内容的提问来源于stack exchange,提问作者Leomar de Souza
相关产品推荐
相关产品推荐

