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格式令牌的普及才变得常见。客户端通过audience或resource参数向授权服务器请求特定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请求单独的令牌,避免产生一个能访问多项服务的“超级令牌”,以此将潜在的安全风险降至最低。
🚫 常见配置误区
在实际配置中,一个常见的错误是混淆aud和client_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;
}
}
浙公网安备 33010602011771号