在使用 客户端凭证(Client Credentials)模式 获取 Access Token 后,我们通常会通过 scope + Policy 的方式来保护 API 资源。

但在实际开发中,我遇到了一个看似“反直觉”的问题:

明明申请了更多的 scope,API 却返回了 403。

本文将完整记录问题现象、分析过程以及最终结论,希望能帮你少踩一个坑。


一、背景说明

整体场景如下:

  • 授权服务器使用 Client Credentials 模式签发 Token
  • API 使用 AuthorizationPolicy + RequireClaim(“scope”) 进行访问控制
  • 不同 scope 表示不同 API 权限

二、API 授权策略配置

API 端的授权策略配置如下:

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("UserRead", policy =>
    {
        policy.RequireAuthenticatedUser();
        policy.RequireClaim("scope", "ahbapi.user.read");
    });
});

三、受保护的 API 接口

[HttpGet]
[Authorize(Policy = "UserRead")]
public async Task<IActionResult> Index()
{
    var user = HttpContext.User;
    return Ok(user.Name);
}

四、请求现象复现

1️⃣ 只申请一个 scope —— 正常

当客户端仅申请 ahbapi.user.read 时,请求正常,API 返回 200

client_id: xxx
client_secret: xxxx
grant_type: client_credentials
scope: ahbapi.user.read

2️⃣ 申请多个 scope —— 403

当客户端同时申请多个 scope 时,请求却返回 403

client_id: xxx
client_secret: xxxx
grant_type: client_credentials
scope: ahbapi.user.read ahbapi.user.delete

五、疑问来了 🤔

按常理来说:

scope 越多,权限应该越大才对。

那为什么反而会被拒绝访问呢?


六、问题定位思路

通过调试和断点跟踪,很快将怀疑点锁定在这一行:

policy.RequireClaim("scope", "ahbapi.user.read");

问题的关键在于:
RequireClaim 到底是如何验证 claim 的?

找到源码关键位置:
在这里插入图片描述


七、RequireClaim 的内部执行逻辑

查阅官方文档与源码后,可以总结出 RequireClaim 的执行流程:

  1. AuthorizationPolicyBuilder.RequireClaim(...)
  2. 创建 ClaimsAuthorizationRequirement
  3. 将 Requirement 加入 AuthorizationPolicy.Requirements
  4. 请求进入时,由 AuthorizationHandler 触发校验
  5. 校验 ClaimsPrincipal 中是否存在指定 Claim / Value

八、真正的坑点:Scope Claim 的结构

这里有一个非常容易忽略的问题:

多个 scope 是多个 Claim,还是一个 Claim?

实际运行时打印 HttpContext.User.Claims,可以看到:

type: scope
value: ahbapi.user.read ahbapi.user.delete

在这里插入图片描述

也就是说:

  • 所有 scope 被合并成了一个 scope Claim
  • claim 的 value 是一个用空格分隔的字符串

九、为什么 RequireClaim 会失败?

策略中配置的是:

policy.RequireClaim("scope", "ahbapi.user.read");

而实际 Claim 的值是:

"ahbapi.user.read ahbapi.user.delete"

RequireClaim 的校验逻辑是:

Claim.Value 必须完全匹配 allowedValues 之一

因此:

  • "ahbapi.user.read ahbapi.user.delete"
  • ❌ 并不等于 "ahbapi.user.read"

最终导致策略校验失败,API 返回 403


十、结论总结 ✅

🔹 scope 并不会拆分成多个 Claim
🔹 多个 scope 会以空格拼接的方式存储在一个 scope Claim 中
🔹 RequireClaim 使用的是精确匹配,而非包含匹配

所以当你:

scope: a b c

而策略写成:

RequireClaim("scope", "a");

就一定会踩坑。


十一、相关源码

  • AuthorizationPolicyBuilder
    https://github.com/dotnet/aspnetcore/blob/main/src/Security/Authorization/Core/src/AuthorizationPolicyBuilder.cs

  • ClaimsAuthorizationRequirement
    https://github.com/dotnet/aspnetcore/blob/main/src/Security/Authorization/Core/src/ClaimsAuthorizationRequirement.cs


Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