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

基于.NET Core的多租户Ocelot API网关按租户配置JWT密钥方案咨询

基于Ocelot实现多租户独立JWT密钥的方案

1. 先搞定租户标识提取

首先得从请求里精准识别租户,常用的三种方式选适合你的:

  • 从请求头提取(比如X-Tenant-ID字段)
  • 从子域名解析(比如tenant1.yourdomain.com)
  • 从JWT的Claim中读取(比如预设的tenant_id Claim)

示例代码:写一个租户解析接口和实现类,方便后续复用

public interface ITenantResolver
{
    string ResolveTenant(HttpContext context);
}

public class HeaderTenantResolver : ITenantResolver
{
    public string ResolveTenant(HttpContext context)
    {
        context.Request.Headers.TryGetValue("X-Tenant-ID", out var tenantId);
        return tenantId.FirstOrDefault();
    }
}

2. 租户密钥的存储与动态获取

需要一个密钥存储服务,把每个租户的JWT密钥(对称密钥或非对称公钥)存在数据库、Redis或者配置中心里,然后写个服务来动态获取:

public interface ITenantJwtKeyProvider
{
    Task<SecurityKey> GetTenantKeyAsync(string tenantId);
}

public class DatabaseTenantKeyProvider : ITenantJwtKeyProvider
{
    private readonly YourDbContext _dbContext;

    public DatabaseTenantKeyProvider(YourDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public async Task<SecurityKey> GetTenantKeyAsync(string tenantId)
    {
        var tenantConfig = await _dbContext.TenantConfigs.FirstOrDefaultAsync(t => t.TenantId == tenantId);
        if (tenantConfig == null)
            throw new UnauthorizedAccessException("无效租户");
        
        // 对称密钥示例,非对称场景换成RSA/ECDSA加载公钥
        return new SymmetricSecurityKey(Encoding.UTF8.GetBytes(tenantConfig.JwtSecret));
    }
}

3. 自定义Ocelot认证逻辑

Ocelot默认认证是全局的,得改造成动态适配租户密钥的逻辑,两种可选方式:

方式一:扩展JWT Bearer认证事件

在JWT验证阶段动态替换当前租户的密钥:

public class TenantJwtBearerEvents : JwtBearerEvents
{
    private readonly ITenantResolver _tenantResolver;
    private readonly ITenantJwtKeyProvider _keyProvider;

    public TenantJwtBearerEvents(ITenantResolver tenantResolver, ITenantJwtKeyProvider keyProvider)
    {
        _tenantResolver = tenantResolver;
        _keyProvider = keyProvider;
    }

    public override async Task TokenValidated(TokenValidatedContext context)
    {
        var tenantId = _tenantResolver.ResolveTenant(context.HttpContext);
        var securityKey = await _keyProvider.GetTenantKeyAsync(tenantId);
        
        // 用当前租户密钥重新验证签名
        var validationParams = new TokenValidationParameters
        {
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = securityKey,
            ValidateIssuer = false, // 如果租户自主管理Issuer,可动态从租户配置获取
            ValidateAudience = false,
            ValidateLifetime = true
        };

        var handler = new JwtSecurityTokenHandler();
        try
        {
            handler.ValidateToken(context.SecurityToken.RawData, validationParams, out _);
        }
        catch (SecurityTokenValidationException ex)
        {
            context.Fail(ex.Message);
        }

        await base.TokenValidated(context);
    }
}

然后在Program.cs里注册配置:

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.EventsType = typeof(TenantJwtBearerEvents);
        // 基础配置留空,动态加载租户专属参数
    });

builder.Services.AddScoped<TenantJwtBearerEvents>();
builder.Services.AddScoped<ITenantResolver, HeaderTenantResolver>();
builder.Services.AddScoped<ITenantJwtKeyProvider, DatabaseTenantKeyProvider>();

方式二:编写自定义Ocelot中间件

如果要在网关层面直接处理认证,写个中间件插在Ocelot前面:

public class TenantAuthenticationMiddleware
{
    private readonly RequestDelegate _next;

    public TenantAuthenticationMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context, ITenantResolver tenantResolver, ITenantJwtKeyProvider keyProvider)
    {
        var tenantId = tenantResolver.ResolveTenant(context);
        if (string.IsNullOrEmpty(tenantId))
        {
            context.Response.StatusCode = StatusCodes.Status401Unauthorized;
            await context.Response.WriteAsync("缺少租户标识");
            return;
        }

        var authHeader = context.Request.Headers.Authorization.FirstOrDefault();
        if (string.IsNullOrEmpty(authHeader) || !authHeader.StartsWith("Bearer "))
        {
            context.Response.StatusCode = StatusCodes.Status401Unauthorized;
            await context.Response.WriteAsync("认证头无效");
            return;
        }

        var token = authHeader.Substring("Bearer ".Length);
        var securityKey = await keyProvider.GetTenantKeyAsync(tenantId);
        
        var validationParams = new TokenValidationParameters
        {
            IssuerSigningKey = securityKey,
            ValidateIssuerSigningKey = true,
            ValidateIssuer = false,
            ValidateAudience = false,
            ValidateLifetime = true
        };

        try
        {
            var handler = new JwtSecurityTokenHandler();
            var claimsPrincipal = handler.ValidateToken(token, validationParams, out _);
            context.User = claimsPrincipal;
        }
        catch
        {
            context.Response.StatusCode = StatusCodes.Status401Unauthorized;
            await context.Response.WriteAsync("当前租户的Token无效");
            return;
        }

        await _next(context);
    }
}

注册中间件:

app.UseMiddleware<TenantAuthenticationMiddleware>();
app.UseOcelot().Wait();

4. Ocelot路由配置优化

在ocelot.json里配置路由时,可把租户标识作为路由前缀,同时确保后端服务能拿到租户信息:

{
  "Routes": [
    {
      "DownstreamPathTemplate": "/api/{everything}",
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        {
          "Host": "your-backend-service",
          "Port": 5001
        }
      ],
      "UpstreamPathTemplate": "/{tenantId}/api/{everything}",
      "UpstreamHttpMethod": ["Get", "Post"],
      "AuthenticationOptions": {
        "AuthenticationProviderKey": "Bearer",
        "AllowedScopes": []
      }
    }
  ]
}

5. 安全与性能优化要点

  • 缓存租户密钥:用Redis缓存高频访问租户的密钥,减少数据库查询
  • 支持密钥轮换:允许租户更新密钥,同时要及时清除对应缓存
  • 前置租户校验:在认证前先验证租户是否存在,拦截无效请求
  • 优先用非对称密钥:让租户保留私钥,网关存储公钥,安全性更高

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.06 14:33:14