using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Results; using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Module.Tenancy; /// /// 多租户解析中间件:支持 Header、域名与 Token Claim 的优先级解析。 /// /// /// 初始化中间件。 /// public sealed class TenantResolutionMiddleware( RequestDelegate next, ILogger logger, ITenantContextAccessor tenantContextAccessor, IOptionsMonitor optionsMonitor) { /// /// 解析租户并将上下文注入请求。 /// public async Task InvokeAsync(HttpContext context) { var options = optionsMonitor.CurrentValue ?? new TenantResolutionOptions(); if (ShouldSkip(context.Request.Path, options)) { await next(context); return; } var tenantContext = ResolveTenant(context, options); tenantContextAccessor.Current = tenantContext; context.Items[TenantConstants.HttpContextItemKey] = tenantContext; if (!tenantContext.IsResolved) { logger.LogDebug("未能解析租户:{Path}", context.Request.Path); if (options.ThrowIfUnresolved) { var response = ApiResponse.Error(ErrorCodes.BadRequest, "缺少租户标识"); context.Response.StatusCode = StatusCodes.Status400BadRequest; await context.Response.WriteAsJsonAsync(response, cancellationToken: context.RequestAborted); tenantContextAccessor.Current = null; context.Items.Remove(TenantConstants.HttpContextItemKey); return; } } try { await next(context); } finally { tenantContextAccessor.Current = null; context.Items.Remove(TenantConstants.HttpContextItemKey); } } private static bool ShouldSkip(PathString path, TenantResolutionOptions options) { if (!path.HasValue) { return false; } var value = path.Value ?? string.Empty; if (options.IgnoredPaths.Contains(value)) { return true; } return options.IgnoredPaths.Any(ignore => { if (string.IsNullOrWhiteSpace(ignore)) { return false; } var ignorePath = new PathString(ignore); return path.StartsWithSegments(ignorePath); }); } private static TenantContext ResolveTenant(HttpContext context, TenantResolutionOptions options) { var request = context.Request; // 1. Header 中的租户 ID if (!string.IsNullOrWhiteSpace(options.TenantIdHeaderName) && request.Headers.TryGetValue(options.TenantIdHeaderName, out var tenantHeader) && long.TryParse(tenantHeader.FirstOrDefault(), out var headerTenantId)) { return new TenantContext(headerTenantId, null, $"header:{options.TenantIdHeaderName}"); } // 2. Header 中的租户编码 if (!string.IsNullOrWhiteSpace(options.TenantCodeHeaderName) && request.Headers.TryGetValue(options.TenantCodeHeaderName, out var codeHeader)) { var code = codeHeader.FirstOrDefault(); if (TryResolveByCode(code, options, out var tenantFromCode)) { return new TenantContext(tenantFromCode, code, $"header:{options.TenantCodeHeaderName}"); } } // 3. Host 映射/子域名解析 var host = request.Host.Host; if (!string.IsNullOrWhiteSpace(host)) { if (options.DomainTenantMap.TryGetValue(host, out var tenantFromHost)) { return new TenantContext(tenantFromHost, null, $"host:{host}"); } var codeFromHost = ResolveCodeFromHost(host, options.RootDomain); if (TryResolveByCode(codeFromHost, options, out var tenantFromSubdomain)) { return new TenantContext(tenantFromSubdomain, codeFromHost, $"host:{host}"); } } // 4. Token Claim var claim = context.User?.FindFirst("tenant_id"); if (claim != null && long.TryParse(claim.Value, out var claimTenant)) { return new TenantContext(claimTenant, null, "claim:tenant_id"); } return TenantContext.Empty; } private static bool TryResolveByCode(string? code, TenantResolutionOptions options, out long tenantId) { tenantId = 0; if (string.IsNullOrWhiteSpace(code)) { return false; } return options.CodeTenantMap.TryGetValue(code, out tenantId); } private static string? ResolveCodeFromHost(string host, string? rootDomain) { if (string.IsNullOrWhiteSpace(rootDomain)) { return null; } var normalizedRoot = rootDomain.TrimStart('.'); if (!host.EndsWith(normalizedRoot, StringComparison.OrdinalIgnoreCase)) { return null; } var suffixLength = normalizedRoot.Length; if (host.Length <= suffixLength) { return null; } var withoutRoot = host[..(host.Length - suffixLength)]; if (withoutRoot.EndsWith('.')) { withoutRoot = withoutRoot[..^1]; } var segments = withoutRoot.Split('.', StringSplitOptions.RemoveEmptyEntries); return segments.Length == 0 ? null : segments[0]; } }