1527 字
约 5 分钟
28
Gateway + Filter + ThreadLocal:登录用户 ID 是如何在微服务中传递的

Gateway + Filter + ThreadLocal:登录用户 ID 是如何在微服务中传递的

在微服务项目中,很多业务都需要知道:

当前正在操作系统的用户是谁?

例如:

修改密码
退出登录
发布笔记
发表评论
修改个人信息

这些操作都需要拿到当前登录用户的 userId。

如果每个接口都手动获取、再一层层往下传,就会非常麻烦。

因此,可以通过:

Gateway + Header + Filter + ThreadLocal

实现登录用户 ID 的自动传递。


一、我们要解决什么问题?

Gateway 已经可以通过 Token 获取当前登录用户。

例如:

Token
  ↓
Gateway
  ↓
userId = 10001

然后 Gateway 会把用户 ID 放到请求头中:

userId: 10001

再把请求转发给下游服务。

但是问题来了:

下游服务每个接口都要这样获取:

@RequestHeader("userId")

如果几十个接口都需要当前用户 ID,就需要写几十遍。

而且 Controller 获取之后,还可能继续传给 Service:

Controller(userId)
      ↓
Service(userId)
      ↓
其他业务方法(userId)

代码会越来越繁琐。

这也是引入 Filter + ThreadLocal 的原因:让当前登录用户 ID 在服务内部可以直接获取,而不用层层传参。


二、整体解决思路

整个流程可以分成两段。

第一段是服务之间传递用户 ID:

Gateway
   ↓
Header
   ↓
下游服务

第二段是下游服务内部传递用户 ID:

Filter
   ↓
ThreadLocal
   ↓
Controller
   ↓
Service

所以可以简单记住:

Header 负责跨服务传递,ThreadLocal 负责服务内部传递。


三、Gateway 负责传递 userId

用户请求首先携带 Token:

Authorization: Bearer xxx

Gateway 通过 Sa-Token 获取当前登录用户:

userId = 10001

然后在转发请求时加入:

userId: 10001

最终请求进入下游服务。

这一步解决的是:

Gateway 已经知道当前用户是谁,怎么把这个信息告诉后面的微服务?

答案就是通过 HTTP Header 透传。


四、Filter 自动获取 userId

请求进入下游服务以后,会先经过 Filter。

Filter 自动读取:

Header
 ↓
userId = 10001

这样就不需要每个 Controller 自己去获取请求头了。

文章中创建的 HeaderUserId2ContextFilter 就负责从 Header 中读取 userId。

但是 Filter 拿到 userId 之后,还需要让后面的业务代码能够方便使用。

这时候就需要 ThreadLocal。


五、ThreadLocal 保存当前用户

可以把 ThreadLocal 简单理解成:

当前请求线程专属的临时存储空间。

例如张三的请求由线程 A 处理:

线程 A
└── userId = 10001

李四的请求由线程 B 处理:

线程 B
└── userId = 20002

两个线程之间的数据互不影响。

Filter 获取到 userId 后,就把它存进去:

Header
 ↓
userId = 10001
 ↓
Filter
 ↓
ThreadLocal
 ↓
userId = 10001

文章中通过 LoginUserContextHolder 对 ThreadLocal 进行了统一封装,用来设置、获取以及删除当前用户 ID。


六、业务代码直接获取 userId

有了 ThreadLocal 后,Controller 就不需要这样:

Controller 获取 userId
       ↓
再传给 Service

而是:

Controller
   ↓
Service
   ↓
直接从 ThreadLocal 获取 userId

例如退出登录业务:

Service
   ↓
LoginUserContextHolder.getUserId()
   ↓
10001
   ↓
执行用户 10001 的退出登录

因此 Controller 可以保持非常干净,只负责调用业务层。

文章最终也是把 /logout 中的 @RequestHeader 删除,然后在 Service 中直接通过 LoginUserContextHolder.getUserId() 获取当前用户。


七、完整业务流程

假设用户 10001 请求退出登录。

第一步,客户端携带 Token:

Authorization: Bearer abc123

第二步,请求进入 Gateway:

客户端
   ↓
Gateway

Gateway 通过 Token 获取:

userId = 10001

第三步,Gateway 把 userId 放到 Header:

userId: 10001

第四步,请求进入认证服务:

Gateway
   ↓
认证服务 Filter

Filter 获取:

userId = 10001

然后存入:

ThreadLocal
└── userId = 10001

第五步,请求继续执行:

Controller
   ↓
Service

Service 需要当前用户时直接:

LoginUserContextHolder.getUserId()

得到:

10001

于是就可以继续完成退出登录、修改密码等业务。


八、为什么请求结束后必须删除?

ThreadLocal 中的数据不能一直保留。

因为服务器线程通常会被重复利用。

例如:

线程 A
 ↓
第一次处理张三请求
 ↓
userId = 10001

请求结束以后,线程 A 后面可能继续处理李四的请求。

所以当前请求结束以后,需要把 ThreadLocal 中的数据删除:

请求开始
 ↓
设置 userId
 ↓
执行业务
 ↓
请求结束
 ↓
remove userId

因此通常会把清理操作放到 finally 中,确保无论业务是否出现异常,都能清除当前线程的数据。文章同样在过滤器执行结束时调用了 remove()。


九、总结

整个设计最终形成这样一条链路:

客户端
  ↓
Token
  ↓
Gateway
  ↓
解析 userId
  ↓
Header
  ↓
下游服务 Filter
  ↓
ThreadLocal
  ↓
Controller
  ↓
Service
  ↓
获取当前登录用户 ID

可以简单总结为:

Gateway 负责识别用户,Header 负责跨服务传递用户 ID,Filter 负责接收用户 ID,ThreadLocal 负责让当前服务中的业务代码方便获取用户 ID。

这样就不需要在每个 Controller 中重复获取 userId,也不需要把用户 ID 一层一层传递到 Service 中。

不过 ThreadLocal 主要适合当前线程内的数据传递,如果业务切换到了异步线程,普通 ThreadLocal 还会遇到上下文无法自动传递的问题,这也是后续需要进一步解决的地方。

Gateway + Filter + ThreadLocal:登录用户 ID 是如何在微服务中传递的
https://clxhxhhr.top/posts/2940/
作者
clxstart
发布于
2026-09-20
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。