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

ASP.NET Core多租户JWT认证租户越权问题及方案困境咨询

多租户ASP.NET Core JWT认证越权问题解决方案

现有JWT认证配置

var key = Encoding.ASCII.GetBytes(jwtConfigs.Secret);

JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
JwtSecurityTokenHandler.DefaultOutboundClaimTypeMap.Clear();

services
    .AddAuthentication(options =>
    {
        options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
        options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    })
    .AddJwtBearer(options =>
    {
        // ...
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = new SymmetricSecurityKey(key),
            // ValidateAudience = ...
            // AudienceValidator = ...
            // ...
        };
    });

问题场景与困境描述

多租户应用中,存在租户管理员利用自身Token修改其他租户数据的越权问题。尝试两种方案均遇阻碍:

  • 按租户分配独立密钥:无法在运行时根据请求域名(租户识别依据)动态指定对应密钥
  • 按租户设置不同Audience:AudienceValidator委托无法直接获取HttpContext或服务提供程序,且需支持租户域名及所有子域名的Token验证

同时,对TokenValidationParameters中ValidateAudience的默认验证逻辑存在疑问。


核心疑问解答:ValidateAudience默认逻辑

当ValidateAudience设为true时,默认验证逻辑会检查Token的aud声明是否完全匹配TokenValidationParameters中配置的ValidAudience(单个受众)或ValidAudiences(受众集合)中的任意一项,只有完全匹配才会通过验证。


方案一:动态切换租户密钥的实现

通过IssuerSigningKeyResolver委托结合IHttpContextAccessor,可在运行时根据请求域名获取对应租户密钥:

  1. 先注册IHttpContextAccessor:
services.AddHttpContextAccessor();
  1. 修改JWT认证配置,替换固定密钥为动态解析逻辑:
var serviceProvider = services.BuildServiceProvider();
var httpContextAccessor = serviceProvider.GetRequiredService<IHttpContextAccessor>();

services
    .AddAuthentication(options =>
    {
        options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
        options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    })
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuerSigningKey = true,
            // 用Resolver动态获取密钥,替代固定IssuerSigningKey
            IssuerSigningKeyResolver = (token, securityToken, kid, validationParameters) =>
            {
                var context = httpContextAccessor.HttpContext;
                if (context == null)
                {
                    throw new UnauthorizedAccessException("无法获取请求上下文");
                }

                // 从请求Host中提取租户域名
                var tenantDomain = context.Request.Host.Host;
                // 自定义逻辑:根据域名从数据库/缓存中获取对应租户密钥
                var tenantSecret = GetTenantSecretByDomain(tenantDomain);
                if (string.IsNullOrWhiteSpace(tenantSecret))
                {
                    throw new SecurityTokenInvalidSignatureException("无效的租户密钥");
                }

                var key = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(tenantSecret));
                return new List<SecurityKey> { key };
            },
            // 按需配置其他验证参数(如Issuer验证等)
        };
    });

注:GetTenantSecretByDomain需自行实现,建议结合缓存提升性能。


方案二:支持域名及子域名的Audience验证

通过IHttpContextAccessor在自定义AudienceValidator中获取请求上下文,同时实现域名与子域名的匹配逻辑:

  1. 同样先注册IHttpContextAccessor:
services.AddHttpContextAccessor();
  1. 修改JWT认证配置,自定义受众验证逻辑:
var serviceProvider = services.BuildServiceProvider();
var httpContextAccessor = serviceProvider.GetRequiredService<IHttpContextAccessor>();

services
    .AddAuthentication(options =>
    {
        options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
        options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    })
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateAudience = true,
            // 自定义Audience验证器
            AudienceValidator = (audiences, securityToken, validationParameters) =>
            {
                var context = httpContextAccessor.HttpContext;
                if (context == null)
                {
                    return false;
                }

                var currentDomain = context.Request.Host.Host;
                // 检查Token中的任意受众是否匹配当前域名或其子域名
                return audiences.Any(aud => IsDomainOrSubdomain(aud, currentDomain));
            },
            // 按需配置其他验证参数
        };
    });
  1. 实现域名与子域名匹配方法:
private bool IsDomainOrSubdomain(string allowedDomain, string currentDomain)
{
    // 提取域名的核心部分(如example.com),匹配当前域名是否为其本身或子域名
    var allowedCoreParts = allowedDomain.Split('.').Reverse().Take(2).Reverse().ToArray();
    var currentCoreParts = currentDomain.Split('.').Reverse().Take(2).Reverse().ToArray();
    
    return allowedCoreParts.SequenceEqual(currentCoreParts);
}

替代方案:利用OnTokenValidated事件补充验证

若希望保留部分默认Audience验证逻辑,可在OnTokenValidated事件中添加子域名检查:

services
    .AddAuthentication(options =>
    {
        options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
        options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    })
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateAudience = true,
            // 可配置所有租户的根域名集合,作为基础验证
            ValidAudiences = new List<string> { "tenant1.com", "tenant2.com" },
            // 其他参数...
        };

        options.Events = new JwtBearerEvents
        {
            OnTokenValidated = async context =>
            {
                var currentDomain = context.HttpContext.Request.Host.Host;
                var jwtToken = context.SecurityToken as JwtSecurityToken;
                if (jwtToken == null)
                {
                    context.Fail("无效的JWT Token");
                    return;
                }

                // 验证当前域名是否为Token受众的子域名或本身
                bool isMatch = jwtToken.Audiences.Any(aud => IsDomainOrSubdomain(aud, currentDomain));
                if (!isMatch)
                {
                    context.Fail("Token受众与当前租户域名不匹配");
                    return;
                }

                await Task.CompletedTask;
            }
        };
    });

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.12 06:07:20