using System.Security.Claims; using Microsoft.Extensions.Options; using System.Text.RegularExpressions; using YarpGateway.Services; namespace YarpGateway.Middleware; /// /// 租户路由中间件 /// /// 安全说明: /// 1. 验证 X-Tenant-Id Header 与 JWT 中的 tenant claim 一致 /// 2. 防止租户隔离绕过攻击 /// 3. 只有验证通过后才进行路由查找 /// public class TenantRoutingMiddleware { private readonly RequestDelegate _next; private readonly IRouteCache _routeCache; private readonly ILogger _logger; public TenantRoutingMiddleware( RequestDelegate next, IRouteCache routeCache, ILogger logger) { _next = next; _routeCache = routeCache; _logger = logger; } public async Task InvokeAsync(HttpContext context) { var headerTenantId = context.Request.Headers["X-Tenant-Id"].FirstOrDefault(); if (string.IsNullOrEmpty(headerTenantId)) { await _next(context); return; } // 安全验证:检查 Header 中的租户 ID 是否与 JWT 一致 if (context.User?.Identity?.IsAuthenticated == true) { var jwtTenantId = context.User.Claims .FirstOrDefault(c => c.Type == "tenant" || c.Type == "tenant_id")?.Value; if (!string.IsNullOrEmpty(jwtTenantId) && jwtTenantId != headerTenantId) { // 记录安全事件 _logger.LogWarning( "Tenant ID mismatch detected! JWT tenant: {JwtTenant}, Header tenant: {HeaderTenant}, User: {User}", jwtTenantId, headerTenantId, context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? "unknown" ); // 拒绝请求 context.Response.StatusCode = StatusCodes.Status403Forbidden; await context.Response.WriteAsync("Tenant ID verification failed"); return; } } var path = context.Request.Path.Value ?? string.Empty; var serviceName = ExtractServiceName(path); if (string.IsNullOrEmpty(serviceName)) { await _next(context); return; } var route = _routeCache.GetRoute(headerTenantId, serviceName); if (route == null) { _logger.LogDebug("Route not found - Tenant: {Tenant}, Service: {Service}", headerTenantId, serviceName); await _next(context); return; } context.Items["DynamicClusterId"] = route.ClusterId; var routeType = route.IsGlobal ? "global" : "tenant-specific"; _logger.LogDebug("Tenant routing - Tenant: {Tenant}, Service: {Service}, Cluster: {Cluster}, Type: {Type}", headerTenantId, serviceName, route.ClusterId, routeType); await _next(context); } private string ExtractServiceName(string path) { var match = Regex.Match(path, @"/api/(\w+)/?"); return match.Success ? match.Groups[1].Value : string.Empty; } }