基于.NET Core的多租户Ocelot API网关按租户配置JWT密钥方案咨询
基于Ocelot实现多租户独立JWT密钥的方案
1. 先搞定租户标识提取
首先得从请求里精准识别租户,常用的三种方式选适合你的:
- 从请求头提取(比如
X-Tenant-ID字段) - 从子域名解析(比如
tenant1.yourdomain.com) - 从JWT的Claim中读取(比如预设的
tenant_idClaim)
示例代码:写一个租户解析接口和实现类,方便后续复用
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
相关产品推荐
相关产品推荐

