背景是这样的:公司统一用 IDaaS(Identity as a Service)做登录,IDaaS 自己持有会话(cookie);后端服务有十几个,谁都不想在自己的代码里接一遍 SDK、写一遍登录跳转、管一遍会话。这篇把整个方案完整收拢:认证架构怎么定、ingress-nginx 怎么全局拦截、用户信息怎么以 header 契约交给业务服务、业务服务侧到底要做什么、本地开发怎么分层调试,最后是一份可以直接拿去过的上线检查表。

前两篇写过这套方案的 GitOps 部分:《GitOps 与 K8s 统一认证落地》讲了 auth 服务落地、ConfigMap 进 Git、命令大全和 observe → enforce 灰度;《GitOps 里 k8s 公共配置放哪》讲了仓库怎么组织。这篇聚焦方案本身和业务侧视角,GitOps 细节不重复展开。

目录

  1. 方案全貌
  2. 三个架构决策(以及为什么)
  3. ingress 全局拦截:五配置、一生效
  4. 用户信息契约:头规范是公司级的
  5. 业务服务侧要做什么
  6. 本地调试:四档保真度
  7. 上线检查表
  8. 对接 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 对清楚

  1. cookie 的 domain 是不是父域。只有 IDaaS 把会话 cookie 直接种在 .company.com,各子域请求才能带着它到 /authz 做 SDK 校验。如果 cookie 只在 IDaaS 自己域名下,每个子域首次访问都得跳一趟换票,最后还是要在应用域种点什么——等于变相回到本地会话,这个简化就不成立了。
  2. 校验方式是什么: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-signin 302 到 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 逗号分隔的角色(粗粒度)

配套防伪造三件套,缺一不可:

  1. NetworkPolicy / 服务暴露收口:后端只能从 ingress 进。这是根本防线——内网能直连 pod 的话,header 谁都能伪造;
  2. auth 服务永远回写这些头,空值也写:防止客户端自带的同名头原样透传到后端;
  3. 头带前缀,后端只信前缀:约定了 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,要自己起一个补位。三个前提先处理:

  1. hosts 把 dev 域名指到 127.0.0.1(cookie 跟域走,localhost 收不到父域 cookie);
  2. 本地要 HTTPS(SSO cookie 几乎必然 Secure),mkcert 签证书;
  3. 网络可达:给 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 前要问清的四件事

  1. 会话 cookie 的 domain、有效期、滑动续期条件;
  2. SDK 校验形态(本地验签还是内省)、内省接口 QPS 限制;
  3. 登录跳转 URL 支不支持 back/redirect 参数、有没有域名白名单;
  4. 用户属性(内省结果 / token claims)带不带角色或组织——决定 RBAC 数据源用 IDaaS 的组还是自己维护的权限表。

这四个答案拿到手,上面所有设计决策就都能定稿了。

参考