背景是这样的:公司统一用 IDaaS(Identity as a Service)做登录,IDaaS 自己持有会话(cookie);后端服务有十几个,谁都不想在自己的代码里接一遍 SDK、写一遍登录跳转、管一遍会话。这篇把整个方案完整收拢:认证架构怎么定、ingress-nginx 怎么全局拦截、用户信息怎么以 header 契约交给业务服务、业务服务侧到底要做什么、本地开发怎么分层调试,最后是一份可以直接拿去过的上线检查表。
前两篇写过这套方案的 GitOps 部分:《GitOps 与 K8s 统一认证落地》讲了 auth 服务落地、ConfigMap 进 Git、命令大全和 observe → enforce 灰度;《GitOps 里 k8s 公共配置放哪》讲了仓库怎么组织。这篇聚焦方案本身和业务侧视角,GitOps 细节不重复展开。
目录
- 方案全貌
- 三个架构决策(以及为什么)
- ingress 全局拦截:五配置、一生效
- 用户信息契约:头规范是公司级的
- 业务服务侧要做什么
- 本地调试:四档保真度
- 上线检查表
- 对接 IDaaS 前要问清的四件事
一、方案全貌
先给一句话版本:
IDaaS 管”你是谁”,auth 服务管”你能不能进这个应用”,ingress 负责”每个请求都替你问一遍”,业务服务只管”从 header 里拿用户”。
浏览器 ──(IDaaS cookie)──> ingress-nginx
│ ① 子请求 /authz(带上 cookie、Host、原始 URI)
▼
auth 服务(IDaaS SDK + RBAC + 分级放行表)
│ ② 200 + X-Auth-User-* 头
▼
业务服务(starter 读头 → UserContext)
失败路径:
页面请求 401 → ingress 302 到 IDaaS 登录页(back=原URL)→ 登录完跳回来
API 请求 401 → 前端拦截器统一跳 IDaaS 登录页(loginUrl 前端自己配置)
三个角色的职责边界:
| 角色 | 职责 | 明确不做的事 |
|---|---|---|
| IDaaS | 认证、会话、登录页、登出 | 应用级 RBAC |
| auth 服务 | /authz 校验、RBAC、注入 header、分级放行表 |
会话、登录回调、自己的 cookie |
| 业务服务 | 业务逻辑 + 操作级权限校验 | 登录、IDaaS SDK、会话、跳转 |
这套结构对应的开源参照物是 oauth2-proxy,只是把”校验凭证”换成了”用公司 SDK 校验 IDaaS 会话”,把”凭证存储”完全交给了 IDaaS。
二、三个架构决策(以及为什么)
决策一:auth 服务做”判断服务”,不做反向代理
ingress-nginx 原生支持 auth-url:每个请求,ingress 内部先发一个子请求问 auth 服务,auth 只回 200/401,业务流量完全不经过它。反过来让 auth 当反向代理(所有 backend 指向它)等于自研网关——超时、websocket、大文件、流式响应、灰度发布全是坑,除非本来就要网关能力,否则不做。
附带一个细节:子请求模式下 auth 服务不能直接回 302(ingress 对非 2xx/401/403 会转成 500),跳转要靠 401 + auth-signin 让 ingress 去做。这反而干净。
决策二:会话归 IDaaS,auth 里没有 StpUtil.login
既然 IDaaS 自持会话,本地就不要再造一份:不 login、不发自己的 cookie、没有 SSO 回调。收益是登出天然同步(IDaaS 会话一没,下个请求自然 401)、没有双头会话的状态不一致问题、auth 服务退化成一个无状态校验器。
但”免本地会话”成立有两个前提,动手前必须跟 IDaaS 对清楚:
- cookie 的 domain 是不是父域。只有 IDaaS 把会话 cookie 直接种在
.company.com,各子域请求才能带着它到/authz做 SDK 校验。如果 cookie 只在 IDaaS 自己域名下,每个子域首次访问都得跳一趟换票,最后还是要在应用域种点什么——等于变相回到本地会话,这个简化就不成立了。 - 校验方式是什么:SDK 是本地验签(JWT/公钥)还是调内省接口?内省接口 QPS 限制多少?
第二个前提直接决定性能设计。全站流量都打 /authz,如果每次都回源内省,IDaaS 就成了全站最热的依赖,所以要两级缓存:ingress 层 auth-cache-duration 按 cookie 缓存一层;auth 服务里再加一层 Redis 缓存(cookie → 校验结果,TTL 30~60s);能本地验签就本地验签,别走内省。代价要说清楚:撤销延迟 = 缓存 TTL(踢人、禁用后最多一个缓存周期内还能访问),按安全要求调这个值。IDaaS 挂 = 全站挂,缓存层同时也是它抖动时的缓冲;失败策略建议 fail-closed。
关于 sa-token(公司指定要用)的正确姿势:只用它的 RBAC 部分,不用它的会话——实现 StpInterface 提供权限数据,用显式传 uid 的 hasRole/hasPermission 做校验,不依赖登录态。说实话这个用法下 sa-token 的增益有限,RBAC 本质就是”uid → 角色 → 允许的应用/路径”一张表加缓存,用 sa-token 只是统一 API 风格。千万别在后端偷偷 StpUtil.login 变相造会话,那就变成和 IDaaS 双头会话,状态不一致的坑全回来。
决策三:页面和 API 的失败行为必须分开
302 只对”浏览器直接访问”有意义:
- 页面请求:401 → ingress
auth-signin302 到 IDaaS,回来后 302 到原 URL; - XHR / SPA:浏览器会自动跟随 302,最后拿到的是 IDaaS 的 HTML,前端代码没法处理。所以 API 统一返回 401 + 状态码,前端拦截器统一跳转。别指望 401 的响应体——nginx 默认会吞掉 auth 子请求的 body,loginUrl 由前端自己配置,前端见 401 就跳。
三、ingress 全局拦截:五配置、一生效
原理一句话:ingress-nginx controller 实时 watch 集群资源,把 Ingress/Service/ConfigMap 渲染成真实的 nginx.conf 再热加载;ConfigMap 里那几个 global-auth-* 是渲染时的全局参数,优先级是 Ingress 注解 > ConfigMap。
# ingress-nginx 的 ConfigMap(controller 启动参数 --configmap 指向的那个)
data:
global-auth-url: "http://auth.auth-system.svc.cluster.local:8080/authz"
global-auth-signin: "https://idaas.xxx.com/login?back=$pass_access_scheme://$http_host$escaped_request_uri"
global-auth-response-headers: "X-Auth-User-Id,X-Auth-User-Name,X-Auth-Roles"
global-auth-cache-key: "$cookie_<idaas的cookie名>"
global-auth-cache-duration: "30s"
| 配置 | 作用 |
|---|---|
global-auth-url |
子请求打到哪(auth 服务的校验接口) |
global-auth-signin |
401 时浏览器被 302 去哪(IDaaS 登录页,back 带原地址) |
global-auth-response-headers |
auth 响应里的哪些头要注入给后端 |
global-auth-cache-key |
缓存 key,按 cookie 区分用户(必须绑凭证,写死会让 A 的结果发给 B,事故级配置) |
global-auth-cache-duration |
校验结果缓存多久,少打 auth 服务 |
生效规则:
- 默认全开:这个 controller 管的所有 ingress 什么都不配,自动被保护——新团队接入成本为零;
- 个别要不同行为:Ingress 上加
auth-url注解覆盖全局值; - 个别要退出(公开入口、auth 服务自己、webhook):加注解
nginx.ingress.kubernetes.io/enable-global-auth: "false"。auth 服务自己的 ingress 必须退出,否则 302 死循环。
ConfigMap 一改全站生效是把双刃剑:推广成本为零,爆炸半径也是全站。所以它必须进 Git 走审批(怎么管见前一篇),上线走 observe → enforce 灰度(怎么灰见《GitOps 与 K8s 统一认证落地》)。
四、用户信息契约:头规范是公司级的
ingress 注入 header 的机制是渲染出来的两行 nginx 指令:auth_request_set 从 auth 响应取值,proxy_set_header 传给上游。头名清单要作为公司规范定死,比如:
| 头 | 含义 |
|---|---|
X-Auth-User-Id |
用户唯一 ID(IDaaS uid) |
X-Auth-User-Name |
显示名 |
X-Auth-Roles |
逗号分隔的角色(粗粒度) |
配套防伪造三件套,缺一不可:
- NetworkPolicy / 服务暴露收口:后端只能从 ingress 进。这是根本防线——内网能直连 pod 的话,header 谁都能伪造;
- auth 服务永远回写这些头,空值也写:防止客户端自带的同名头原样透传到后端;
- 头带前缀,后端只信前缀:约定了
X-Auth-前缀,业务代码就不该去读别的身份头。
另外别把全量权限塞 header——体积、时效都有问题。header 只带身份 + 粗粒度角色,够 ingress 层做”能不能进这个应用”的判断就行。
五、业务服务侧要做什么
先说结论:认证零改动(不接 IDaaS SDK、不做登录、没有会话代码),但”读 header 的接入层”是实打实的工作量。四件事:
1. 把读 header 封装成共享 starter,别让每个团队手写
一个 company-auth-starter:Filter 把 X-Auth-* 转成 UserContext(ThreadLocal),业务代码直接取当前用户:
@Component
@Order(Ordered.HIGHEST_PRECEDENCE + 10)
public class UserContextFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
throws ServletException, IOException {
String uid = req.getHeader(Headers.USER_ID);
if (AuthRequirements.requiresUser(req) && uid == null) {
res.sendError(401); // 缺头 = 未认证,绝不 302
return;
}
UserContext.set(UserContext.of(uid, req));
try {
chain.doFilter(req, res);
} finally {
UserContext.clear(); // 线程池复用,必须清
}
}
}
否则十几个服务会解析出十种花样,还容易有人把头名写错。业务团队的实际改动就是:加一个 starter 依赖 + 把原来取 session 的地方换成取 UserContext。如果现有服务已有”取当前用户”的封装,把它的实现换成读 header,业务代码一行不用动。
2. 认证语义:缺头 = 401,默认拒绝
需要用户身份的接口,header 不在就返回 401(不是空指针往下跑)。业务服务永远不要自己 302 跳登录——那是 ingress/auth 的事,它只管 401。
3. 细粒度权限要想清楚放哪
header 里的角色够做粗校验(这个服务能不能进),但操作级权限(”能不能改这张单子”)是业务逻辑:
- 要么业务服务查共享权限服务/表(传 uid);
- 要么 IDaaS 的组/角色 claim 够用就直接用。
4. CSRF 还在
浏览器自动带 IDaaS cookie,从业务服务视角这仍然是 cookie 认证,写操作要防 CSRF。IDaaS cookie 若是 SameSite=Lax,大部分场景已兜住;若是 None(SSO 产品为跨域常这么配),后端要对 mutating 请求校验 Origin 或要求自定义头(XHR-only 模式)。
六、本地调试:四档保真度
先建立认知:生产链路是 浏览器 --cookie→ ingress --子请求→ auth --注入header→ 业务服务,本地缺三样东西——ingress 的注入动作、IDaaS cookie、auth 的可达性。所以本地调试本质上是在决定两件事:用户信息从哪来(替代品)、验证到哪一层(保真度)。
| 档位 | 用户信息来源 | 能验证什么 | 场景 |
|---|---|---|---|
| ① mock | starter 本地配置 | 业务逻辑 | 日常写代码(90% 时间) |
| ② 造 header | nginx/curl 伪造的头 | starter 解析、401 语义、权限判断 | 调 starter/权限、接口自测 |
| ③ 本地 nginx → dev auth | 真实 IDaaS cookie | /authz 校验、RBAC 数据 | 调 auth 服务本身 |
| ④ dev 集群 | 全真实 | 302 跳转、cookie 域、全链路 | 上线前验收 |
① 日常开发:starter 的 mock 模式
业务代码两个模式下一行不差,只是用户来源不同:
# application-local.yml
company:
auth:
mode: mock # header(生产)| mock(本地)
mock:
user-id: "dev001"
user-name: "张三(本地)"
roles: "app-user,admin"
必须加护栏——生产环境 + mock 模式直接拒绝启动,否则哪天本地配置带上线,等于开了个伪造身份的后门:
if (props.getMode() == Mode.MOCK && env.acceptsProfiles(Profiles.of("prod"))) {
throw new IllegalStateException("auth mock 模式禁止在生产 profile 下启用");
}
② 接口自测:直接造 header(最容易被忽略的好办法)
生产契约就是”ingress 会带这几个头进来”,那么本地 curl / IDEA HTTP Client / Apifox 手工带上同样的头,走的就是和生产完全相同的代码路径:
curl http://localhost:8080/api/orders \
-H "X-Auth-User-Id: dev001" \
-H "X-Auth-User-Name: test" \
-H "X-Auth-Roles: app-admin"
不用任何额外组件。浏览器前后端联调想让 starter 走真实 header 路径,可以起一个本地 nginx 注入头(模拟 ingress,不碰 auth):
server {
listen 80;
location / {
proxy_set_header X-Auth-User-Id dev001;
proxy_set_header X-Auth-Roles app-admin;
proxy_pass http://127.0.0.1:8080;
}
}
③ 调 auth 服务自身:本地 nginx 模拟 ingress,指向 dev auth
集群里 auth_request 是 ingress 替你做的,本地没有 ingress,要自己起一个补位。三个前提先处理:
- hosts 把 dev 域名指到 127.0.0.1(cookie 跟域走,localhost 收不到父域 cookie);
- 本地要 HTTPS(SSO cookie 几乎必然
Secure),mkcert 签证书; - 网络可达:给 auth 服务开 dev ingress(它自己的 ingress 必须
enable-global-auth: "false"),或kubectl port-forward。
location / {
auth_request /_authz;
error_page 401 = @sso;
proxy_pass http://127.0.0.1:8080; # 本地业务服务
}
location = /_authz {
internal;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_pass http://auth.dev.xxx.svc/authz; # 或 VPN/port-forward 可达的地址
}
location @sso {
return 302 "https://idaas.xxx.com/login?back=$scheme://$host$request_uri";
}
直接调 /authz 也行:浏览器登录一次 dev IDaaS,F12 复制 cookie,然后 curl 本地 auth。这里有个设计上的坑要提前定:/authz 的判断依赖 ingress 子请求会传什么进来——Host(识别应用)、原始 URI、X-Auth-Request-Redirect 等。这个输入契约必须写成文档,否则本地没法模拟、线上出问题也没法 curl 复现:
curl -v http://localhost:9000/authz \
-H "Cookie: <idaas cookie>" \
-H "Host: a.dev.company.com" \
-H "X-Auth-Request-Redirect: /api/orders"
④ 全链路:只有 dev 集群能验
302 跳 IDaaS、回跳、cookie 域名、SameSite、缓存行为——这些和域名/浏览器强绑定,本地模拟不完整。做法就是把服务部署到 dev 集群的个人 namespace,走真实 ingress + dev auth 验一遍。这是上线前的验收动作,不是日常。
红线:本地和联调只允许指 dev 的 auth / IDaaS / 权限库,架构上不出现直连生产的路径。
七、上线检查表
- IDaaS cookie 父域确认、
Secure/SameSite确认(免本地会话成立的前提) /authz输入契约文档化(Host、URI、X-Auth-Request-Redirect)- 分级放行表初始化:所有应用 observe 起步,公开端点 bypass(分级放行见前一篇)
- OPTIONS 预检放行(预检不带 cookie,不放过所有跨域前端直接报 CORS 错)
- auth 服务自身 ingress
enable-global-auth: "false"(防 302 死循环) auth-cache-key绑 cookie(写死 = A 用户的认证结果发给 B,事故级)- auth 服务多副本 + Redis 高可用,失败策略 fail-closed
- NetworkPolicy 收口后端入口 + auth 服务空值也回写
X-Auth-*头(防伪造) - 撤销延迟(= 缓存 TTL)过安全评审
- 前端统一 401 拦截器组件,别每个前端项目写一遍
- starter 生产禁用 mock 护栏验证过
- 全部服务域名收进 IDaaS cookie 的父域
- ingress-nginx ConfigMap 进 Git(见前一篇),改动走审批
八、对接 IDaaS 前要问清的四件事
- 会话 cookie 的 domain、有效期、滑动续期条件;
- SDK 校验形态(本地验签还是内省)、内省接口 QPS 限制;
- 登录跳转 URL 支不支持
back/redirect参数、有没有域名白名单; - 用户属性(内省结果 / token claims)带不带角色或组织——决定 RBAC 数据源用 IDaaS 的组还是自己维护的权限表。
这四个答案拿到手,上面所有设计决策就都能定稿了。
参考
- 前篇:GitOps 与 K8s 统一认证落地(ConfigMap 进 Git、命令大全、observe → enforce 灰度)
- 前篇:GitOps 里 k8s 公共配置放哪
- ingress-nginx 官方文档 - External Authentication
- ingress-nginx 官方文档 - ConfigMap
- oauth2-proxy——本文这套”子请求 + 注入 header + 302 登录”模式的参照物
- sa-token 官方文档