1666 字
约 5 分钟
5
S04-J12 · 微服务请求链路:Gateway 透传、ThreadLocal 与鉴权

适用读者:从单体转到微服务,被 Controller/Service/RPC/Feign/Gateway/ThreadLocal 绕晕的人。
前置:S04(Spring Boot 基础)、S07(Spring 框架)、S17-J03(可靠投递的幂等思想)。
覆盖旧文:2944(从单体到微服务:Controller/Service/RPC/Feign/Mapper 该怎么写)、2940(Gateway + Filter + ThreadLocal 传 userId)、2941(TransmittableThreadLocal 解决异步丢上下文)、2939(Gateway 如何把 userId 透传给下游)、2938(Gateway + Sa-Token 鉴权完整流程)、2937(Sa-Token 自定义 Token 格式:为什么用 Authorization: Bearer)、2943(引入 OpenFeign 组件)→ 7 篇 / 26,706 字符。
扩展点:E-S04-039(微服务唯一变化:进程内调用变网络调用)、E-S04-040(身份透传三段链路与 TTL 的边界)、E-S04-041(鉴权放哪里:Gateway 粗判 + 服务内细判)。
下一篇:S04-X03 · Nacos 与杂项登记。


1. 一句话结论

微服务没有推翻你的 Spring Boot 写法,只改了一件事:原来一个 JVM 里的方法调用,现在变成了跨进程的网络调用。由此派生出三件事必须显式处理:身份怎么带过去、上下文怎么不丢、鉴权在哪一层做。

出处:A-2944、A-2940、A-2938。


2. 分层怎么写(E-S04-039)

单体:请求 → Controller → Service → Mapper → MySQL   (同进程,Service 之间直接方法调用)
微服务:请求 → Gateway → Service A →(RPC/Feign)→ Service B → Mapper → 各自 DB
概念 单体里的样子 微服务里的样子
Controller 接 HTTP 不变(但对内可能接 RPC)
Service 调别的 Service 方法 调别的服务的接口(网络、可能失败、有超时)
Mapper 同库 只操作本服务自己的库
RPC / Feign 不存在 声明式远程调用(Feign 是"长得像本地方法"的 HTTP 客户端)
API 模块 / BIZ 模块 不存在 API 放对外契约(DTO/接口),BIZ 放实现

判据:"跨服务的调用必须当成会失败的 I/O 对待"——超时、重试、降级、幂等,一个都不能省。同库跨服务直接连库是分库失败的开始。

Feign 的三个必设项:

// 环境:Spring Cloud OpenFeign 4.x / Spring Boot 3.x
@FeignClient(name = "user-service", configuration = FeignTimeoutConfig.class)
public interface UserClient {
    @GetMapping("/internal/users/{id}")
    UserDTO get(@PathVariable("id") Long id);
}
// 必设:connectTimeout / readTimeout / 重试(幂等方法才可重试)/ 降级(fallback)

判据:无关超时设置的 Feign 客户端 = 线程池泄漏源(默认超时可能长达数十秒)。


3. 身份透传三段链路(E-S04-040)

① Gateway:校验 Token → 解析 userId → 写入请求头 userId: 10001
② 下游服务:Filter 读头 → 存进 ThreadLocal → 业务层直接取
③ 服务间再调用:Feign 拦截器把 ThreadLocal 里的 userId 再写进新请求头
// ② 入口 Filter
public class UserContextFilter implements Filter {
    @Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        String uid = ((HttpServletRequest) req).getHeader("userId");
        try {
            UserContext.set(uid == null ? null : Long.valueOf(uid));
            chain.doFilter(req, res);
        } finally {
            UserContext.clear();          // 必须清理,否则线程复用会串号
        }
    }
}

为什么必须 clear():容器线程池会复用线程,不清理就会把 A 用户的 id 带给 B 用户——这是最危险的串号 bug,且只在并发下偶发。

异步场景的边界(TTL):ThreadLocal 只在当前线程有效;CompletableFuture / @Async / 线程池会丢失上下文。TransmittableThreadLocal(TTL)配合 TtlExecutors 包装线程池可传递,但代价是必须包装每一个线程池——漏一个就丢。

方案 覆盖范围 代价
ThreadLocal + Filter 同步链路 异步丢
TTL + 包装线程池 异步链路 每个池都要包装
显式传参 全部 侵入但最可靠

判据:"上下文传递的复杂度与线程池数量成正比"——线程池超过 3 个时,显式传参反而更省心。


4. 鉴权放哪一层(E-S04-041)

Gateway(粗判):Token 是否有效、是否过期、路由是否有权限 → 拒绝无效请求,减轻后端压力
服务内(细判):这个用户能不能操作这条数据(数据级权限)→ 必须做,不能只靠网关

Sa-Token 的两个实践点(2937/2938):

点 说明
Authorization: Bearer <token> 标准语义:scheme + 凭证;不要用自定义头(网关/代理/日志工具会丢)
网关只做 checkLogin 数据级鉴权留在服务内,用 SaCheckPermission + 资源归属校验

判据:"网关是筛子,不是保险柜"——越过网关的内部调用(服务间 RPC、定时任务)不受网关保护,所以服务内校验不能省。


5. 反例:八种"链路串号与越权"

做法 后果
ThreadLocal 不 clear() 线程复用导致跨用户串号
信任前端传来的 userId 头 直接伪造身份
网关鉴权后就信任所有内部调用 内部 RPC 绕过鉴权
Feign 不设超时 线程池耗尽,服务雪崩
重试非幂等接口 重复下单/重复扣款
跨服务直连别人的库 表结构耦合,无法独立演进
用自定义头传 Token 代理/网关剥离,偶发 401
异步任务用裸 ThreadLocal 上下文丢失,写日志无 userId

6. 自测

  1. 微服务相对单体"唯一的变化"是什么?由此派生哪三件事?
  2. 身份透传的三段链路?TTL 的代价是什么?
  3. 为什么 ThreadLocal 必须 clear?什么场景下会串号?
  4. 鉴权为什么不能只在 Gateway 做?
  5. 为什么 Token 用 Authorization: Bearer?
答案
  1. 进程内调用变网络调用;身份传递、上下文不丢、鉴权分层。
  2. 网关写头、Filter 存 ThreadLocal、Feign 拦截器再写头;每个线程池都要包装。
  3. 容器线程池复用线程,残留值会给下一个请求。
  4. 内部 RPC/定时任务绕过网关;数据级权限必须在服务内判。
  5. 标准语义明确、代理与网关默认识别、日志脱敏工具可识别。

7. 下一篇

S04-X03 · Nacos 与杂项登记 —— Nacos 五问(是什么/服务注册/Namespace/动态加载 Bean)、策略+工厂的文件上传、Java 网络编程与多线程双份重复的处置。


本篇属于系列《S04 · Java 与微服务基础》。旧文来源:A-2944/2940/2941/2939/2938/2937/2943;扩展点 E-S04-039/040/041。Sa-Token / Nacos / OpenFeign 版本细节按官方文档核对(待核)。

S04-J12 · 微服务请求链路:Gateway 透传、ThreadLocal 与鉴权
https://www.clxhxhhr.top/posts/5235/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。