PAYATHON 2026

backend client 如何验证其他 Keycloak realm 的 access token?

技术老齐

结论

可以。backend 可以配置多个受信任的 Keycloak realm,再根据 access token 的 iss claim 选择相应的验证配置。这是常见的 multi-issuer resource server 模式。

backend 需要预先维护 issuer 白名单,并完整验证每个 token,不能因为签名有效或 token 中带有某个 iss 就予以信任。验证内容包括:

  • 签名和允许使用的签名算法
  • iss
  • aud
  • exp、nbf
  • 所需的 scope、role 或 permission

Keycloak realm 通常各自构成独立的安全边界。是否允许跨 realm 接受 token,要看这些 realm 是否属于同一个信任域,以及各 backend 是否接受另一个 realm 授予的权限。

如果统一系统需要长期访问多个 backend,通常会使用统一 realm、identity brokering,或让聚合 backend 通过 token exchange 获取面向目标 API 的 token。让浏览器直接持有宽权限 token 并访问所有 backend,会增加 audience、权限和审计的管理难度。

多 issuer 验证怎么实现

JWT 的 payload 可以在验签前解析。middleware 可以先读取 iss,再选择相应的 JWKS 和验证规则。此时取得的 iss 仍然不可信,只能用于查询本地白名单,不能用来拼接 URL 或从网络动态加载密钥。

例如,backend 可以维护以下配置:

https://auth.example.com/realms/stack-a
  audience: api-a
  jwks: https://auth.example.com/realms/stack-a/protocol/openid-connect/certs

https://auth.example.com/realms/conglomerate
  audience: api-a
  jwks: https://auth.example.com/realms/conglomerate/protocol/openid-connect/certs

收到 token 后,可以按以下流程处理:

  1. 在 claims 尚未受信任时解析 JWT,读取 iss。
  2. 检查 iss 是否与白名单中的某个值完全一致。
  3. 使用该 issuer 预先配置的 JWKS 验证签名。
  4. 验证 iss、aud、有效期和允许使用的算法。
  5. 按照目标 API 的授权规则检查 scope、role 或 permission。
  6. 使用 (iss, sub) 标识用户身份,不能只使用 sub。

不同 realm 可能签发相同的 sub,所以数据库中的外部用户标识应包含 issuer。

Node.js 示例

下面的示例使用 jose 验证两个 realm 签发的 JWT。具体的包版本和模块加载方式需要根据项目环境调整。

import {
  createRemoteJWKSet,
  decodeJwt,
  jwtVerify,
} from 'jose';

const trustedIssuers = new Map([
  [
    'https://auth.example.com/realms/stack-a',
    {
      audience: 'api-a',
      jwks: createRemoteJWKSet(
        new URL(
          'https://auth.example.com/realms/stack-a/protocol/openid-connect/certs'
        )
      ),
    },
  ],
  [
    'https://auth.example.com/realms/conglomerate',
    {
      audience: 'api-a',
      jwks: createRemoteJWKSet(
        new URL(
          'https://auth.example.com/realms/conglomerate/protocol/openid-connect/certs'
        )
      ),
    },
  ],
]);

export async function authenticate(request, response, next) {
  try {
    const authorization = request.headers.authorization;

    if (!authorization?.startsWith('Bearer ')) {
      return response.status(401).json({ error: 'missing_access_token' });
    }

    const token = authorization.slice('Bearer '.length);

    // 这里只为选择本地配置。此时 claims 尚未通过验证。
    const unverifiedClaims = decodeJwt(token);
    const config = trustedIssuers.get(unverifiedClaims.iss);

    if (!config) {
      return response.status(401).json({ error: 'untrusted_issuer' });
    }

    const { payload } = await jwtVerify(token, config.jwks, {
      issuer: unverifiedClaims.iss,
      audience: config.audience,
      algorithms: ['RS256'],
    });

    const scopes = new Set(
      typeof payload.scope === 'string'
        ? payload.scope.split(/\s+/)
        : []
    );

    if (!scopes.has('data:read')) {
      return response.status(403).json({ error: 'insufficient_scope' });
    }

    request.user = {
      id: `${payload.iss}|${payload.sub}`,
      issuer: payload.iss,
      subject: payload.sub,
      claims: payload,
    };

    next();
  } catch {
    return response.status(401).json({ error: 'invalid_access_token' });
  }
}

