跳到主要内容

Keycloak 集成

本指南端到端地把一个 Keycloak Realm 接入网关:用户以自己的身份登录 Keycloak,携带获得的 JWT 调用网关,并按部门或群组落到对应的调用方 API Key 上运行——无需向每个人分发 Key,也无需重复维护目录。它是 JWT 认证(信任与验证)和 JWT Claim 映射(评估语义)两篇的实操配套。

你将搭建:

  • 一个 Keycloak Realm,其访问令牌携带网关受众(audience)、department claim 和 groups claim。
  • 网关侧的两把策略 Key——受限的 finance Key 和不受限的平台 Key。
  • 两条 Claim 映射:platform-admin 群组的成员使用平台 Key;department = finance 的所有人使用 finance Key;其余请求一律拒绝。

以下步骤在 Keycloak 26(quay.io/keycloak/keycloak:26.3 start-dev)上逐步验证过。其他版本中字段的位置可能略有不同,关键在于配置的取值。

第 1 步:创建 Realm 和 Client

创建一个 Realm(本指南使用 aisix),并在其中为需要请求令牌的应用创建一个 OpenID Connect Client:

Client 设置取值
Client IDai-clients
Client authentication开启(机密客户端——Client 持有密钥)
Direct access grants若想按下文用 curl 密码授权测试,则开启
Standard flow按你的应用登录形态决定

在 Client 的 Credentials 页记下 Client Secret。

第 2 步:塑造访问令牌

默认情况下 Keycloak 的访问令牌既不携带网关的受众,也不携带你的目录属性。给 Client 添加三个 Protocol Mapper(Clients → ai-clients → Client scopes → ai-clients-dedicated → Add mapper → By configuration):

Mapper 类型配置
AudienceIncluded Custom Audienceaisix-gateway,加入访问令牌。令牌的 aud 不包含任何已配置受众时网关会拒绝。
User AttributeUser AttributedepartmentToken Claim Namedepartment,claim 类型 String,加入访问令牌。
Group MembershipToken Claim NamegroupsFull group path:关闭,加入访问令牌。

Keycloak 24 及之后的版本还有一个 Realm 级开关需要注意:department 这类自定义用户属性若未在 Realm 的 User Profile 中声明,会被静默丢弃。要么声明该属性(Realm settings → User profile → Create attribute),要么在同一页面把 Unmanaged attributes 设为 Enabled。否则你在用户上填写的属性根本不会保存,claim 也永远不会出现在令牌里。

第 3 步:创建群组和用户

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

  • alice——属性 department = finance
  • bob——platform-admin 群组成员。
  • charlie——两者都不占,用来验证默认拒绝路径。

给每个用户设置密码和完整的资料(邮箱、名、姓——或在 Realm 的 User Profile 中放宽这些必填要求)。资料不完整或存在待完成必需操作(Required Actions)的用户在密码授权登录时会失败,报 Account is not fully set up

第 4 步:在网关侧信任该 Realm

把 Realm 注册为信任提供方。issuer 必须与令牌的 iss claim 逐字节相同——对名为 aisix 的 Realm 来说就是 <Keycloak 基础 URL>/realms/aisix——JWKS URI 会由此自动发现。

资源文件中:

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

受管部署则在控制台 Trust Providers 中创建同样的提供方,或调用 Admin API——字段完全一致(参见 JWT 认证)。

第 5 步:把部门和群组映射到策略 Key

创建两把策略 Key 和两条映射。群组规则使用更优(更小)的优先级,这样身在 finance 部门的平台管理员仍然落在平台 Key 上:

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

contains 是针对 Keycloak 数组型 claim 的操作符(这里的 groups,或 Realm 角色对应的 realm_access.roles——点号会深入嵌套对象);exact 适合 department 这类单值属性。评估顺序、并列打破规则,以及与 jwt_subject 直接绑定的交互,见 JWT Claim 映射

第 6 步:验证

为每个用户获取令牌,并作为 Bearer 调用网关:

TOKEN=$(curl -s "https://sso.example.com/realms/aisix/protocol/openid-connect/token" \
-d grant_type=password -d client_id=ai-clients -d client_secret="$CLIENT_SECRET" \
-d username=alice -d password="$ALICE_PASSWORD" | jq -r .access_token)

curl "https://gateway.example.com/v1/chat/completions" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"model": "finance-model", "messages": [{"role": "user", "content": "hello"}]}'

三个用户正好钉死整个契约:

调用方请求结果
alice(department = finance)finance-model成功——finance-dept 映射选中了 finance Key。
alice其他任意模型403——她的请求受 finance Key 的 allowed_models 约束。
bob(platform-admin 群组)任意模型成功——群组规则以优先级 50 胜出。
charlie(无匹配)任意模型401,错误码 jwt_identity_unmapped——没有规则命中,也不存在默认放行。

每个请求的用量都归因到个人:用量事件和日志携带 jwt_subject(默认取 Keycloak 的 sub)、提供方名称和命中的映射——见用量归因

排障

现象原因与处理
令牌端点返回 Account is not fully set up用户存在待完成的必需操作,或资料不满足密码授权的要求。补全邮箱/名/姓(或放宽 Realm User Profile 的必填要求)并清空 Required Actions。
令牌里始终没有 departmentKeycloak 24+ 的 User Profile 会丢弃未声明的属性。声明该属性或开启 Unmanaged attributes(第 2 步),然后重新在用户上填值。
令牌明明是新的却报 401 jwt_invalid令牌的 aud 不包含任何已配置受众(缺 Audience Mapper),或 issuer 与令牌的 iss 不完全一致——协议、主机、路径都要相同。
401 jwt_identity_unmapped验证通过但没有映射命中:claim 不在令牌里、操作符与 claim 类型不符(对数组用 exact、对字符串用 contains),或没有规则覆盖该身份。解码令牌(例如在 jwt.io)并与 match 条件逐一比对。