C#/.NET6:基于AccessToken范围构建API控制器授权策略遇阻
问题:Bearer令牌授权验证通过但Scope策略失效,租户声明无法获取
我有一个API控制器,希望使用Bearer令牌对调用服务进行授权,要求API端点验证令牌包含指定scope,但目前功能异常。
令牌负载示例
{ "iss": "https://someauthority.com", "nbf": 1699891816, "iat": 1699891816, "exp": 1699895416, "aud": "https://myapi.com", "scope": [ "myapi:user-read" ], "client_id": "MyApiConsumer", "tenant_id": "fcbebe85-5e17-4986-dffd-ede94e9b6a07", "tenant_external_id": "7123", "tenant_owner_client_id": "SomeTenantOwnerApp", "jti": "ADE83169F38F3EA14B5E99AF998821EF" }
当前实现代码
1. JWT验证配置
builder.Services.AddAuthentication(options => { options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(options => { options.Authority = "https://someauthority.com"; options.Audience = "https://myapi.com"; options.SaveToken = true; // 两种情况都试过 options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = true, ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ClockSkew = TimeSpan.Zero, IssuerSigningKeyResolver = (token, securityToken, kid, parameters) => { var json = new WebClient().DownloadString("https://myapi.com/.well-known/openid-configuration/jwks"); var keys = JsonConvert.DeserializeObject<JwksKeys>(json); return keys?.Keys; } }; options.Events = new JwtBearerEvents { OnAuthenticationFailed = context => { var logger = context.HttpContext.RequestServices.GetRequiredService<ILoggerFactory>().CreateLogger("AuthenticationFailed"); logger.LogError("Token validation failed", context.Exception); return Task.CompletedTask; }, OnTokenValidated = context => { var logger = context.HttpContext.RequestServices.GetRequiredService<ILoggerFactory>().CreateLogger("TokenValidated"); logger.LogInformation("Token validated successfully."); logger.LogInformation("Claims:"); foreach (var claim in context.Principal.Claims) { logger.LogInformation($"{claim.Type}: {claim.Value}"); } return Task.CompletedTask; } }; });
2. 授权策略配置
builder.Services.AddAuthorization(options => { options.AddPolicy("ScopeUserRead", policy => policy.RequireClaim("scope", "myapi:user-read")); options.AddPolicy("ScopeUserCreate", policy => policy.RequireClaim("scope", "myapi:user-create")); options.AddPolicy("ScopeUserWrite", policy => policy.RequireClaim("scope", "myapi:user-read-write")); });
3. 中间件检查代码
app.Use(async (context, next) => { var logger = context.RequestServices.GetRequiredService<ILoggerFactory>().CreateLogger("ClaimsMiddleware"); var user = context.User; if (user.Identity.IsAuthenticated) { logger.LogInformation("Authenticated User Claims:"); foreach (var claim in user.Claims) { logger.LogInformation($"{claim.Type}: {claim.Value}"); } } else { logger.LogInformation("User is not authenticated."); } await next.Invoke(); });
问题现象
- 日志显示令牌验证成功,
OnTokenValidated中能看到scope声明 - 控制器类加
[Authorize],方法加[Authorize("ScopeUserRead")],调用后返回401错误 - 中间件检查发现
HttpContext.User中没有scope和tenant_id等声明
疑问
- 问题出在哪里?是否方案有误?
- 能否手动将令牌声明复制到
HttpContext.User? - 多租户场景下,如何便捷获取
tenant_id声明?是否有类似[FromToken]的注入方式?
解决方案
核心问题分析
问题出在JWT令牌中数组类型的scope声明在转换为Claims时的处理方式,以及自定义声明(如tenant_id)未被正确映射到ClaimsPrincipal。
1. 修复Scope授权失效问题
JWT中的scope是数组,但默认JwtBearer中间件会将其转换为单个Claim,值为空格分隔的字符串(而非数组)。而RequireClaim("scope", "myapi:user-read")是精确匹配Claim的完整值,导致匹配失败。
解决方法一:修改授权策略,自定义Scope验证
builder.Services.AddAuthorization(options => { options.AddPolicy("ScopeUserRead", policy => policy.RequireAssertion(context => context.User.HasClaim(c => c.Type == "scope" && c.Value.Split(' ').Contains("myapi:user-read")))); // 其他Scope策略同理 options.AddPolicy("ScopeUserCreate", policy => policy.RequireAssertion(context => context.User.HasClaim(c => c.Type == "scope" && c.Value.Split(' ').Contains("myapi:user-create")))); });
解决方法二:在Token验证后拆分Scope为多个Claim
在OnTokenValidated事件中手动处理数组类型的scope,拆分为独立Claim:
options.Events = new JwtBearerEvents { OnTokenValidated = context => { var jwtToken = context.SecurityToken as JwtSecurityToken; if (jwtToken != null && jwtToken.Payload.TryGetValue("scope", out var scopeValue)) { var claimsIdentity = context.Principal.Identity as ClaimsIdentity; if (claimsIdentity != null) { // 兼容数组或空格分隔的字符串格式 var scopes = scopeValue switch { string s => s.Split(' ', StringSplitOptions.RemoveEmptyEntries), IEnumerable<string> arr => arr, _ => Array.Empty<string>() }; foreach (var scope in scopes) { claimsIdentity.AddClaim(new Claim("scope", scope)); } } } // 同时处理tenant_id等自定义声明 if (jwtToken?.Payload.TryGetValue("tenant_id", out var tenantId) == true) { var claimsIdentity = context.Principal.Identity as ClaimsIdentity; claimsIdentity?.AddClaim(new Claim("tenant_id", tenantId.ToString())); } return Task.CompletedTask; } };
修改后,授权策略可继续使用RequireClaim:
options.AddPolicy("ScopeUserRead", policy => policy.RequireClaim("scope", "myapi:user-read"));
2. 修复自定义声明(如tenant_id)无法获取的问题
默认JwtBearer中间件只会映射标准Claim类型,自定义声明需要手动添加到ClaimsPrincipal中。上面的OnTokenValidated事件处理代码已经包含了tenant_id的映射逻辑,处理后即可通过HttpContext.User.FindFirst("tenant_id")?.Value获取。
3. 实现类似[FromToken]的参数注入
可以自定义绑定属性实现令牌声明的直接注入:
步骤1:创建FromTokenAttribute
[AttributeUsage(AttributeTargets.Parameter)] public class FromTokenAttribute : Attribute, IBindingSourceMetadata { public string ClaimName { get; } public FromTokenAttribute(string claimName) { ClaimName = claimName; } public BindingSource BindingSource => BindingSource.Custom; }
步骤2:创建自定义模型绑定器
public class TokenClaimModelBinder : IModelBinder { public Task BindModelAsync(ModelBindingContext bindingContext) { var attribute = bindingContext.ParameterInfo.GetCustomAttribute<FromTokenAttribute>(); if (attribute == null) { bindingContext.Result = ModelBindingResult.Failed(); return Task.CompletedTask; } var claim = bindingContext.HttpContext.User.FindFirst(attribute.ClaimName); if (claim == null) { bindingContext.Result = ModelBindingResult.Failed(); return Task.CompletedTask; } // 转换参数类型 try { var value = Convert.ChangeType(claim.Value, bindingContext.ModelType); bindingContext.Result = ModelBindingResult.Success(value); } catch { bindingContext.Result = ModelBindingResult.Failed(); } return Task.CompletedTask; } }
步骤3:注册模型绑定提供器
builder.Services.AddControllers(options => { options.ModelBinderProviders.Insert(0, new TokenClaimModelBinderProvider()); }); public class TokenClaimModelBinderProvider : IModelBinderProvider { public IModelBinder GetBinder(ModelBinderProviderContext context) { if (context.ParameterInfo.GetCustomAttribute<FromTokenAttribute>() != null) { return new TokenClaimModelBinder(); } return null; } }
步骤4:在控制器中使用
[Authorize("ScopeUserRead")] [HttpGet("users")] public IActionResult GetUsers([FromToken("tenant_id")] Guid tenantId) { // 直接使用tenantId return Ok($"Tenant ID: {tenantId}"); }
额外注意事项
- 确保中间件顺序正确:
app.UseAuthentication()必须在app.UseAuthorization()和自定义中间件之前 - 避免重复设置
TokenValidationParameters,options.Authority已自动配置Issuer、Audience和签名密钥解析,无需手动实现IssuerSigningKeyResolver(除非有特殊需求) - 建议使用
HttpClient而非WebClient获取JWKS密钥,WebClient已被标记为过时
内容的提问来源于stack exchange,提问作者Aileron79
相关产品推荐
相关产品推荐

