使用 Keycloak 授权 M2M 请求
Keycloak 可以向与机密 OIDC 客户端关联的服务账号签发访问 Token。后台服务、定时任务、命令行工具和自动化程序可以使用这些 Token 调用 API,而无需最终用户登录。
OAuth 2.0 客户端凭证授权专为这类机器到机器(M2M)通信设计。Apache APISIX 可以在代理请求之前验证每个 Token 的签名、签发者、受众和已授予作用域,从而保护目标 API。
本指南使用一个 Keycloak 客户端表示由 APISIX 保护的 API,另一个服务账号客户端表示调用服务。服务账号会获得一个客户端作用域,并请求受众标识为受保护 API 的访问 Token。APISIX 会验证这两项限制,并在将请求转发到示例上游之前移除身份认证数据。主要配置使用 Keycloak 的 JSON Web Key Set(JWKS)在本地验证 Token;备选配置则使用 Keycloak 的 Token 内省端点。
前置条件
- 安装 Docker。
- 安装 cURL 和 jq。
- 按照快速入门教程,使用 Docker 启动 APISIX。
- 如果计划使用 ADC,请先安装并配置 ADC。
- 如果使用 Admin API 示例且启用了身份验证,请将管理员角色的密钥导出为
ADMIN_API_KEY。请在deployment.admin.admin_key下配置该密钥。Docker 快速入门为了便于本地评估而禁用了身份验证;生产环境中请保持启用。
配置 Keycloak
启动本地 Keycloak 服务器,然后创建 Realm、受保护 API 客户端、客户端作用域和用于 M2M 授权的服务账号客户端。
启动 Keycloak
如果已通过设置 Keycloak 单点登录准备好 Keycloak 和 quickstart-realm,请复用它们并继续创建 API 作用域。
否则,请使用临时管理员账号以开发模式启动 Keycloak:
docker run -d --name apisix-keycloak \
-e 'KC_BOOTSTRAP_ADMIN_USERNAME=quickstart-admin' \
-e 'KC_BOOTSTRAP_ADMIN_PASSWORD=quickstart-admin-pass' \
-p 8080:8080 \
quay.io/keycloak/keycloak:26.7.3 start-dev
开发模式和示例凭证仅用于本地测试。生产部署应使用 HTTPS、生产数据库和永久管理员账号。
打开 http://localhost:8080/admin/,使用管理员用户名 quickstart-admin 和密码 quickstart-admin-pass 登录。
创建 Realm
为本指南创建一个隔离的 Realm:
- 选择 Manage realms → Create realm。
- 输入
quickstart-realm作为 Realm 名称。 - 选择 Create。

创建 API 作用域
创建 APISIX 将要求 M2M 访问 Token 包含的作用域:
- 选择 Client scopes → Create client scope。
- 输入
apisix.read作为名称,并保持 Protocol 为 OpenID Connect。 - 开启 Include in token scope,然后选择 Save。
创建受保护 API 客户端
创建一个表示 APISIX 所保护 API 的机密客户端:
- 选择 Clients → Create client。
- 保持 Client type 为 OpenID Connect,输入
apisix-protected-api作为客户端 ID,然后选择 Next。 - 开启 Client authentication。关闭所有身份认证流程,然后选择 Save。
APISIX 使用此客户端 ID 作为预期 Token 受众。只有可选的 Token 内省配置需要客户端密钥。
创建 M2M 客户端
创建一个带有 Keycloak 服务账号的机密客户端:
- 选择 Clients → Create client。
- 保持 Client type 为 OpenID Connect,输入
apisix-m2m-client作为客户端 ID,然后选择 Next。 - 开启 Client authentication 和 Service account roles。关闭 Standard flow 和其他身份认证流程,然后选择 Save。
- 打开 Client scopes 标签页,然后选择 Add client scope。
- 选择
apisix.read,选择 Add,并将其添加为可选客户端作用域。
服务账号使此客户端能够通过客户端凭证授权获取 Token。该流程不会重定向浏览器,也不会对最终用户进行身份认证,因此不需要重定向 URI。
添加 Token 受众
将受保护 API 客户端添加为签发给 M2M 客户端的访问 Token 受众:
- 打开 Clients → apisix-m2m-client → Client scopes。
- 选择
apisix-m2m-client-dedicated,然后选择 Add mapper → By configuration → Audience。 - 输入
apisix-audience作为名称,并选择apisix-protected-api作为包含的客户端受众。 - 保持 Add to access token 开启 ,然后选择 Save。

