keycloak~aud受众字段的作用及如何生成

在OAuth 2.0框架中,aud(受众)声明的核心功能是明确指定令牌的合法接收者,它是一个关键的安全验证机制

⚙️ 核心原则:标识与验证

具体来说,这个机制遵循以下原则:

  • 标识:授权服务器在签发令牌时,会将目标API的唯一标识写入aud字段,明确告知客户端“这个令牌是发给谁用的”。
  • 验证:API在收到令牌后,必须验证aud字段的值是否与自身的标识(如https://api.my-api.com或客户端ID)相匹配。如果不匹配,则必须拒绝该令牌。由于aud可能为字符串数组,此时只需确保API自身的标识至少包含在其中一项即可。

虽然aud验证很关键,但它不能作为唯一的授权检查,还应结合角色(roles)、权限范围(scopes)等其他声明进行多层授权校验。

📌 在不同场景下的使用细节

aud字段的具体使用在不同场景下有所不同:

  • 访问令牌 (Access Token)aud标识该令牌可访问的目标API。在OAuth 2.0核心规范中,令牌通常是“不透明白字符串”,aud字段是随着JWT格式令牌的普及才变得常见。客户端通过audienceresource参数向授权服务器请求特定API的令牌。
  • ID令牌 (ID Token):在基于OAuth 2.0的OpenID Connect中,aud声明是必需(REQUIRED) 字段。它必须包含客户端的client_id,用于令牌接收方(客户端)确认令牌是“颁发给自己的”,防止被其他客户端滥用。
  • 客户端断言 (Client Assertion):用于客户端向授权服务器证明自己身份。此场景下的JWT中的aud(受众)字段,必须设为授权服务器的令牌端点URL(如https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token)。例如,在客户端凭证模式中使用private_key_jwt认证方式时,JWT断言的aud值就必须指向授权服务器的令牌端点。
  • 令牌内省 (Token Introspection):授权服务器的内省端点会返回令牌的元数据,其中包含aud字段。资源服务器可以利用此信息对令牌进行更严格的验证,例如确认aud是否包含自身的资源标识符。
  • 资源指示器 (RFC 8707):通过引入resource参数(取代非标准的audience参数),允许客户端在一个授权请求中为多个API请求单独的令牌,避免产生一个能访问多项服务的“超级令牌”,以此将潜在的安全风险降至最低。

🚫 常见配置误区

在实际配置中,一个常见的错误是混淆audclient_id。例如,将访问令牌的aud(应指API)错误地设置成了客户端的client_id,这将导致API因无法匹配自身标识而拒绝令牌。

keycloak中的aud如何生成

  • 位于:org.keycloak.protocol.oidc.mappers.AudienceResolveProtocolMapper
  • 作用:从当前用户的客户端角色中,将客户端client_id提取出来,放到aud字段里,它是一个数组类型

原码

/**
 * Protocol mapper, which adds all client_ids of "allowed" clients to the audience field of the token. Allowed client means the client
 * for which user has at least one client role
 *
 * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
 */
public class AudienceResolveProtocolMapper extends AbstractOIDCProtocolMapper implements OIDCAccessTokenMapper {

    private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();


    public static final String PROVIDER_ID = "oidc-audience-resolve-mapper";


    public List<ProviderConfigProperty> getConfigProperties() {
        return configProperties;
    }

    @Override
    public String getId() {
        return PROVIDER_ID;
    }

    @Override
    public String getDisplayType() {
        return "Audience Resolve";
    }

    @Override
    public String getDisplayCategory() {
        return TOKEN_MAPPER_CATEGORY;
    }

    @Override
    public String getHelpText() {
        return "Adds all client_ids of \"allowed\" clients to the audience field of the token. Allowed client means the client\n" +
                " for which user has at least one client role";
    }

    @Override
    public int getPriority() {
        return ProtocolMapperUtils.PRIORITY_AUDIENCE_RESOLVE_MAPPER;
    }

    @Override
    public AccessToken transformAccessToken(AccessToken token, ProtocolMapperModel mappingModel, KeycloakSession session,
                                            UserSessionModel userSession, ClientSessionContext clientSessionCtx) {
        String clientId = clientSessionCtx.getClientSession().getClient().getClientId();

        for (Map.Entry<String, AccessToken.Access> entry : RoleResolveUtil.getAllResolvedClientRoles(session, clientSessionCtx).entrySet()) {
            // Don't add client itself to the audience
            if (entry.getKey().equals(clientId)) {
                continue;
            }

            AccessToken.Access access = entry.getValue();
            if (access != null && access.getRoles() != null && !access.getRoles().isEmpty()) {
                token.addAudience(entry.getKey());
            }
        }

        return token;
    }

    public static ProtocolMapperModel createClaimMapper(String name) {
        ProtocolMapperModel mapper = new ProtocolMapperModel();
        mapper.setName(name);
        mapper.setProtocolMapper(PROVIDER_ID);
        mapper.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
        mapper.setConfig(Collections.emptyMap());
        return mapper;
    }
}
posted @ 2026-06-04 14:38  张占岭  阅读(0)  评论(0)    收藏  举报