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

.NET 6基于Keycloak实现无需重启的多租户API授权方案问询

.NET 6 + Keycloak 运行时新增Realm无需重启的多租户授权方案

你之前依赖多JWT Schema的思路走不通,因为.NET的中间件管道和服务注册都是启动时固化的,运行时没法直接新增Schema或修改中间件。换个思路,用动态JWT验证逻辑适配多Realm,完全不用依赖预定义的Schema。

核心思路

Keycloak每个Realm生成的Token里都会带iss声明(格式一般为https://<keycloak-host>/realms/<realm-name>),我们可以从Token里解析出这个Issuer,然后动态获取对应Realm的验证配置(比如JWKS公钥),而非启动时就把所有Realm的Schema配置死。

具体实现步骤

1. 搭建动态Realm配置存储

把Realm的基础配置(Issuer、Audience)从appSettings.json迁移到支持运行时更新的存储(比如数据库、Redis),新增Realm时直接更新存储即可,不用修改配置文件重启应用。

先定义Realm配置模型:

public class KeycloakRealmConfig
{
    public string Issuer { get; set; }
    public string Audience { get; set; }
    public string JwksEndpoint => $"{Issuer}/protocol/openid-connect/certs";
}

再实现一个读写配置的服务:

public interface IRealmConfigProvider
{
    Task<KeycloakRealmConfig> GetRealmConfigByIssuer(string issuer);
    Task AddOrUpdateRealmConfig(KeycloakRealmConfig config);
}

// 示例:基于数据库的实现
public class DbRealmConfigProvider : IRealmConfigProvider
{
    private readonly IDbConnection _dbConn;

    public DbRealmConfigProvider(IDbConnection dbConn)
    {
        _dbConn = dbConn;
    }

    public async Task<KeycloakRealmConfig> GetRealmConfigByIssuer(string issuer)
    {
        return await _dbConn.QueryFirstOrDefaultAsync<KeycloakRealmConfig>(
            "SELECT Issuer, Audience FROM RealmConfigs WHERE Issuer = @Issuer",
            new { Issuer = issuer });
    }

    public async Task AddOrUpdateRealmConfig(KeycloakRealmConfig config)
    {
        await _dbConn.ExecuteAsync(
            "INSERT INTO RealmConfigs (Issuer, Audience) VALUES (@Issuer, @Audience) " +
            "ON DUPLICATE KEY UPDATE Audience = @Audience",
            config);
    }
}

2. 自定义JWT验证逻辑

放弃多Schema,注册一个默认JWT处理程序,通过JwtBearerEvents动态修改验证参数:

在Program.cs中配置服务和中间件:

builder.Services.AddScoped<IRealmConfigProvider, DbRealmConfigProvider>();
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            // 不硬编码Issuer和Audience,留到事件中动态验证
        };

        options.Events = new JwtBearerEvents
        {
            OnTokenValidated = async context =>
            {
                // 从Token中解析Issuer
                var issuer = context.SecurityToken.Claims.First(c => c.Type == JwtRegisteredClaimNames.Iss).Value;
                
                var realmConfigProvider = context.HttpContext.RequestServices.GetRequiredService<IRealmConfigProvider>();
                var realmConfig = await realmConfigProvider.GetRealmConfigByIssuer(issuer);

                if (realmConfig == null)
                {
                    context.Fail("无效的Realm Issuer");
                    return;
                }

                // 验证Audience
                if (!context.SecurityToken.Claims.Any(c => c.Type == JwtRegisteredClaimNames.Aud && c.Value == realmConfig.Audience))
                {
                    context.Fail("无效的Audience");
                    return;
                }
            }
        };

        // 动态拉取JWKS配置,替换默认硬编码逻辑
        options.ConfigurationManager = new ConfigurationManager<OpenIdConnectConfiguration>(
            "placeholder", new OpenIdConnectConfigurationRetriever(), new HttpDocumentRetriever());

        options.ConfigurationManager.GetConfigurationAsync = async (cancellationToken) =>
        {
            var httpContext = options.HttpContextAccessor.HttpContext;
            if (httpContext == null)
                throw new InvalidOperationException("HttpContext不可用");

            var token = httpContext.Request.Headers.Authorization.FirstOrDefault()?.Split(" ").Last();
            if (string.IsNullOrEmpty(token))
                throw new UnauthorizedAccessException("未提供Token");

            var jwtToken = new JwtSecurityTokenHandler().ReadJwtToken(token);
            var issuer = jwtToken.Issuer;

            var realmConfigProvider = httpContext.RequestServices.GetRequiredService<IRealmConfigProvider>();
            var realmConfig = await realmConfigProvider.GetRealmConfigByIssuer(issuer);

            if (realmConfig == null)
                throw new UnauthorizedAccessException("无效的Realm");

            // 拉取对应Realm的JWKS配置
            var retriever = new OpenIdConnectConfigurationRetriever();
            var docRetriever = new HttpDocumentRetriever();
            return await retriever.GetConfigurationAsync(realmConfig.JwksEndpoint, docRetriever, cancellationToken);
        };
    });

