TenantBusinessContextHelper.cs 4.68 KB
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using Microsoft.AspNetCore.Http;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Security.Claims;
using Yi.Framework.Rbac.Domain.Shared.Consts;
using Yi.Framework.SqlSugarCore.Abstractions;

namespace FoodLabeling.Application.Helpers;

/// <summary>
/// SaaS 业务租户上下文判定(平台主库 / Default 租户不走公司菜单子集过滤)。
/// </summary>
public static class TenantBusinessContextHelper
{
    private static readonly Guid ProtectedDefaultTenantId = Guid.Parse("11111111-1111-1111-1111-111111111111");

    /// <summary>
    /// 解析当前请求的业务租户 Id:优先 <see cref="ICurrentTenant"/>,其次 JWT / Authorization Claim。
    /// </summary>
    public static Guid? ResolveBusinessTenantId(ICurrentTenant? currentTenant, HttpContext? httpContext)
    {
        if (currentTenant?.Id is { } fromContext
            && fromContext != Guid.Empty
            && fromContext != ProtectedDefaultTenantId)
        {
            return fromContext;
        }

        var fromJwt = TryGetTenantIdFromHttpContext(httpContext);
        if (fromJwt.HasValue
            && fromJwt.Value != Guid.Empty
            && fromJwt.Value != ProtectedDefaultTenantId)
        {
            return fromJwt;
        }

        return null;
    }

    /// <summary>
    /// 当前请求是否处于真实公司业务租户上下文(须按 fl_th_tenant_menu_permission 限定可分配菜单)。
    /// </summary>
    public static bool ShouldScopeMenusByCompany(
        DbConnOptions options,
        ICurrentTenant? currentTenant,
        HttpContext? httpContext)
    {
        return ShouldScopeMenusByCompany(options, ResolveBusinessTenantId(currentTenant, httpContext));
    }

    /// <summary>
    /// 当前请求是否处于真实公司业务租户上下文(须按 fl_th_tenant_menu_permission 限定可分配菜单)。
    /// </summary>
    public static bool ShouldScopeMenusByCompany(DbConnOptions options, Guid? tenantId)
    {
        return options.EnabledSaasMultiTenancy
               && tenantId.HasValue
               && tenantId.Value != Guid.Empty
               && tenantId.Value != ProtectedDefaultTenantId;
    }

    /// <summary>
    /// 从 HttpContext Principal 或 Authorization Bearer JWT 读取 TenantId Claim 原始值。
    /// </summary>
    public static string? TryGetTenantIdClaimValue(HttpContext? httpContext)
    {
        if (httpContext is null)
        {
            return null;
        }

        return TryGetTenantIdFromPrincipal(httpContext.User)
               ?? TryGetTenantIdFromAuthorizationHeader(httpContext.Request.Headers.Authorization.ToString());
    }

    private static Guid? TryGetTenantIdFromHttpContext(HttpContext? httpContext)
    {
        var tenantClaim = TryGetTenantIdClaimValue(httpContext);
        if (string.IsNullOrWhiteSpace(tenantClaim)
            || !Guid.TryParse(tenantClaim, out var tenantGuid)
            || tenantGuid == Guid.Empty)
        {
            return null;
        }

        return tenantGuid;
    }

    private static string? TryGetTenantIdFromPrincipal(ClaimsPrincipal? user)
    {
        if (user?.Identity?.IsAuthenticated != true)
        {
            return null;
        }

        return user.FindFirst(TokenTypeConst.TenantId)?.Value
               ?? user.FindFirst(AbpClaimTypes.TenantId)?.Value
               ?? user.Claims.FirstOrDefault(c =>
                   c.Type.EndsWith("tenantId", StringComparison.OrdinalIgnoreCase)
                   || c.Type.Equals("tenantid", StringComparison.OrdinalIgnoreCase))?.Value;
    }

    /// <summary>
    /// 多租户中间件可能早于 JWT Principal 就绪;直接从 Authorization 解析 TenantId Claim。
    /// </summary>
    private static string? TryGetTenantIdFromAuthorizationHeader(string? authorization)
    {
        if (string.IsNullOrWhiteSpace(authorization))
        {
            return null;
        }

        const string bearerPrefix = "Bearer ";
        if (!authorization.StartsWith(bearerPrefix, StringComparison.OrdinalIgnoreCase))
        {
            return null;
        }

        var jwt = authorization[bearerPrefix.Length..].Trim();
        if (string.IsNullOrWhiteSpace(jwt))
        {
            return null;
        }

        try
        {
            var token = new JwtSecurityTokenHandler().ReadJwtToken(jwt);
            return token.Claims.FirstOrDefault(c =>
                       c.Type == TokenTypeConst.TenantId
                       || c.Type == AbpClaimTypes.TenantId
                       || c.Type.EndsWith("tenantId", StringComparison.OrdinalIgnoreCase))
                   ?.Value;
        }
        catch (Exception)
        {
            return null;
        }
    }
}