跳到主要内容
版本:3.10.x

将外部认证用户信息转发到上游

使用 openid-connectsaml-auth 保护路由时,API7 网关会先对请求进行身份认证,再将其代理到上游。有时上游服务还需要知道请求来自哪个用户。

两个插件都会将已认证用户存储在 ctx.external_user 中。可以配合 serverless-post-function 使用该值:

  • 设置 ctx.consumer_name,使日志和其他插件能够使用此身份。
  • 为上游服务添加请求头。

对于 OpenID Connect,插件已经可以通过请求头转发用户信息和 Token。需要设置 consumer_name 或为 SAML 构建自定义请求头时,请使用 serverless-post-function

前置条件

设置消费者名称

在同一路由的 rewrite 阶段添加 serverless-post-function。默认情况下,它在 openid-connectsaml-auth 之后运行,因此函数执行时 ctx.external_user 已填充。如果自定义过插件优先级,请验证执行顺序。

以下函数对 OpenID Connect 使用 sub,对 SAML 使用 name_id,并将其写入 ctx.consumer_name

serverless-post-function
phase: rewrite
functions:
- |
return function(conf, ctx)
local user = ctx.external_user
if type(user) ~= "table" then
return
end
local id = user.sub or user.name_id
if id then
ctx.consumer_name = id
end
end

配置下发到网关后,通过路由发送已认证请求。其他插件随后可以读取 ctx.consumer_name。如果希望在访问日志中记录该值,请在日志格式中包含 consumer_name

将用户信息转发到上游

OpenID Connect

使用标准 OpenID Connect 请求头转发时无需使用 serverless-post-function。插件可以在上游请求中添加以下请求头:

标志默认值在上游请求中设置的请求头
set_userinfo_headertrueX-Userinfo——用户信息文档的 Base64 编码 JSON。
set_access_token_headertrueX-Access-Token——原始 Access Token。(将 access_token_in_authorization_header: true 设置为在 Authorization 中传递。)
set_id_token_headertrueX-ID-Token——流程中存在时的原始 ID Token。
set_refresh_token_headerfalseX-Refresh-Token——Refresh Token。仅当上游确实需要自行刷新时才启用。

将任一标志设置为 false 可禁止相应请求头。有关所有请求头相关选项,请参阅 openid-connect插件参考

SAML

SAML 属性没有等效的内置请求头。请使用 serverless-post-function 读取 ctx.external_user 并明确设置请求头。先清除请求头,确保客户端提供的值不会到达上游:

serverless-post-function
phase: rewrite
functions:
- |
return function(conf, ctx)
local core = require("apisix.core")
-- 先清除客户端提供的值。
core.request.set_header(ctx, "X-SAML-NameID", nil)
core.request.set_header(ctx, "X-SAML-Userinfo", nil)

local user = ctx.external_user
if user and user.authenticated then
core.request.set_header(ctx, "X-SAML-NameID", user.name_id or "")
local userinfo = {
name_id = user.name_id,
session_index = user.session_index,
issuer = user.issuer,
attrs = user.attrs,
}
core.request.set_header(
ctx,
"X-SAML-Userinfo",
ngx.encode_base64(core.json.encode(userinfo))
)
end
end

此示例发送:

  • X-SAML-NameID——用于快速查找的原始 NameID。
  • X-SAML-Userinfo——包含 NameID、会话索引、IdP 颁发者和 IdP 属性映射的 Base64 编码 JSON 文档。

请限制对上游服务的直接访问,确保仅信任经过 API7 网关的请求中的这些请求头。

在 SAML 路由上同时使用两种模式

如果路由同时需要 consumer_name 和 SAML 请求头,请将逻辑合并到一个 serverless-post-function 实例中:

serverless-post-function
phase: rewrite
functions:
- |
return function(conf, ctx)
local core = require("apisix.core")
local user = ctx.external_user
if type(user) ~= "table" then
return
end

local id = user.sub or user.name_id
if id then
ctx.consumer_name = id
end

core.request.set_header(ctx, "X-SAML-NameID", nil)
core.request.set_header(ctx, "X-SAML-Userinfo", nil)
if user.authenticated and user.name_id then
core.request.set_header(ctx, "X-SAML-NameID", user.name_id)
local userinfo = {
name_id = user.name_id,
session_index = user.session_index,
issuer = user.issuer,
attrs = user.attrs,
}
core.request.set_header(
ctx,
"X-SAML-Userinfo",
ngx.encode_base64(core.json.encode(userinfo))
)
end
end

在 OpenID Connect 路由上,只保留 consumer_name 代码块,并使用插件内置的请求头标志。

验证结果

应用配置后,确认上游收到预期用户信息,并且 consumer_name 在所需位置可用。

检查上游请求头

  1. 将路由指向 httpbin.org/anything 等请求回显上游,使响应体列出上游收到的所有请求头。
  2. 通过路由发送已认证请求。 — 对于设置了 bearer_only: trueopenid-connect,从 IdP 获取 Access Token,并使用 -H "Authorization: Bearer <token>" 调用路由。 — 对于 saml-auth 或交互式 OpenID Connect 流程,请在浏览器中完成登录。
  3. 检查上游响应中回显的请求头。 — 对于 OpenID Connect,查找 X-UserinfoX-Access-Token,以及流程中存在时的 X-ID-Token。 — 对于 SAML,查找函数设置的请求头,如 X-SAML-NameIDX-SAML-Userinfo

检查 consumer_name

如果设置了 ctx.consumer_name,请确认该值出现在部署暴露它的位置,例如访问日志字段或读取消费者身份的其他插件。

故障排除

如果上游请求头或 consumer_name 未按预期出现,请检查 ctx.external_user 实际包含的内容。将以下 serverless-post-function 挂载到路由,发送一个已认证请求,然后读取网关错误日志:

serverless-post-function
phase: rewrite
functions:
- |
return function(conf, ctx)
local core = require("apisix.core")
core.log.warn("ctx.external_user: ", core.json.encode(ctx.external_user, true))
end

具体字段集取决于 IdP、请求的作用域,以及 IdP 端配置的声明或属性映射器。典型字段如下:

插件常见字段
openid-connectsubemailemail_verifiednamepreferred_usernamegiven_namefamily_nameissaud,以及 IdP 返回的额外声明,例如 Keycloak 的 realm_accessresource_access,或 Auth0 的 roles
saml-authname_idsession_indexissuerattrs(IdP 属性映射)、authenticated

如果值为空或为 nil,说明身份认证插件没有在 serverless-post-function 之前运行,或请求在到达 rewrite 阶段前已被拒绝。请检查路由插件列表和 IdP 端配置。如果值已填充,但读取的字段(subname_id 或自定义声明)缺失,请调整 IdP 的声明或属性映射器,或从输出中读取其他字段。

如果配置看起来正确但未发生变化,请等待几秒,让最新配置下发到网关后重试。

后续步骤