跳到主要内容

Keycloak 集成

本演练端到端地将 Keycloak Realm 连接到网关:用户以自己的身份登录 Keycloak,使用得到的 JWT 调用网关,并根据所属部门或用户组选定的调用方 API Key 运行,无需向每位用户分发密钥,也无需复制目录数据。本演练是 JWT 身份认证(信任与验证)和 JWT Claim 映射(评估语义)的具体实践。

你将构建:

  • 一个 Keycloak Realm,其访问 Token 包含网关受众、department Claim 和 groups Claim。
  • 网关上的两个策略 Key:受限的 finance Key 和不受限的平台 Key。
  • 两条 Claim 映射:platform-admin 用户组成员使用平台 Key;所有 department = finance 的用户使用财务 Key;其他用户均被拒绝。

以下步骤已使用 Keycloak 26(quay.io/keycloak/keycloak:26.3 start-dev)验证。其他版本中的字段位置可能略有不同,但所需值不变。

前提条件

开始前,请准备:

  • Keycloak 部署的管理员权限。
  • 一个网关代理 URL,以及两个可用的模型别名 finance-modelgeneral-model。第二个模型用于验证映射优先级会选择权限范围更广的管理员 Key。
  • 以下一种网关配置方式:
    • AISIX Cloud:已有环境和附加到该环境的网关,拥有管理信任提供商、调用方 API Key 和 Claim 映射的权限,并取得两个测试模型的模型 ID。
    • 开源 AISIX 网关:已有一份定义了两个测试模型及其提供商 Key 的完整 resources.yaml 文件,并可访问网关进程环境。
  • curljq。开源 AISIX 网关流程还使用 OpenSSL 生成不会分发给 Agent 的占位调用方 Key 值。

创建 Realm 和客户端

创建一个 Realm(本演练使用 aisix),并在其中为需要申请 Token 的应用创建一个 OpenID Connect 客户端:

客户端设置
Client IDai-clients
Client authenticationOn(机密客户端,具有 Secret)
Direct access grants若要按下文使用 curl 密码授权进行测试,请设为 On
Standard flow根据应用的登录方式设置

记下客户端 Credentials 标签页中的客户端 Secret。

向访问 Token 添加 Claim

Keycloak 访问 Token 默认既不包含网关受众,也不包含目录属性。请为客户端添加三个协议映射器(Clients → ai-clients → Client scopes → ai-clients-dedicated → Add mapper → By configuration):

映射器类型配置
AudienceIncluded Custom Audienceaisix-gateway,添加到访问 Token。若 Token 的 aud 不包含配置的受众,网关会拒绝该 Token。
User AttributeUser AttributedepartmentToken Claim Namedepartment;Claim 类型为 String;添加到访问 Token。
Group MembershipToken Claim NamegroupsFull group path:Off;添加到访问 Token。

Keycloak 24 及更高版本还有一个重要的 Realm 级开关:除非 Realm 的用户配置文件识别 department 之类的自定义用户属性,否则这些属性会被静默丢弃。请声明该属性(Realm settings → User profile → Create attribute),或在同一页面将 Unmanaged attributes 设为 Enabled。否则,即使为用户输入了该属性,它也不会持久化,相应 Claim 也不会出现在 Token 中。

创建用户组和用户

创建 platform-admin 用户组,然后创建以下用户:

  • alice:属性 department = finance
  • bob:属性 department = finance,且属于 platform-admin 用户组,用于证明两条映射同时匹配时,用户组映射会胜出。
  • charlie:两者均不具备,用于证明默认拒绝路径。

为每位用户设置密码和完整的配置文件(电子邮件、名字和姓氏;也可以在 Realm 的用户配置文件中放宽要求)。配置文件未满足要求或仍有待执行操作的用户在直接授权登录时会得到 Account is not fully set up

信任 Keycloak Realm

将 Realm 注册为信任提供商。issuer 必须逐字节等于 Token 的 iss Claim。对于名为 aisix 的 Realm,其值为 <keycloak base URL>/realms/aisix;JWKS URI 会通过该值自动发现。

对于开源 AISIX 网关,将提供商加入已经定义 finance-modelgeneral-model 及其提供商 Key 的完整资源文件。如果 JWT 身份认证流程已创建 corp-keycloak,请替换该条目,不要添加同名的第二个提供商,并省略其 bound_claimsrequired_scopes

oidc_providers:
- name: corp-keycloak
issuer: https://sso.example.com/realms/aisix
audiences: ["aisix-gateway"]

在 AISIX Cloud 中,通过 Trust Providers 或 AISIX Cloud Admin API 创建相同的提供商。使用上面所示的名称、Issuer 和 Audiences。除非 Token 包含相应值,否则请省略 bound_claimsrequired_scopes;否则,AISIX 会在执行 Claim 映射前以 jwt_claims_rejected 拒绝请求。其他提供商字段参见 JWT 身份认证

将 Claim 映射到策略 Key

创建两个策略 Key 和两条映射。用户组规则使用更高的优先级(更小的数值),因此财务部门中的平台管理员仍会使用平台 Key。请使用与信任 Realm 相同的配置方式。

AISIX Cloud

在环境中创建两个调用方 API Key

  • finance-policy-key:只能调用 finance-model
  • admin-policy-key:可以调用 finance-modelgeneral-model,以及管理员应能访问的其他模型。

记录两个 Key ID。然后打开环境的 Claim mappings 页面并创建以下映射,或通过 AISIX Cloud Admin API 流程提交等效负载:

