PAYATHON 2026

OIDC 客户端应用应如何判断用户是否有权访问 UI 组件?

技术老齐

建议由应用后端提供“当前用户可执行哪些操作”的接口,客户端根据返回结果显示菜单、按钮和页面入口。这样,客户端无需解析 access token 就能控制 UI;每个受保护的 API 仍须独立检查权限。

如果 IdP、客户端和 API 都由你们控制,技术上可以直接读取 access token,但客户端会因此依赖你们自行约定的 token 结构。采用 RFC 9068 的 JWT access token profile 时,规范明确禁止客户端检查 access token 内容。即使全部系统都由自己控制,也仍须遵守这一要求。

为什么 UI 需要单独获取权限

OIDC 负责认证用户,应用还要决定用户可以执行哪些业务操作。OIDC 没有规定通用的菜单、按钮或页面权限模型。

access token 主要供资源服务器使用,也就是接收它的 API。OAuth 2.0 不要求 access token 使用特定格式:它可以是 JWT,也可以是需要服务器查询才能解释的 opaque token。客户端将它作为凭据传递即可。RFC 6749 §1.4

UI 需要的是可用于判断操作权限的信息,例如:

{
  "permissions": [
    "dashboard:read",
    "users:read",
    "users:create"
  ]
}

应用可以自行定义这种响应,不必与 token 的内部结构一致。

前后端的权限判断各有用途:

  • 前端判断“是否显示创建用户按钮”,减少无效操作。
  • 后端判断“是否允许创建用户”,保护业务数据。

用户可以修改浏览器中的代码,也可以绕过页面直接发送 HTTP 请求。因此,隐藏按钮或增加前端路由守卫,都不能代替服务端授权。

推荐做法:提供当前用户权限接口

可以定义 GET /api/me/permissions 这样的应用接口。它是自定义接口,并非 OIDC 标准端点。

请求和使用权限的流程如下:

  1. 客户端完成 OIDC 登录,取得用于目标 API 的 access token。
  2. 客户端携带该 token 请求权限接口。
  3. 后端验证 token,结合用户角色、当前租户和业务规则计算有效权限。
  4. 客户端根据返回的权限渲染 UI。
  5. 用户执行操作时,对应 API 再次检查该次操作的权限。

权限接口和业务 API 应共用授权策略,避免两边分别维护规则。例如,是否允许 users:create 应由同一套策略决定,前端只负责根据结果显示或隐藏按钮。

返回具体操作权限,比只返回 admin 更容易支持后续需求。例如,客服可以查看用户,但不能创建用户。用操作权限表达这种区别,前端就无须到处判断角色组合。

客户端代码示例

示例假设浏览器直接调用同源 API,且应用已经通过 OIDC 客户端库取得 access token。/api/me/permissions 和权限名称均为示例。

HTML 中先隐藏受保护的入口:

<a id="users-menu" href="/users" hidden>用户管理</a>
<button id="create-user" hidden>创建用户</button>
<p id="permission-error" role="alert"></p>

客户端获取权限后再显示:

async function applyUiPermissions(accessToken) {
  const usersMenu = document.querySelector("#users-menu");
  const createButton = document.querySelector("#create-user");
  const errorMessage = document.querySelector("#permission-error");

  usersMenu.hidden = true;
  createButton.hidden = true;
  errorMessage.textContent = "";

  try {
    const response = await fetch("/api/me/permissions", {
      headers: {
        Authorization: `Bearer ${accessToken}`
      },
      cache: "no-store"
    });

    if (!response.ok) {
      throw new Error(`Permission request failed: ${response.status}`);
    }

    const data = await response.json();

    if (
      !Array.isArray(data.permissions) ||
      !data.permissions.every(value => typeof value === "string")
    ) {
      throw new Error("Invalid permissions response");
    }

    const permissions = new Set(data.permissions);

    usersMenu.hidden = !permissions.has("users:read");
    createButton.hidden = !permissions.has("users:create");
  } catch {
    errorMessage.textContent = "无法加载权限,请稍后重试。";
  }
}

