saml-auth
saml-auth 插件使 APISIX 和 API7 网关能够充当 SAML 2.0 服务提供商(SP)。用户请求受保护的路由时,网关会将浏览器重定向到身份提供商(IdP)。网关验证已签名的 SAML 响应并创建认证会话,然后再将请求代理到上游。
插件默认支持 HTTP-Redirect 绑定,同时支持 HTTP-POST 绑定 、由服务提供商发起的单点注销以及会话密钥轮换。同一请求上的其他插件可通过 ctx.external_user 获取已认证的用户数据。
响应校验
自 API7 企业版 3.9.21 起,插件无需额外配置即会对每个登录响应进行以下校验:
- 携带
AudienceRestriction的断言必须声明sp_issuer或sp_audiences中的某个值。 - 断言必须在其
NotBefore和NotOnOrAfter时间窗口内使用,允许clock_skew秒的偏差。 - 断言的
Recipient以及响应中存在的Destination必须与断言消费服务(ACS)URL 一致。 - 如果断言声明了其所响应的身份认证请求(
InResponseTo),该请求必须由同一浏览器会话发起。
未设置 sp_acs_url 时,ACS URL 根据回调请求的 scheme 和 host 构建。如果网关位于终止 TLS 但不设置 X-Forwarded-Proto 或会改写 host 的代理之后,请将 sp_acs_url 设置为公开的回调 URL。否则网关构建的 URL 与 IdP 写入 Recipient 的 URL 不一致,登录会被拒绝。
示例
以下示例使用 Keycloak 配置 SAML 单点登录和单点注销。本地测试使用 HTTP 和默认的 HTTP-Redirect 绑定;生产环境中的网关和 Keycloak 应使用 HTTPS。
开始前,请完成以下准备工作:
- 安装 Docker、cURL、jq 和 OpenSSL。
- 按照入门教程使用 Docker 启动 APISIX。
- 如需使用 ADC,请先安装并配置 ADC。
- 如需使用 Ingress Controller 示例,请在
aic命名空间中设置 Ingress Controller 和网关。
启动 Keycloak
在与网关部署方式相匹配的环境中启动 Keycloak。
- Docker
- Kubernetes
以开发模式启动 Keycloak:
docker run -d --name apisix-saml-keycloak \
-e 'KC_BOOTSTRAP_ADMIN_USERNAME=quickstart-admin' \
-e 'KC_BOOTSTRAP_ADMIN_PASSWORD=quickstart-admin-pass' \
-p 127.0.0.1:8080:8080 \
quay.io/keycloak/keycloak:26.7.3 start-dev
如果命名空间尚不存在,请创建该命名空间:
kubectl create namespace aic --dry-run=client -o yaml | kubectl apply -f -
创建包含 Keycloak Deployment 和 Service 的 keycloak.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
namespace: aic
name: keycloak
spec:
replicas: 1
selector:
matchLabels:
app: keycloak
template:
metadata:
labels:
app: keycloak
spec:
containers:
- name: keycloak
image: quay.io/keycloak/keycloak:26.7.3
args:
- start-dev
env:
- name: KC_BOOTSTRAP_ADMIN_USERNAME
value: quickstart-admin
- name: KC_BOOTSTRAP_ADMIN_PASSWORD
value: quickstart-admin-pass
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
namespace: aic
name: keycloak
spec:
selector:
app: keycloak
ports:
- port: 8080
targetPort: 8080
应用清单并等待 Keycloak 就绪:
kubectl apply -f keycloak.yaml
kubectl rollout status -n aic deployment/keycloak
在另一个终端中转发 Keycloak 端口,以便浏览器可在 SAML 流程中访问 IdP:
kubectl port-forward -n aic service/keycloak 8080:8080
开发服务器和示例管理员凭证仅供本地测试使用。生产部署应使用 启用 HTTPS 的 Keycloak 生产模式、生产数据库以及妥善管理的管理员凭证。
创建服务提供商证书
生成网关用于签署 SAML 请求的证书和私钥:
openssl req -x509 -newkey rsa:2048 -nodes \
-keyout sp-private-key.pem \
-out sp-cert.pem \
-days 365 \
-subj "/CN=APISIX SAML"
请安全存储私钥。在生产环境中,请将自签名证书替换为符合组织证书策略的证书。
配置 Keycloak
打开 http://localhost:8080/admin/,使用用户名 quickstart-admin 和密码 quickstart-admin-pass 登录。
为示例创建 Realm:
- 选择 Manage realms → Create realm。
- 输入
apisix-saml作为 Realm 名称,然后选择 Create。
创建一个可通过 IdP 进行身份认证的用户:
- 选择 Users → Add user。
- 输入
alice作为用户名,填写必填的个人资料字段,然后选择 Create。 - 打开 Credentials 标签页,然后选择 Set password。
- 输入
alice-pass,关闭 Temporary,然后保存密码。
将网关注册为 SAML 客户端:
- 选择 Clients → Create client。
- 选择
SAML作为客户端类型,输入apisix-saml作为客户端 ID,然后选择 Save。客户端 ID 必须与插件中配置的sp_issuer值相同。 - 在 Access settings 中添加以下有效重定向 URI:
http://127.0.0.1:9080/anything/login_callbackhttp://127.0.0.1:9080/anything/logout_callback
- 在 SAML capabilities 中,保持 Force POST binding 关闭。
- 在 Signature and encryption 中,启用 Sign documents 和 Sign assertions,并选择
RSA_SHA256作为签名算法。 - 在 Advanced → Fine Grain SAML Endpoint Configuration 中,将 Logout Service Redirect Binding URL 设置为
http://127.0.0.1:9080/anything/logout_callback。 - 保存客户端。
- 打开 Keys 标签页,启用 Client signature required,然后以 PEM 格式证书导入
sp-cert.pem。
插件使用 sp-private-key.pem 签署身份认证和注销请求。Keycloak 使用导入的服务提供商证书验证这些签名。插件还会拒绝未签名的 Keycloak 响应,因此 Keycloak 必须对返回给网关的 SAML 文档进行签名。
保存 Keycloak 证书
打开 http://localhost:8080/realms/apisix-saml/protocol/saml/descriptor。从 X509Certificate 元素复制签名证书,并使用 PEM 标头和页脚将其保存为 idp-cert.pem:
-----BEGIN CERTIFICATE-----
replace-with-the-keycloak-signing-certificate
-----END CERTIFICATE-----
元数据还列出了 SAML 单点登录端点。本示例使用用户浏览器可以访问的 http://localhost:8080/realms/apisix-saml/protocol/saml。
生成会话密钥,并将证书加载到用于配置网关的环境变量中:
export SAML_SESSION_SECRET="$(openssl rand -hex 16)"
export IDP_CERT="$(cat idp-cert.pem)"
export SP_CERT="$(cat sp-cert.pem)"
export SP_PRIVATE_KEY="$(cat sp-private-key.pem)"
请在每个网关节点上使用相同的会话密钥。轮换密钥时,请将上一个值移至 secret_fallbacks,以便现有会话仍可读取。
配置网关
此配置保护 /anything/*,并将 /anything/logout-complete 保持为公开路径,以便用户在单点注销后进入落地页。
选择用于配置路由的 API。
- Admin API
- ADC
- Ingress Controller
创建公开的注销目标:
curl "http://127.0.0.1:9180/apisix/admin/routes/saml-logout-complete" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"uri": "/anything/logout-complete",
"upstream": {
"type": "roundrobin",
"nodes": {
"httpbin.org:80": 1
}
}
}'
创建受保护的路由。jq 在不改变 PEM 格式的情况下读取证书和私钥文件:
jq -n \
--arg secret "$SAML_SESSION_SECRET" \
--rawfile idp_cert idp-cert.pem \
--rawfile sp_cert sp-cert.pem \
--rawfile sp_private_key sp-private-key.pem \
'{
"uri": "/anything/*",
"plugins": {
"saml-auth": {
"secret": $secret,
"sp_issuer": "apisix-saml",
"idp_uri": "http://localhost:8080/realms/apisix-saml/protocol/saml",
"login_callback_uri": "/anything/login_callback",
"logout_uri": "/anything/logout",
"logout_callback_uri": "/anything/logout_callback",
"logout_redirect_uri": "/anything/logout-complete",
"idp_cert": $idp_cert,
"sp_cert": $sp_cert,
"sp_private_key": $sp_private_key
}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"httpbin.org:80": 1
}
}
}' | \
curl "http://127.0.0.1:9180/apisix/admin/routes/saml-auth" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-H "Content-Type: application/json" \
--data-binary @-
创建包含公开注销目标和受保护路由的 adc.yaml:
services:
- name: saml-auth-httpbin
labels:
docs-example: saml-auth
routes:
- name: saml-logout-complete
uris:
- /anything/logout-complete
- name: saml-auth
uris:
- /anything/*
plugins:
saml-auth:
secret: "${SAML_SESSION_SECRET}"
sp_issuer: apisix-saml
idp_uri: http://localhost:8080/realms/apisix-saml/protocol/saml
login_callback_uri: /anything/login_callback
logout_uri: /anything/logout
logout_callback_uri: /anything/logout_callback
logout_redirect_uri: /anything/logout-complete
idp_cert: "${IDP_CERT}"
sp_cert: "${SP_CERT}"
sp_private_key: "${SP_PRIVATE_KEY}"
upstream:
type: roundrobin
nodes:
- host: httpbin.org
port: 80
weight: 1
ADC 将服务协调为所需状态。标签选择器将本示例限定为其自身带有标签的资源。预览限定范围内的变更,并确认其中不包含非预期的更新或删除:
adc diff -f adc.yaml \
--include-resource-type service \
--label-selector docs-example=saml-auth
同步已检查的服务配置:
adc sync -f adc.yaml \
--include-resource-type service \
--label-selector docs-example=saml-auth
应用任一清单前,请替换证书、私钥和会话密钥占位符。替换证书和密钥值时,请保留 PEM 标头、页脚和 \n 分隔符。
- Gateway API
- APISIX CRD
创建 saml-auth-ic.yaml:
apiVersion: v1
kind: Service
metadata:
namespace: aic
name: httpbin-external-domain
spec:
type: ExternalName
externalName: httpbin.org
---
apiVersion: apisix.apache.org/v1alpha1
kind: PluginConfig
metadata:
namespace: aic
name: saml-auth
spec:
plugins:
- name: saml-auth
config:
secret: replace-with-session-secret
sp_issuer: apisix-saml
idp_uri: http://localhost:8080/realms/apisix-saml/protocol/saml
login_callback_uri: /anything/login_callback
logout_uri: /anything/logout
logout_callback_uri: /anything/logout_callback
logout_redirect_uri: /anything/logout-complete
idp_cert: "-----BEGIN CERTIFICATE-----\nreplace-with-keycloak-certificate\n-----END CERTIFICATE-----"
sp_cert: "-----BEGIN CERTIFICATE-----\nreplace-with-service-provider-certificate\n-----END CERTIFICATE-----"
sp_private_key: "-----BEGIN PRIVATE KEY-----\nreplace-with-service-provider-private-key\n-----END PRIVATE KEY-----"
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
namespace: aic
name: saml-auth
spec:
parentRefs:
- name: apisix
rules:
- matches:
- path:
type: Exact
value: /anything/logout-complete
backendRefs:
- name: httpbin-external-domain
port: 80
- matches:
- path:
type: PathPrefix
value: /anything/
filters:
- type: ExtensionRef
extensionRef:
group: apisix.apache.org
kind: PluginConfig
name: saml-auth
backendRefs:
- name: httpbin-external-domain
port: 80
应用清单:
kubectl apply -f saml-auth-ic.yaml
创建 saml-auth-ic.yaml:
apiVersion: apisix.apache.org/v2
kind: ApisixUpstream
metadata:
namespace: aic
name: httpbin-external-domain
spec:
ingressClassName: apisix
externalNodes:
- type: Domain
name: httpbin.org
---
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
namespace: aic
name: saml-auth
spec:
ingressClassName: apisix
http:
- name: saml-logout-complete
match:
paths:
- /anything/logout-complete
upstreams:
- name: httpbin-external-domain
- name: saml-auth
match:
paths:
- /anything/*
upstreams:
- name: httpbin-external-domain
plugins:
- name: saml-auth
enable: true
config:
secret: replace-with-session-secret
sp_issuer: apisix-saml
idp_uri: http://localhost:8080/realms/apisix-saml/protocol/saml
login_callback_uri: /anything/login_callback
logout_uri: /anything/logout
logout_callback_uri: /anything/logout_callback
logout_redirect_uri: /anything/logout-complete
idp_cert: "-----BEGIN CERTIFICATE-----\nreplace-with-keycloak-certificate\n-----END CERTIFICATE-----"
sp_cert: "-----BEGIN CERTIFICATE-----\nreplace-with-service-provider-certificate\n-----END CERTIFICATE-----"
sp_private_key: "-----BEGIN PRIVATE KEY-----\nreplace-with-service-provider-private-key\n-----END PRIVATE KEY-----"
应用清单:
kubectl apply -f saml-auth-ic.yaml
验证单点登录和注销
在浏览器中打开 http://127.0.0.1:9080/anything/saml-test。Keycloak 会将浏览器重定向到其登录页面。使用用户名 alice 和密码 alice-pass 登录。
身份认证后,浏览器会返回受保护的路由。上游响应包含请求详情和类似以下内容的 saml_session Cookie:
{
"headers": {
"Cookie": "saml_session=...",
"Host": "127.0.0.1",
"X-Forwarded-Host": "127.0.0.1:9080"
},
"method": "GET",
"url": "http://127.0.0.1/anything/saml-test"
}
在同一浏览器中打开 http://127.0.0.1:9080/anything/logout。网关向 Keycloak 发送已签名的注销请求,在 /anything/logout_callback 处理已签名的注销响应,清除 SAML 会话,然后将浏览器重定向到 /anything/logout-complete。
返回 http://127.0.0.1:9080/anything/saml-test。Keycloak 应再次提示输入凭证,从而确认网关和 Keycloak 会话均已终止。