跳到主要内容
版本:3.19.0

使用 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:

  1. 选择 Manage realms → Create realm。
  2. 输入 quickstart-realm 作为 Realm 名称。
  3. 选择 Create。

在 Keycloak 中创建 Realm

创建 API 作用域​

创建 APISIX 将要求 M2M 访问 Token 包含的作用域:

  1. 选择 Client scopes → Create client scope。
  2. 输入 apisix.read 作为名称,并保持 Protocol 为 OpenID Connect。
  3. 开启 Include in token scope,然后选择 Save。

创建受保护 API 客户端​

创建一个表示 APISIX 所保护 API 的机密客户端:

  1. 选择 Clients → Create client。
  2. 保持 Client type 为 OpenID Connect,输入 apisix-protected-api 作为客户端 ID,然后选择 Next。
  3. 开启 Client authentication。关闭所有身份认证流程,然后选择 Save。

APISIX 使用此客户端 ID 作为预期 Token 受众。只有可选的 Token 内省配置需要客户端密钥。

创建 M2M 客户端​

创建一个带有 Keycloak 服务账号的机密客户端:

  1. 选择 Clients → Create client。
  2. 保持 Client type 为 OpenID Connect,输入 apisix-m2m-client 作为客户端 ID,然后选择 Next。
  3. 开启 Client authentication 和 Service account roles。关闭 Standard flow 和其他身份认证流程,然后选择 Save。
  4. 打开 Client scopes 标签页,然后选择 Add client scope。
  5. 选择 apisix.read,选择 Add,并将其添加为可选客户端作用域。

服务账号使此客户端能够通过客户端凭证授权获取 Token。该流程不会重定向浏览器,也不会对最终用户进行身份认证,因此不需要重定向 URI。

添加 Token 受众​

将受保护 API 客户端添加为签发给 M2M 客户端的访问 Token 受众:

  1. 打开 Clients → apisix-m2m-client → Client scopes。
  2. 选择 apisix-m2m-client-dedicated,然后选择 Add mapper → By configuration → Audience。
  3. 输入 apisix-audience 作为名称,并选择 apisix-protected-api 作为包含的客户端受众。
  4. 保持 Add to access token 开启,然后选择 Save。

将受保护 API 客户端添加到 Keycloak Token 受众

访问 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 配置路由。

创建一个使用 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 或其声明,请重新审查这些请求头设置。

验证 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。

使用内省验证 Token​

本地 JWT 验证避免在每次 Token 检查时向提供方发送请求。如果需要立即检查 Token 状态,APISIX 可以改为将 Bearer Token 发送到 Keycloak 的内省端点。此前配置的受众映射器允许受保护 API 客户端内省 M2M 客户端的 Token。

选择 Admin API 或 ADC 替换路由配置。

创建不含 use_jwks 的路由,并提供用于内省身份认证的客户端密钥:

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",
"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 Token。

重新请求包含必需作用域的访问 Token,然后再次请求受保护路由。响应 HTTP/1.1 200 OK 表明 Keycloak 报告该 Token 处于活动状态,并且 APISIX 接受了其受众和作用域。

后续步骤​

你已配置 APISIX 使用 Keycloak 授权 M2M 请求。若要使用同一身份提供商对浏览器用户进行身份认证,请参阅设置 Keycloak 单点登录。有关更多配置选项,请参阅 openid-connect 插件参考。