前端路由也可以使用这份权限结果,在用户直接访问 /users 时显示无权限提示。页面中的敏感数据仍应通过受保护的 API 获取,避免先把数据下发到浏览器,再靠隐藏组件限制访问。

服务端授权逻辑可用以下伪代码表示:

principal = authenticateAccessToken(request)
resource = loadTargetResource(request)

if not policy.can(principal, "users:update", resource):
    return 403

return updateUser(resource, request.body)

authenticateAccessToken 和 policy.can 是占位函数,分别负责验证访问凭据,以及判断当前用户能否对目标资源执行操作。服务端不能把客户端传回的权限列表当作授权依据。

能否使用 ID token 或 UserInfo

可以通过 ID token 或 UserInfo 提供约定好的角色或权限声明。这些声明属于你们自行定义的扩展。

OIDC 允许 ID token 包含额外声明,UserInfo 响应也可以包含自定义声明。如果 IdP 本身掌握应用角色,就可以通过这些渠道向客户端提供信息。OIDC Core 并未为所有应用定义统一的业务权限字段或语义。OIDC Core §5.1.2、§5.3.2

具体选择取决于信息的来源和使用时机:

  • 登录时就需要、变化较少的应用角色,可以放在 ID token 的自定义声明中。
  • 由 IdP 管理、需要在登录后读取的用户声明,可以通过 UserInfo 获取。
  • 依赖业务数据库、租户或具体资源状态的权限,通常更适合由应用后端计算。

使用 ID token 前,应通过 OIDC 客户端库完成当前流程要求的验证,不能只解码 JWT。使用 UserInfo 时,必须确认响应中的 sub 与 ID token 中的 sub 完全一致。OIDC Core §3.1.3.7、§5.3.2

ID token 用于向客户端传递认证结果,不应替代 access token 调用业务 API。无论通过哪个渠道取得角色,都需要处理权限变更后的刷新问题。

全部系统可控,能否直接解析 access token

你们可以约定 JWT 中始终包含某个字段,让前端据此显示 UI。不过,这种实现是否符合规范,取决于所采用的规范要求。

RFC 9068 §5 使用 MUST NOT,明确禁止客户端检查 access token 内容。它考虑的风险包括授权服务器和资源服务器改变 token 格式,例如改用 opaque token,导致客户端功能失效。

如果仍选择这种私有设计,就要承担字段语义、格式兼容和权限刷新方面的维护成本。“JWT 可以解码”本身不能保证这些约定长期稳定,解码结果也不能替代服务端授权。

整个系统都由你们控制时,通常更容易提供明确的权限接口。API 可以在服务端读取经过验证的 token 声明,再将适合 UI 使用的权限结果返回给客户端。

权限更新与资源级判断

权限接口返回的是某个时刻的结果。用户被移出管理员角色后,已经打开的页面不会自动获知变化。应用应明确重新获取权限的时机,例如切换租户、切换账号或收到权限变更通知时。退出登录时还应清除缓存,避免下一位用户沿用旧结果。

即使 UI 尚未刷新,后端也必须按系统规定的权限生效机制拒绝越权操作。如果后端完全依赖 token 中的角色快照,撤权可能要等到旧 token 失效,或通过额外的撤销机制才能生效。

对于“只能编辑自己创建的项目”这样的规则,仅凭全局 projects:edit 无法作出判断。项目接口可以返回该项目的 allowedActions,供 UI 显示对应操作;更新项目的 API 仍须检查所有权及其他业务条件。

OAuth scope 表示授予客户端的访问范围,未必包含用户的全部业务权限。除非应用已经明确定义映射关系,否则不能仅凭存在某个 scope 就显示所有对应的管理功能。

备注:内容仅供参考。