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,可在运行时根据请求域名获取对应租户密钥:
- 先注册
IHttpContextAccessor:
services.AddHttpContextAccessor();
- 修改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中获取请求上下文,同时实现域名与子域名的匹配逻辑:
- 同样先注册
IHttpContextAccessor:
services.AddHttpContextAccessor();
- 修改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)); }, // 按需配置其他验证参数 }; });
- 实现域名与子域名匹配方法:
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
相关产品推荐
相关产品推荐