builder.Services.AddAuthorization();

3. 缓存JWKS减少重复请求

Keycloak的JWKS不会频繁更新,用IDistributedCache缓存配置,减少对Keycloak的请求:

修改GetConfigurationAsync方法加入缓存逻辑:

options.ConfigurationManager.GetConfigurationAsync = async (cancellationToken) =>
{
    var httpContext = options.HttpContextAccessor.HttpContext;
    if (httpContext == null)
        throw new InvalidOperationException("HttpContext不可用");

    var token = httpContext.Request.Headers.Authorization.FirstOrDefault()?.Split(" ").Last();
    if (string.IsNullOrEmpty(token))
        throw new UnauthorizedAccessException("未提供Token");

    var jwtToken = new JwtSecurityTokenHandler().ReadJwtToken(token);
    var issuer = jwtToken.Issuer;

    var cache = httpContext.RequestServices.GetRequiredService<IDistributedCache>();
    var cacheKey = $"Keycloak_JWKS_{issuer}";
    var cachedConfig = await cache.GetStringAsync(cacheKey, cancellationToken);

    if (!string.IsNullOrEmpty(cachedConfig))
    {
        return JsonSerializer.Deserialize<OpenIdConnectConfiguration>(cachedConfig);
    }

    var realmConfigProvider = httpContext.RequestServices.GetRequiredService<IRealmConfigProvider>();
    var realmConfig = await realmConfigProvider.GetRealmConfigByIssuer(issuer);

    if (realmConfig == null)
        throw new UnauthorizedAccessException("无效的Realm");

    var retriever = new OpenIdConnectConfigurationRetriever();
    var docRetriever = new HttpDocumentRetriever();
    var config = await retriever.GetConfigurationAsync(realmConfig.JwksEndpoint, docRetriever, cancellationToken);

    // 缓存1小时,可根据实际调整
    await cache.SetStringAsync(cacheKey, JsonSerializer.Serialize(config), new DistributedCacheEntryOptions
    {
        AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1)
    }, cancellationToken);

    return config;
};

运行时新增Realm流程

  1. 在Keycloak中创建新Realm,记录Issuer和Audience
  2. 通过后台管理接口或工具调用IRealmConfigProvider.AddOrUpdateRealmConfig,将新Realm配置存入数据库
  3. 新Realm客户端生成的Token可直接被API验证,无需重启应用

注意事项

  • 确保IRealmConfigProvider实现线程安全,避免并发读写问题
  • 合理设置JWKS缓存过期时间,避免Keycloak更新公钥后API无法验证新Token
  • 处理Token解析失败、Realm配置不存在等异常,返回合适的HTTP状态码

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.09 02:50:33