一句话速览:本项目的鉴权架构是”网关一处验签、下游全部信任 Header”——这个模式用”业务服务零鉴权代码”换来了”直连业务端口这条路在语义上不成立”的硬约束;JWT 本身用签名换无状态,代价是无法主动失效,踢人/续期都要额外的机制来补。

涉及原始记录:07-面试问题/验证问题/01-业务逻辑问题.md P0-01;
07-面试问题/验证问题/02-数据与环境问题.md P1-02、P1-03;
补充内容:CI/CD 基础概念(阶段12引入,本项目未做 CD)

最近修订:2026-08-07

目录


技术点 1:网关统一鉴权 + Header 透传模式

难度:⭐⭐ | 掌握要求:能画出透传链路、说清直连端口为什么必然拿不到用户

核心概念

本项目的鉴权架构是一种常见的微服务模式:只在网关这一层做 JWT 验签,验签通过后把解析出的用户信息通过请求 Header 透传给下游服务,下游业务服务本身不再重复验签,只信任 Header 里的内容

1
2
3
4
5
6
客户端请求(带 JWT token)
→ Gateway:JwtAuthGlobalFilter 验签 token
→ 验签通过:把 userId/role 写入请求 Header(X-User-Id / X-User-Role)
→ 转发给业务服务
→ 业务服务:UserContextInterceptor 只读 Header,不解析 JWT
→ 填充 UserContextHolder,业务代码通过它拿到当前用户

这个模式的核心假设是:业务服务的入口只能是网关,一旦这个假设被打破(比如测试时直连业务端口),下游的 UserContextHolder 就会拿到空值——因为业务服务的拦截器压根不会去解析请求里的 JWT token,它只认 Header。

我们踩过的坑

这个模式带来的”副作用”在本项目里以两种不同的表现形式反复出现,值得放在一起理解成同一个问题:

验证问题 P0-01:直连业务服务导致 tenant_id 为空,进而影响 Seata 回滚

验证脚本为了图方便,直接调用了 lease 服务的内部接口(绕过 Gateway),UserContextHolder.getUserId() 返回 null,导致创建的租约记录 tenant_id 字段是空的,后续再牵连到 Seata 回滚异常(详见 02-分布式事务与消息队列.md)。表面上看是一个 Seata 问题,根因其实是验证方式违反了架构假设。

数据与环境问题 P1-02:直连业务端口返回”请先登录”

同样是直连业务服务的表现,这次更直白:接口直接返回 10003”请先登录”的业务错误码,即使请求明明带了有效的 token。原因跟上面完全一致——业务服务的拦截器根本不解析 Header 之外的任何东西:

1
2
3
4
5
6
7
8
9
public class UserContextInterceptor implements HandlerInterceptor {
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String userId = request.getHeader("X-User-Id"); // 只认这个 Header
if (userId != null) {
UserContextHolder.setUserId(Long.valueOf(userId));
}
return true; // 即使 userId 是 null,也会放行到 Controller,让业务代码自己判断未登录
}
}

