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

.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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.01 22:54:03