algorithms 必须与实际 realm 使用的签名算法一致,不能直接照搬示例。JWKS 客户端通常会缓存公钥,遇到未知 kid 时重新获取密钥,以支持 Keycloak 的签名密钥轮换。

如果 access token 是需要 introspection 的 opaque token,应根据 issuer 调用相应 realm 的 introspection endpoint。backend 还要为每个 realm 配置对应的 confidential client 凭据。

aud 验证不能省略

即使 token 由受信任的 realm 签发,也不意味着所有 backend 都可以接受它。

目标 backend 应包含在 token 的 aud 中。例如,api-a 不应接受只面向 conglomerate-frontend 或 api-b 的 token,否则为其他组件签发的 token 可能被错误用于当前 API。

在 Keycloak 中,可以通过 client scope、audience mapper 或相应的授权配置,使签发给聚合系统的 token 包含目标 API 的 audience。具体配置取决于 Keycloak 版本和现有的 client 配置。

azp 可以帮助判断 token 是由哪个 client 获取的,但无法代替 aud。token 验证成功只说明 authentication 已完成,读写权限仍需通过 authorization 单独判断。

跨 realm 的授权如何处理

即使其他 realm 中没有该用户,backend 仍然可以验证其 token。backend 可以直接使用 (iss, sub) 作为外部身份,无须先在自己的 realm 中创建同名用户。

需要单独设计的是权限含义。两个 realm 中名称相同的 role 未必表示同一种权限。例如,聚合 realm 中的 admin 不能自动视为业务 realm 中的 admin。

常见的处理方式包括:

  • backend 按 issuer 分别配置 scope 或 role 映射。
  • 使用 API 自己定义的 scope,例如 orders:read、orders:write。
  • 由集中式授权服务或 API gateway 统一执行策略。
  • 用户首次访问时,在业务系统中建立 (iss, sub) 与本地用户、租户或权限记录的关联。

授权规则应明确配置。用户名、邮箱和显示名称可能发生变化,也未必唯一,不适合用于跨 realm 身份匹配。

更适合长期维护的架构

使用同一个 realm

如果这些系统属于同一组织,并且共享用户生命周期和安全策略,可以将它们配置为同一 realm 下的不同 clients。每个 backend 都是独立的 resource server,通过 audience 和 scopes 隔离权限。

这种结构的配置和运维成本较低,前提是业务隔离要求允许各系统共享 realm。

Identity brokering 或用户联邦

如果必须保留多个 realm,可以让业务 realm 信任统一身份源。用户仍然从中心入口登录,在目标 realm 中获得可识别的 federated identity,随后由目标 realm 签发 token。

当目标系统要求用户存在于本 realm 中,或现有授权模型高度依赖本地 realm roles 时,这种方式更合适。

Token exchange

聚合 backend 可以接收用户 token,再通过 OAuth 2.0 Token Exchange 换取面向目标 API 的下游 token。换取的 token 可以使用更窄的 audience 和权限范围。

典型调用链如下:

Browser
  -> Conglomerate Backend
  -> Token Exchange
  -> Target API

这种结构不会把多个 backend 的访问能力全部交给浏览器,也便于收窄权限、记录审计信息和保护凭据。Keycloak 对 token exchange 的支持方式、启用条件和配置能力可能随版本变化,实施前应根据实际部署版本核对文档。

服务身份

如果下游操作无须保留终端用户身份,聚合 backend 可以使用 Client Credentials flow,以自身的服务身份调用目标 API。

这种方式适合系统级任务。如果 API 需要记录“哪个用户执行了操作”,则应传递受保护的用户上下文,或使用 delegation/token exchange。不要把任意用户信息放入普通请求头。

需要避免的做法

  • 接受 token 中的任意 iss。
  • 根据未经验证的 iss 动态访问任意 discovery 或 JWKS URL。
  • 只验证 JWT 签名,省略 iss 和 aud 验证。
  • 因为两个 realm 位于同一台 Keycloak 服务器,就默认它们彼此可信。
  • 将不同 realm 中的同名 role 视为相同权限。
  • 使用用户名或邮箱作为跨 realm 的唯一身份。
  • 向聚合 frontend 签发可调用所有 API、长期有效的高权限 token。

按 issuer 选择验证器是可行的。对于数量有限且信任关系明确的 realm,这种方案直接、合理。如果系统还会继续扩展,可以采用聚合 backend,并配合 audience 限制和 token exchange。相比让浏览器携带跨 realm token 直接调用所有 backend,这种结构更容易管理。

备注:内容仅供参考。