解决方式是建立一条硬性验证规范:所有需要鉴权的验证请求,必须统一通过 Gateway 端口(8080)访问 /api/** 路径,不允许为了”方便调试”直连业务服务端口

必须记住的结论

  • “网关统一鉴权、业务服务信任 Header”这个架构模式的代价是:业务服务自己完全丧失了独立验证请求合法性的能力。这不是缺陷,是这个架构模式本身的设计取舍(换来的是业务服务不用各自实现一遍 JWT 验签逻辑),但意味着这个架构下”直连业务服务”这条路径在语义上是不完整的、不能代表真实的请求处理链路,任何验证、调试都必须走网关。
  • 排查”带了 token 但还是提示未登录/用户信息为空”类问题时,第一步永远是确认请求到底有没有经过网关,而不是去查 JWT 本身的签名、有效期这些。
  • 这类架构模式的选择也解释了为什么 WebSocket(IM 模块)不能照搬同样的方式——WebSocket 是直连独立端口、不经过 Gateway 的,所以它必须自己具备完整的 JWT 验签能力,这是两种架构模式的分界点(详见 05-网络通信Netty.md 的鉴权部分)。
  • 安全视角的提醒(本项目单机部署暂未暴露,但生产必须考虑):”信任 Header”的前提是外部流量无法绕过网关直达业务服务。生产环境必须在网络层(安全组/内网隔离/服务端口不对外暴露)保证这一点,否则任何人都可以自己伪造一个 X-User-Id 头直连业务服务冒充任意用户。

技术点 2:JWT(JSON Web Token)基础

难度:⭐⭐ | 掌握要求:三段结构、HS256 vs RS256、无状态的优缺点

核心概念

JWT 是一种自包含的身份令牌,由三部分组成(header.payload.signature),服务端不需要为每个登录用户保存 session 状态——校验的时候只需要验证签名是否合法(用密钥/公钥验签),再解析出 payload 里携带的用户信息(userId、role、过期时间等)即可,这也是为什么 JWT 特别适合分布式/微服务架构:任何一个持有验签密钥的服务,都能独立完成校验,不需要跟一个中心化的 session 存储通信。

原理深挖:三段结构各是什么

1
2
3
4
5
eyJhbGciOiJSUzI1NiJ9  .  eyJ1c2VySWQiOjEsImV4cCI6MTc1NDU2fQ  .  X9v...签名
↓ ↓ ↓
Header Payload Signature
{"alg":"RS256"} {"userId":1,"exp":...} 用私钥对前两段签名
Base64URL 编码 Base64URL 编码 Base64URL 编码

两个最容易被误解的点:

  • Base64URL 是编码不是加密:payload 部分任何人都能解码出明文(在线工具一秒解),所以 JWT 里绝对不能放密码、身份证号等敏感信息。JWT 保证的是”不可篡改”(改了签名就对不上),不是”不可见”。
  • 签名验签的过程:签发方对 header + "." + payload 用私钥(RS256)或共享密钥(HS256)算出签名;验签方用公钥/同一密钥重新算一遍对比。任何对 payload 的篡改都会导致签名不匹配。

对称 HS256 vs 非对称 RS256

HS256(对称) RS256(非对称,本项目选择)
密钥 同一个密钥既签发又验签 私钥签发、公钥验签
微服务适配性 差:每个要验签的服务都得持有”能伪造 token 的密钥”,一个服务泄漏全盘皆输 好:验签服务只持有公钥,公钥泄漏也无法伪造 token
性能 慢一些(RSA 运算贵),可忽略

本项目用的是非对称签名(RSA):Auth 服务持有私钥,签发 token 时用私钥签名;其他所有需要验签的地方(Gateway、以及独立于网关之外的 IM 服务)只需要持有公钥即可完成验签,不需要也不应该持有私钥。这个设计的好处是即使某个下游服务的公钥配置泄漏,也不可能被用来伪造 token(伪造需要私钥)。

我们踩过的坑

参见 05-网络通信Netty.md 里 IM 服务忘记配置 JWT 公钥导致鉴权必然失败的案例——这个坑本质上属于”JWT 验签所需的密钥材料没有正确分发到需要它的服务”这一类问题。

必须记住的结论

  • 区分清楚”谁需要私钥、谁只需要公钥”:只有签发 token 的服务(本项目是 auth 服务)需要私钥,所有其他做验签的地方只需要公钥。新增一个需要独立验签能力的服务时,只需要同步公钥配置,不要图省事把私钥也一起同步过去(会扩大私钥泄漏的风险面)。
  • JWT 本身不提供”主动失效”能力(一旦签发,在过期之前始终有效),如果业务上需要强制下线/踢人,需要额外的机制(比如本项目 IM 模块的”同用户多端登录踢旧连接”就是应用层维护会话表实现的,不是依赖 JWT 本身)。
  • payload 是明文编码不是加密,敏感信息绝不进 JWT;签名保证的是”不可篡改”,不是”不可见”。

技术点 3:JWT 的续期与主动失效 —— 无状态的两块补丁

难度:⭐⭐ | 掌握要求:refresh token 双 token 机制、黑名单/版本号两种失效方案

JWT 无状态设计带来两个天然的”功能缺口”,是面试必考的延伸问题,本项目虽未实现但方案要能讲清:

缺口一:token 过期了怎么办——双 Token 续期

access token 有效期设短了用户体验差(频繁重新登录),设长了泄漏风险大。标准解法是双 Token

sequenceDiagram
    participant C as 客户端
    participant A as Auth服务
    participant R as 业务链路

    C->>A: 登录
    A-->>C: accessToken(2小时) + refreshToken(7天, 存服务端/Redis)
    C->>R: 用 accessToken 调业务接口
    Note over C,R: 2小时后 accessToken 过期
    C->>A: 用 refreshToken 换新 accessToken
    A-->>C: 新 accessToken(无感续期,不用重新登录)

要点:accessToken 无状态验签(快),refreshToken 有状态存服务端(可控制)——吊销 refreshToken 就能阻止续期,等现有 accessToken 自然过期即完成下线。这是”无状态性能”与”可控制性”的折中。

缺口二:怎么让没过期的 token 立刻失效——两种方案

  • 黑名单方案:把被吊销的 token(或其 jti 唯一标识)存 Redis,TTL 设为 token 剩余有效期;验签时先查黑名单。缺点:每次验签多一次 Redis 查询,部分牺牲了无状态性
  • 用户版本号方案(更常用):用户表维护一个 tokenVersion 字段,签发时写进 payload;踢人/改密时把 tokenVersion +1,验签时对比 token 里的版本号和当前版本号,不一致即拒绝。好处是一次操作吊销该用户所有已签发 token,且版本号可以缓存,开销比黑名单小。

本项目 IM 模块的”多端登录踢旧连接”走的是另一条路——不吊销 JWT,而是在应用层会话表里让新连接顶掉旧 Channel(详见 05-网络通信Netty.md),这说明”登录态控制”和”JWT 失效”是两个可以分开解决的问题。


技术点 4:CI/CD 基础概念(了解即可,本项目未落地 CD)

难度:⭐ | 掌握要求:CI 与 CD 的区别、本项目”只做 CI”的判断逻辑

这部分是纯概念性学习内容,本项目已经搭建了 CI(见 .github/workflows/ci.yml),但结合项目实际情况(本地 2核2G 服务器资源无法承载 11 微服务+8 中间件的完整技术栈)判断不做自动化部署,只需要理解概念、知道行业里通常怎么做即可。

核心概念

  • CI(持续集成,Continuous Integration):代码提交后自动触发一套流水线,做编译、跑测试、代码质量扫描,尽早发现”这次改动把项目弄坏了”,而不是等到人工发现或者部署时才暴露。
  • CD(持续交付/持续部署,Continuous Delivery / Deployment):CI 通过之后,自动把构建产物发布出去。”持续交付”是打包好、但上线需要人工点一下确认;”持续部署”是全自动,测试通过就直接上线,中间没有人工审批环节。这两者的区别就在于”最后一步要不要人工确认”。
  • 常见工具:GitHub Actions、GitLab CI、Jenkins。核心工作模式都是相同的——监听代码仓库事件(push/PR),按照一份 YAML/Groovy 定义的流水线脚本,在一个临时的、全新的 runner 环境里依次执行各个步骤。

本项目的技术选型判断(为什么只做 CI 不做 CD)

这是一次真实的、基于资源约束做的架构判断,值得记住判断过程而不只是结论:

  1. 项目由 11 个自研微服务 + 8+ 个中间件(Nacos/RocketMQ/Seata/ES/Redis/MySQL/MongoDB/MinIO)组成,粗略估算内存需求:光中间件就需要 3~4G,11 个 Java 微服务哪怕每个只给 256M 堆内存也要 2.7G+,合计轻松超过 8G。
  2. 现有可用于部署的服务器只有 2核2G,连单独跑好 ES 一个组件都紧张,完全不可能承载完整技术栈——不是”能不能优化”的问题,是内存总量的硬约束。
  3. GitHub Actions 的 runner 是每次全新创建的临时容器,即使强行想在流水线里做自动部署,也没有一个现实存在的、能承载这套技术栈的目标环境可以部署过去。
  4. 结论:CI(编译/打包门禁)依然有实际价值,能防止把多模块聚合顺序、依赖声明这类低级错误合入主分支;但 CD 环节因为没有可行的部署目标,本阶段不实施,属于”评估后主动不做”,而不是”不会做”。

必须记住的结论

  • CI 和 CD 是两个独立的、可以分开决策是否要做的环节,不是绑定的一个整体。”做了 CI”不代表一定要”做 CD”,两者的价值和成本是分开评估的。
  • 判断要不要上某个工程化设施(这里是 CD),核心问题永远是”这个东西的价值能不能落地”,而不是”这个技术流不流行”。没有可部署的目标环境时,强行搭建自动部署流水线只是空转,没有实际产出。
  • 如果未来有了更充足的服务器资源,把 CD 补上的思路应该是:先从”只部署 1~2 个核心服务做演示”这种小范围场景切入验证流程通不通,而不是一上来就追求”全部 11 个服务自动部署”,这跟本项目其他很多问题的教训是一致的——先保证核心链路正确可信,再谈规模化。

术语表

术语 含义
Header 透传模式 网关验签后把用户信息写入请求头,下游服务只读头不验签的架构模式
Base64URL JWT 三段用的编码方式——是编码不是加密,payload 可被任何人解码
HS256 / RS256 对称签名(共享密钥)/ 非对称签名(私钥签、公钥验)算法
accessToken / refreshToken 双 Token 机制:短效业务令牌 + 长效可吊销的续期令牌
黑名单 / tokenVersion 两种主动失效方案:吊销列表逐个封禁 / 用户版本号一次全废
白名单 网关鉴权中显式放行、不做登录校验的路径清单
CI / CD 持续集成(自动编译测试门禁)/ 持续交付与持续部署(自动发布)
runner CI 工具中执行流水线的临时干净环境