使用 Keycloak 授权 M2M 请求
Keycloak 可以向与机密 OIDC 客户端关联的服务账号签发访问令牌。后台服务、定时任务、命令行工具和自动化程序 可以使用这些令牌调用 API,而无需最终用户登录。
OAuth 2.0 客户端凭证授权专为这类机器到机器(M2M)通信设计。Apache APISIX 可以在代理请求之前验证每个令牌的签名、签发者、受众和已授予作用域,从而保护目标 API。
本指南使用一个 Keycloak 客户端表示由 APISIX 保护的 API,另一个服务账号客户端表示调用服务。服务账号会获得一个客户端作用域,并请求受众标识为受保护 API 的访问令牌。APISIX 会验证这两项限制,并在将请求转发到示例上游之前移除身份认证数据。主要配置使用 Keycloak 的 JSON Web Key Set(JWKS)在本地验证令牌;备选配置则使用 Keycloak 的令牌内省端点。
前置条件
配置 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 访问令牌包含的作用域:
- 选择 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 作为预期令牌受众。只有可选的令牌内省配置需要客户端密钥。
创建 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,并将其添加为可选客户端作用域。
服务账号使此客户端能够通过客户端凭证授权获取令牌。该流程不会重定向浏览器,也不会对最终用户进行身份认证,因此不需要重定向 URI。
添加令牌受众
将受保护 API 客户端添加为签发给 M2M 客户端的访问令牌受众:
- 打开 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。

访问令牌的 aud 声明将包含 apisix-protected-api。这可以区分调用服务与受保护 API,并让 APISIX 拒绝为其他受众签发的令牌。
保存 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 令牌。/anything/m2m/* 端点会返回请求详情以供验证。
选择 Admin API 或 ADC 配置路由。
- Admin API
- ADC
创建一个使用 Keycloak JWKS 验证 Bearer 令牌的路由:
curl "http://127.0.0.1:9180/apisix/admin/routes/keycloak-m2m" -X PUT \
--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 访问令牌,并使用 Keycloak 发布的公钥在本地验证其 JWT 签名。
❷ claim_validator.audience:要求令牌的 aud 声明包含配置为 client_id 的受保护 API 客户端 ID。
❸ required_scopes:要求访问令牌包含分配给服务账号客户端的 API 作用域。
❹ set_access_token_header、set_id_token_header 和 set_userinfo_header:设置为 false,防止 APISIX 将访问令牌、ID 令牌和令牌声明添加到上游请求头。
❺ proxy-rewrite.headers.remove:在 APISIX 代理请求之前移除原始 Bearer 令牌。如果上游应用程序必须接收访问令牌或其声明,请重新审查这些请求头设置。
创建包含相同路由配置的 adc.yaml 文件:
services:
- name: httpbin
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 访问令牌,并使用 Keycloak 发布的公钥在本地验证其 JWT 签名。
❷ claim_validator.audience:要求令牌的 aud 声明包含配置为 client_id 的受保护 API 客户端 ID。
❸ required_scopes:要求访问令牌包含分配给服务账号客户端的 API 作用域。
❹ set_access_token_header、set_id_token_header 和 set_userinfo_header:设置为 false,防止 APISIX 将访问令牌、ID 令牌和令牌声明添加到上游请求头。
❺ proxy-rewrite.headers.remove:在 APISIX 代理请求之前移除原始 Bearer 令牌。如果上游应用程序必须接收访问令牌或其声明,请重新审查这些请求头设置。
将配置同步到 APISIX:
adc sync -f adc.yaml
验证 M2M 授权
从 Keycloak 请求访问令牌。--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'
)"
将访问令牌发送到受保护路由:
curl -i "http://127.0.0.1:9080/anything/m2m/get" \
-H "Authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}"
响应 HTTP/1.1 200 OK 表明 APISIX 已接受受众和作用域符合配置的 Keycloak 访问令牌。响应正文应包含类似以下内容的字段:
{
"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 令牌、访问令牌、ID 令牌或令牌声明。
请求一个不含必需作用域的令牌,并将其发送到路由:
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}"
由于令牌不包含 apisix.read,APISIX 会返回 HTTP/1.1 403 Forbidden。
分别发送不含令牌的请求和带有格式错误令牌的请求:
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。
使用内省验证令牌
本地 JWT 验证避免在每次令牌检查时向提供方发送请求。如果需要立即检查令牌状态,APISIX 可以改为将 Bearer 令牌发送到 Keycloak 的内省端点。此前配置的受众映射器允许受保护 API 客户端内省 M2M 客户端的令牌。
选择 Admin API 或 ADC 替换路由配置。
- Admin API
- ADC
创建不含 use_jwks 的路由,并提供用于内省身份认证的客户端密钥:
curl "http://127.0.0.1:9180/apisix/admin/routes/keycloak-m2m" -X PUT \
--data-binary @- <<EOF
{
"uri": "/anything/m2m/*",
"plugins": {
"openid-connect": {
"client_id": "$KEYCLOAK_API_CLIENT_ID",
"client_secret": "$KEYCLOAK_API_CLIENT_SECRET",
"discovery": "$KEYCLOAK_DISCOVERY",
"bearer_only": 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
❶ client_secret:用于向 Keycloak 的内省端点证明 APISIX 作为受保护 API 客户端的身份。未配置 use_jwks 时,APISIX 会从发现文档获取该端点,并远程验证 Bearer 令牌 。
使用内省配置更新 adc.yaml:
services:
- name: httpbin
routes:
- name: keycloak-m2m
uris:
- /anything/m2m/*
plugins:
openid-connect:
client_id: "${KEYCLOAK_API_CLIENT_ID}"
client_secret: "${KEYCLOAK_API_CLIENT_SECRET}"
discovery: "${KEYCLOAK_DISCOVERY}"
bearer_only: 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
❶ client_secret:用于向 Keycloak 的内省端点证明 APISIX 作为受保护 API 客户端的身份。未配置 use_jwks 时,APISIX 会从发现文档获取该端点,并远程验证 Bearer 令牌。
同步更新后的配置:
adc sync -f adc.yaml
重新请求包含必需作用域的访问令牌,然后再次请求受保护路由。响应 HTTP/1.1 200 OK 表明 Keycloak 报告该令牌处于活动状态,并且 APISIX 接受了其受众和作用域。
后续步骤
你已配置 APISIX 使用 Keycloak 授权 M2M 请求。若要使用同一身份提供商对浏览器用户进行身份认证,请参阅设置 Keycloak 单点登录。有关更多配置选项,请参阅 openid-connect 插件参考。