映射优先级条件策略 Key
platform-admins50groups 包含 platform-adminadmin-policy-key
finance-dept100department 精确匹配 financefinance-policy-key

开源 AISIX 网关

为两个调用方 Key 资源生成值,并将其设置到网关进程环境中。用户使用 Keycloak Token 进行身份认证,因此不要把这些值分发给用户:

export FINANCE_POLICY_KEY="$(openssl rand -hex 32)"
export ADMIN_POLICY_KEY="$(openssl rand -hex 32)"

将两个策略 Key 添加到 api_keys,并将两个映射添加到 claim_mappings。需要时创建 claim_mappings,并保持其他资源不变:

resources.yaml(API Key 和 Claim 映射)
api_keys:
- display_name: finance-policy-key
key_env: FINANCE_POLICY_KEY
allowed_models: ["finance-model"]
- display_name: admin-policy-key
key_env: ADMIN_POLICY_KEY
allowed_models: ["*"]

claim_mappings:
- name: platform-admins
jwt_provider: corp-keycloak
priority: 50
match:
- claim: groups
op: contains
values: ["platform-admin"]
resolve:
api_key: admin-policy-key
- name: finance-dept
jwt_provider: corp-keycloak
priority: 100
match:
- claim: department
op: exact
values: ["finance"]
resolve:
api_key: finance-policy-key

按照添加新的环境变量验证并应用完整的资源文件。在宿主机上导出变量不会将其加入已经运行的容器,因此请使用这两个变量重新创建网关,不要只发送 SIGHUP

contains 是 Keycloak 数组 Claim(此处为 groups,Realm 角色则为 realm_access.roles,句点用于遍历嵌套对象)的运算符;exact 适用于 department 等单值属性。评估顺序、相同优先级时的规则,以及与直接 jwt_subject 绑定的交互,请参见 JWT Claim 映射

验证 Claim 映射

导出 Keycloak 客户端、用户、网关和模型的值。Keycloak URL 和网关 URL 末尾均不带斜杠:

export KEYCLOAK_URL="https://sso.example.com"
export KEYCLOAK_CLIENT_ID="ai-clients"
export KEYCLOAK_CLIENT_SECRET="YOUR_CLIENT_SECRET"
export KEYCLOAK_USERNAME="alice"
export KEYCLOAK_PASSWORD="ALICE_PASSWORD"
export AISIX_PROXY="https://gateway.example.com"
export AISIX_MODEL="finance-model"

获取用户的 Token,并将其作为 Bearer Token 调用网关。URL 编码能够保留包含表单分隔符字符的客户端 Secret 和密码:

TOKEN=$(curl -sS --fail-with-body \
"$KEYCLOAK_URL/realms/aisix/protocol/openid-connect/token" \
--data-urlencode "grant_type=password" \
--data-urlencode "client_id=$KEYCLOAK_CLIENT_ID" \
--data-urlencode "client_secret=$KEYCLOAK_CLIENT_SECRET" \
--data-urlencode "username=$KEYCLOAK_USERNAME" \
--data-urlencode "password=$KEYCLOAK_PASSWORD" \
| jq -er '.access_token')

curl -sS --fail-with-body "$AISIX_PROXY/v1/chat/completions" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
--data-binary @- <<EOF
{
"model": "${AISIX_MODEL}",
"messages": [
{
"role": "user",
"content": "hello"
}
]
}
EOF

为测试矩阵中的每一行设置 KEYCLOAK_USERNAMEKEYCLOAK_PASSWORDAISIX_MODEL,然后重新发起 Token 请求和网关请求。测试另一个模型需要另一个已经配置的模型别名。

对于预期返回 401403 的测试,--fail-with-body 会使 curl 以非零状态退出,但仍会打印网关错误响应体。

以下三个用户固定了完整的行为约定:

调用方请求结果
alice(department = finance)finance-model成功。finance-dept 映射选择了财务 Key。
alicegeneral-model返回 403。财务 Key 的 allowed_models 控制它允许的每个请求。
bob(财务部门和 platform-admin 用户组)general-model成功。优先级 50 的用户组规则优先于同样匹配但优先级为 100 的财务规则。
charlie(无匹配)finance-model返回 401,错误码为 jwt_identity_unmapped。没有规则匹配,也没有默认放行。

每个请求的用量都会归因到具体用户:用量事件和日志包含 jwt_subject(Keycloak 默认为 sub)、提供商名称和匹配的映射。参见归因

故障排除

现象原因与修复方法
Token 端点返回 Account is not fully set up用户仍有待执行操作,或其配置文件不满足直接授权要求。补全电子邮件、名字和姓氏(也可以放宽 Realm 的用户配置文件要求),并清除待执行操作。
department 始终不出现在 Token 中Keycloak 24 及更高版本的用户配置文件会丢弃未声明的属性。请按照向访问 Token 添加 Claim声明该属性或启用非托管属性,然后重新为用户设置值。
使用新 Token 仍收到带有 jwt_invalid401Token 的 aud 不包含已配置的受众(缺少 Audience 映射器),或 issuer 没有完全等于 Token 的 iss,包括 Scheme、主机和路径。
收到带有 jwt_identity_unmapped401验证成功,但没有映射匹配:Token 缺少相应 Claim、运算符不适合 Claim 类型(对数组使用 exact 或对字符串使用 contains),或没有规则覆盖该身份。请解码 Token(例如使用 jwt.io),并将其 Claim 与 match 条件进行比较。