Nginx CAS 认证与 SSO:使用 Casdoor 在 Nginx 后保护任意应用
开源版 nginx 没有 CAS 模块,因此实际上“nginx CAS 认证”意味着两种配置之一。要么 nginx 通过其 auth_request 模块向每个请求询问认证服务,由 Casdoor 回答(直接通过 /api/forward-auth,或通过 casdoor-forward-auth 或 OAuth2 Proxy),要么 nginx 后面的应用本身是一个 CAS 客户端,并与 Casdoor 的 CAS 服务器端点通信。如果您可以重新构建 nginx,Casdoor nginx 模块会在 nginx 内部运行登录;如果您不想运行 nginx,Casdoor 的站点代理可以位于应用之前。在所有情况下,登录页面都是 Casdoor 的,因此一个 SSO 会话、一组权限和一个 MFA 策略覆盖所有应用。
选项概览
| 选项 | 登录逻辑运行位置 | Nginx 需要什么 | 与 Casdoor 的协议 | 谁可以进入 | 何时选择它 |
|---|---|---|---|---|---|
| Casdoor 转发认证 | Casdoor, /api/forward-auth | auth_request | 应用域名上的 Casdoor 会话 Cookie | 应用权限,至少每分钟再次检查 | Nginx 已经在前端,且您不想添加额外服务 |
| casdoor-forward-auth | 一个小型 Go 服务 | auth_request | OIDC 授权码 | Casdoor 角色或组,按 location 区分 | 多个子域名共享一个会话 Cookie |
| OAuth2 Proxy | OAuth2 Proxy | auth_request | OIDC | allowed_groups, 电子邮件域 | 您已经运行 OAuth2 Proxy |
| casdoor-nginx-module | nginx 本身 | 一个编译好的模块 | 带有 PKCE 的 OIDC,本地验证令牌 | 该应用的任何已登录用户 | 无需额外进程,并为 API 提供 Bearer 令牌 |
| 应用中的 CAS 客户端 | 应用 | 无,仅代理 | CAS 1.0, 2.0, 3.0 | 应用如何处理属性 | 应用已经支持 CAS |
| Casdoor 站点代理 | Casdoor 在端口 80 和 443 | 无 Nginx | Casdoor 会话 | 应用权限 | 没有其他东西位于应用前端 |
前三个选项的区别仅在于谁回答 nginx 的子请求。
为什么没有“nginx CAS 模块”
CAS 是应用程序与 CAS 服务器之间的对话:应用将浏览器发送到服务器的 /login 并附带 service URL,在该 URL 上获得服务票据,并通过服务器到服务器的调用验证票据。Apache httpd 有 Apereo 的 mod_auth_cas 来在 Web 服务器上执行此操作,但 nginx 的官方模块中没有 CAS 模块。对于 nginx,标准构建块是 ngx_http_auth_request_module:在提供服务之前,nginx 向您选择的 URL 发送子请求。2xx 响应允许请求通过,401 或 403 以该代码拒绝,其他代码视为错误。
对用户而言,CAS 登录和 OIDC 登录看起来相同:重定向到 Casdoor 登录页面再返回。因此,对于不支持 CAS 的应用,实际解决方案是在 nginx 层使用 OIDC。关于协议的背景信息:什么是 CAS。
auth_request 模块默认不构建:编译时添加 --with-http_auth_request_module,或在打包版的 nginx 上检查 nginx -V。
选项 1:Casdoor 自身的转发认证
Casdoor 可以自行回答 nginx 的子请求。您在 Casdoor 中添加一个 Site,其 Domain 为应用的域名,其 Casdoor app 为用户登录的应用,并将 https://app.example.com/caswaf-handler 添加到该应用的 Redirect URLs。主机和端口可以留空,因为流量不经过 Casdoor。
由于 auth_request 无法跟随重定向,Casdoor 对未认证的子请求响应 401,并将登录 URL 放入 Location。nginx 将其转换为 302:
server {
server_name app.example.com;
proxy_buffer_size 16k;
proxy_buffers 8 16k;
location = /caswaf-handler {
proxy_pass http://casdoor:8000/api/forward-auth;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-Uri $request_uri;
}
location = /casdoor-auth {
internal;
proxy_pass http://casdoor:8000/api/forward-auth;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header X-Original-URL $scheme://$http_host$request_uri;
proxy_set_header X-Forwarded-Method $request_method;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location / {
auth_request /casdoor-auth;
auth_request_set $casdoor_redirect $upstream_http_location;
auth_request_set $casdoor_user $upstream_http_x_forwarded_user;
auth_request_set $casdoor_email $upstream_http_x_forwarded_email;
auth_request_set $casdoor_groups $upstream_http_x_forwarded_groups;
error_page 401 =302 $casdoor_redirect;
proxy_set_header X-Forwarded-User $casdoor_user;
proxy_set_header X-Forwarded-Email $casdoor_email;
proxy_set_header X-Forwarded-Groups $casdoor_groups;
proxy_pass http://app:3000;
}
}
后端在 X-Forwarded-User、X-Forwarded-Email 和 X-Forwarded-Groups 中接收用户。访问遵循应用上的权限,至少每分钟再次检查,因此被禁用的用户会迅速失去访问权限。站点的 Public paths(Webhooks,健康检查)无需登录即可通过。
有关参考信息,请参阅 使用您自己的反向代理,有关 Grafana 的分步指南,请参阅 在不使用 OIDC 的情况下为应用添加登录。该应用必须仅通过 nginx 访问,否则任何人都可以自己发送 X-Forwarded-User。
选项 2:casdoor-forward-auth 或 OAuth2 Proxy
如果您不想为每个应用创建一个 Casdoor 站点,可以运行一个小型服务,针对 Casdoor 执行 OIDC 流程并响应子请求。
casdoor-forward-auth 是 Casdoor 为此提供的独立服务(它取代了 traefik-casdoor-auth)。它将用户保存在签名 Cookie 中,无需服务器状态。对于 nginx,它暴露 /verify 端点,返回带有身份标头的 200 状态码或 401 状态码,nginx 在收到 401 时重定向到其 /login:
server {
server_name app.example.com;
location = /_casdoor_verify {
internal;
proxy_pass http://casdoor-forward-auth:9999/verify?groups=built-in/dev;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
}
location @casdoor_login {
return 302 https://auth.example.com/login?rd=$scheme://$http_host$request_uri;
}
location / {
auth_request /_casdoor_verify;
error_page 401 = @casdoor_login;
auth_request_set $casdoor_user $upstream_http_x_forwarded_user;
auth_request_set $casdoor_roles $upstream_http_x_forwarded_roles;
proxy_set_header X-Forwarded-User $casdoor_user;
proxy_set_header X-Forwarded-Roles $casdoor_roles;
proxy_pass http://app:8080;
}
}
验证 URL 上的 roles 和 groups 查询参数将每个应用限制为持有特定 Casdoor 角色或属于特定群组的用户;已登录但缺少这些权限的用户会收到 403 错误。角色和群组在登录时读取并在会话期间保留,因此更改会在用户重新登录后生效。设置 COOKIE_DOMAIN=example.com 可以让一次登录覆盖所有子域名。Traefik 指南 介绍了如何在 Traefik 中使用相同的服务。
OAuth2 Proxy 的工作原理类似,需将 provider = "oidc" 且 oidc_issuer_url 设置为您的 Casdoor URL。其 /oauth2/auth 端点返回 202 或 401,用户和电子邮件标头需要 --set-xauthrequest,并且其 nginx 示例中的转发标头处理需要 --reverse-proxy。有两个 Casdoor 设置很重要:使用 JWT-Standard 令牌格式,否则未验证邮箱的用户将被拒绝;保留 profile 范围以便返回群组和角色。请参阅 OAuth2 Proxy with Casdoor 和 nginx 社区版指南。
选项 3:Casdoor nginx 模块
casdoor-nginx-module 在 nginx 内部处理登录,无需单独的进程。没有会话的浏览器会跳转到 Casdoor 的登录页面;模块使用 PKCE 交换代码,根据 Casdoor 证书验证访问令牌的签名、exp、aud 和类型,并将用户保存在签名 Cookie 中。API 客户端可以发送包含 Casdoor 访问令牌的 Authorization: Bearer,在本地进行验证而无需调用 Casdoor。
它需要 nginx 1.22 或更新版本以及 OpenSSL 1.1.1 或更新版本。您可以将其作为动态模块构建在您现有的 nginx 上:
./configure --with-compat --with-http_ssl_module --add-dynamic-module=/path/to/casdoor-nginx-module
make modules
cp objs/ngx_http_casdoor_module.so /etc/nginx/modules/
load_module modules/ngx_http_casdoor_module.so;
events {}
http {
server {
listen 443 ssl;
server_name app.example.com;
ssl_certificate /etc/nginx/app.crt;
ssl_certificate_key /etc/nginx/app.key;
casdoor_endpoint https://door.example.com;
casdoor_client_id <client-id>;
casdoor_client_secret <client-secret>;
casdoor_certificate /etc/nginx/casdoor.pem;
casdoor_redirect_uri https://app.example.com/casdoor/callback;
casdoor_token_location /_casdoor/token;
location = /_casdoor/token {
internal;
proxy_pass https://door.example.com/api/login/oauth/access_token;
proxy_ssl_server_name on;
}
location = /casdoor/callback {
casdoor_callback;
}
location / {
casdoor on;
proxy_set_header X-User $casdoor_user;
proxy_pass http://127.0.0.1:3000;
}
}
}
将占位符替换为应用的值。在会话过期之前,不会再次向 Casdoor 检查会话,因此禁用用户在会话结束时生效;casdoor_session_timeout 可缩短该时间窗口。该模块没有角色或群组检查。在 NGINX Plus 上,请参阅 NGINX Plus 指南。
选项 4:应用是 CAS 客户端
如果应用已经支持 CAS,例如使用 Apereo CAS 客户端的 Java 应用,nginx 仅充当代理,Casdoor 作为 CAS 服务器。每个 Casdoor 应用在 /cas/<organization>/<application> 下都有 CAS 端点:/login 和 /logout,用于 CAS 1.0 的 /validate,用于 2.0 的 /serviceValidate、/proxyValidate 和 /proxy,用于 3.0 的 /p3/serviceValidate 和 /p3/proxyValidate,以及用于 SAML 1.1 验证的 /samlValidate。
应用将浏览器发送到 /login?service=...,Casdoor 携带 ticket=ST-... 重定向回来,应用对其进行验证:
curl "https://door.example.com/cas/my-org/my-app/p3/serviceValidate?ticket=ST-1234567890&service=https%3A%2F%2Fapp.example.com%2F"
成功的响应是一个 cas:serviceResponse,其中包含 cas:user 以及电子邮件、显示名称和 memberOf 等属性;添加 format=json 以获取 JSON 格式。容易让人困惑的地方包括:
- 每个
serviceURL 都必须列在应用的 Redirect URLs 中,并且在验证时的service必须与颁发票据时使用的完全一致。 - 票据只能使用一次,并在 5 分钟后过期。
- 票据保存在内存中,因此在有多个 Casdoor 副本的情况下,验证请求必须到达颁发票据的那个副本(粘性会话)。
- 在 nginx 后面,传递
Host和X-Forwarded-Proto,以便应用构建与浏览器看到的相同的serviceURL。
设置方法(包括针对不同 CAS 版本的 Java 客户端过滤器)请参见 连接 CAS 客户端。
选项 5:让 Casdoor 成为反向代理
当应用前面没有其他组件时,Casdoor 可以直接提供服务:一旦存在站点,其站点代理将在 gatewayHttpPort 和 gatewayHttpsPort(默认为 80 和 443)上监听。将站点的 Host 设置为后端,选择证书,设置 Casdoor app,并将 DNS 指向 Casdoor。登录、权限和标头的行为与选项 1 相同。请参阅 Sites。
添加 MFA
MFA 不需要对 nginx 或应用进行任何更改。Casdoor 在其自身的登录页面上在发出 OAuth 授权代码或 CAS 服务票据之前询问第二因素,因此上述每个选项都支持它。用户可以使用 TOTP 应用、短信、电子邮件或 RADIUS,并配有恢复代码,管理员可以通过 MFA 项目 要求组织启用 MFA。这也回答了“CAS 2FA”的问题:当 Casdoor 作为 CAS 服务器时,第二因素是 CAS 登录的一部分。
MFA 在登录时强制执行,而不是针对每个请求:nginx 或 forward-auth 服务已经持有的会话将持续到过期为止,组织的 MFA remember time 允许用户在受信任的设备上一段时间内跳过第二因素。请参阅 MFA 指南 和 什么是 MFA。
一旦登录到 Casdoor,用户即可访问其他将请求发送到 Casdoor 的应用,而无需新的密码提示;启用 Auto signin 后,Casdoor 还会跳过账户选择器。请参阅 单点登录 和 Single logout。
如何选择
- 应用中无登录功能且 nginx 已在前面:使用 Casdoor 的 forward auth。
- 跨多个子域名的统一登录,或按位置进行角色检查:casdoor-forward-auth(如果你已经运行 OAuth2 Proxy,也可以使用它)。
- 你自己构建 nginx 并使用 bearer 令牌保护 API:casdoor-nginx-module。
- 应用支持 CAS:使用 Casdoor 的 CAS 端点,nginx 作为普通代理。
- 尚无代理:使用 Casdoor 的网站代理。
参考资料
- nginx: ngx_http_auth_request_module
- OAuth2 Proxy: nginx integration
- Apereo: CAS Protocol Specification, mod_auth_cas
- Casdoor: casdoor-forward-auth, casdoor-nginx-module
- Casdoor docs: Sites, Connect a CAS client, MFA