跳到主要内容

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

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

在 Keycloak 中创建 Realm

创建 API 作用域

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

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

创建受保护 API 客户端

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

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

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

创建 M2M 客户端

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

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

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

添加令牌受众

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

  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 令牌受众

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

创建一个使用 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_onlyuse_jwks:要求提供 Bearer 访问令牌,并使用 Keycloak 发布的公钥在本地验证其 JWT 签名。

claim_validator.audience:要求令牌的 aud 声明包含配置为 client_id 的受保护 API 客户端 ID。

required_scopes:要求访问令牌包含分配给服务账号客户端的 API 作用域。

set_access_token_headerset_id_token_headerset_userinfo_header:设置为 false,防止 APISIX 将访问令牌、ID 令牌和令牌声明添加到上游请求头。

proxy-rewrite.headers.remove:在 APISIX 代理请求之前移除原始 Bearer 令牌。如果上游应用程序必须接收访问令牌或其声明,请重新审查这些请求头设置。

验证 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 替换路由配置。

创建不含 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 令牌。

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

后续步骤

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