访问 Token 的 aud 声明将包含 apisix-protected-api。这可以区分调用服务与受保护 API,并让 APISIX 拒绝为其他受众签发的 Token。
保存 OAuth 配置
选择 Realm settings。在 General 标签页中找到 Endpoints,然后打开 OpenID Endpoint Configuration。APISIX 容器必须能够通过发现文档中的主机名访问 Keycloak。
将示例地址替换为可访问的主机地址,并把该地址和发现端点保存为环境变量:
export KEYCLOAK_HOST=192.168.1.100
export KEYCLOAK_DISCOVERY="http://${KEYCLOAK_HOST}:8080/realms/quickstart-realm/.well-known/openid-configuration"
选择 Clients → apisix-m2m-client → Credentials 并复制调用方客户端的密钥。然后打开 Clients → apisix-protected-api → Credentials,复制受保护 API 客户端的密钥。
将客户端 ID、密钥和必需作用域保存为环境变量:
export KEYCLOAK_API_CLIENT_ID=apisix-protected-api
export KEYCLOAK_API_CLIENT_SECRET=replace-with-your-protected-api-client-secret
export KEYCLOAK_M2M_CLIENT_ID=apisix-m2m-client
export KEYCLOAK_M2M_CLIENT_SECRET=replace-with-your-client-secret
export KEYCLOAK_M2M_SCOPE=apisix.read
请妥善保管客户端密钥。生产环境应将凭证存储在 Secret 管理器中,并按照组织的凭证轮换策略进行轮换。
配置本地 JWT 验证
配置一个路由,在将请求转发到公共 HTTP 请求与响应服务 httpbin.org 之前,本地验证 Keycloak Bearer Token。/anything/m2m/* 端点会返回请求详情以供验证。
选择 Admin API 或 ADC 配置路由。
- Admin API
- ADC
创建一个使用 Keycloak JWKS 验证 Bearer Token 的路由:
curl "http://127.0.0.1:9180/apisix/admin/routes/keycloak-m2m" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
--data-binary @- <<EOF
{
"uri": "/anything/m2m/*",
"plugins": {
"openid-connect": {
"client_id": "$KEYCLOAK_API_CLIENT_ID",
"discovery": "$KEYCLOAK_DISCOVERY",
"bearer_only": true,
"use_jwks": true,
"claim_validator": {
"audience": {
"required": true,
"match_with_client_id": true
}
},
"required_scopes": ["$KEYCLOAK_M2M_SCOPE"],
"set_access_token_header": false,
"set_id_token_header": false,
"set_userinfo_header": false
},
"proxy-rewrite": {
"headers": {
"remove": ["Authorization"]
}
}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"httpbin.org:80": 1
}
}
}
EOF
❶ bearer_only 和 use_jwks:要求提供 Bearer 访问 Token,并使用 Keycloak 发布的公钥在本地验证其 JWT 签名。
❷ claim_validator.audience:要求 Token 的 aud 声明包含配置为 client_id 的受保护 API 客户端 ID。
❸ required_scopes:要求访问 Token 包含分配给服务账号客户端的 API 作用域。
❹ set_access_token_header、set_id_token_header 和 set_userinfo_header:设置为 false,防止 APISIX 将访问 Token、ID Token 和 Token 声明添加到上游请求头。
❺ proxy-rewrite.headers.remove:在 APISIX 代理请求之前移除原始 Bearer Token。如果上游应用程序必须接收访问 Token 或其声 明,请重新审查这些请求头设置。
创建包含相同路由配置的 adc.yaml 文件:
services:
- name: keycloak-m2m
routes:
- name: keycloak-m2m
uris:
- /anything/m2m/*
plugins:
openid-connect:
client_id: "${KEYCLOAK_API_CLIENT_ID}"
discovery: "${KEYCLOAK_DISCOVERY}"
bearer_only: true
use_jwks: true
claim_validator:
audience:
required: true
match_with_client_id: true
required_scopes:
- "${KEYCLOAK_M2M_SCOPE}"
set_access_token_header: false
set_id_token_header: false
set_userinfo_header: false
proxy-rewrite:
headers:
remove:
- Authorization
upstream:
type: roundrobin
nodes:
- host: httpbin.org
port: 80
weight: 1
❶ bearer_only 和 use_jwks:要求提供 Bearer 访问 Token,并使用 Keycloak 发布的公钥在本地验证其 JWT 签名。
❷ claim_validator.audience:要求 Token 的 aud 声明包含配置为 client_id 的受保护 API 客户端 ID。
❸ required_scopes:要求访问 Token 包含分配给服务账号客户端的 API 作用域。
❹ set_access_token_header、set_id_token_header 和 set_userinfo_header:设置为 false,防止 APISIX 将访问 Token、ID Token 和 Token 声明添加到上游请求头。
❺ proxy-rewrite.headers.remove:在 APISIX 代理请求之前移除原始 Bearer Token。如果上游应用程序必须接收访问 Token 或其声明,请重新审查这些请求头设置。
ADC 会将服务作为期望状态进行协调。标签选择器将此示例限制在其自身带标签的资源内。请先预览限定范围的变更,并确认其中没有意外更新或删除:
adc diff -f adc.yaml \
--include-resource-type service \
--label-selector docs-example=keycloak-m2m
同步已审查的服务配置:
adc sync -f adc.yaml \
--include-resource-type service \
--label-selector docs-example=keycloak-m2m
验证 M2M 授权
从 Keycloak 请求访问 Token。--user 选项使用 HTTP 基本身份认证发送客户端 ID 和密钥:
export KEYCLOAK_ACCESS_TOKEN="$(
curl -sS "http://${KEYCLOAK_HOST}:8080/realms/quickstart-realm/protocol/openid-connect/token" \
--user "${KEYCLOAK_M2M_CLIENT_ID}:${KEYCLOAK_M2M_CLIENT_SECRET}" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=client_credentials" \
--data-urlencode "scope=${KEYCLOAK_M2M_SCOPE}" | \
jq -er '.access_token'
)"
将访问 Token 发送到受保护路由:
curl -i "http://127.0.0.1:9080/anything/m2m/get" \
-H "Authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}"
响应 HTTP/1.1 200 OK 表明 APISIX 已接受受众和作用域符合配置的 Keycloak 访问 Token。响应正文应包含类似以下内容的字段:
{
"args": {},
"data": "",
"files": {},
"form": {},
"headers": {
"Accept": "*/*",
"Host": "127.0.0.1",
"User-Agent": "curl/8.7.1",
"X-Amzn-Trace-Id": "Root=1-...",
"X-Forwarded-Host": "127.0.0.1:9080"
},
"json": null,
"method": "GET",
"origin": "192.168.155.1, xxx.xxx.xxx.xxx",
"url": "http://127.0.0.1:9080/anything/m2m/get"
}
请求头值和报告的来源地址因环境而异。上游请求头不应包含 Bearer Token、访问 Token、ID Token 或 Token 声明。
请求一个不含必需作用域的 Token,并将其发送到路由:
TOKEN_WITHOUT_SCOPE="$(
curl -sS "http://${KEYCLOAK_HOST}:8080/realms/quickstart-realm/protocol/openid-connect/token" \
--user "${KEYCLOAK_M2M_CLIENT_ID}:${KEYCLOAK_M2M_CLIENT_SECRET}" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=client_credentials" | \
jq -er '.access_token'
)"
curl -i "http://127.0.0.1:9080/anything/m2m/get" \
-H "Authorization: Bearer ${TOKEN_WITHOUT_SCOPE}"
由于 Token 不包含 apisix.read,APISIX 会返回 HTTP/1.1 403 Forbidden。
分别发送不含 Token 的请求和带有格式错误 Token 的请 求:
curl -i "http://127.0.0.1:9080/anything/m2m/get"
curl -i "http://127.0.0.1:9080/anything/m2m/get" \
-H "Authorization: Bearer invalid-access-token"
APISIX 会对这两类请求返回 HTTP/1.1 401 Unauthorized。