一个所有人都遇到过的场景
设想一个最常见的下单链路:
| |
订单服务要写 created_by,库存服务要校验"这个用户是不是有权操作这个仓库",优惠券服务要判断"这张券是不是这个用户的",积分服务在异步消费里要给"这个用户"加分。
四个服务,每一跳都要回答同一个问题:当前是谁在操作?
这件事看似简单,真做起来却处处是坑。下面我们就把"用户信息怎么在微服务之间传"这件事,从最朴素的做法一路讲到 Service Mesh,把每条路的优缺点摊开。
朴素做法:把用户信息当参数到处传
最直觉的做法——既然下游需要 userId,那就在接口签名里多加一个参数好了:
| |
第一眼看着没问题,真用起来你会发现:
- 侵入性极强:业务接口里混进了和业务无关的"操作人"字段,函数签名越来越长。
- 极易出错:调用方写错一个
userId,相当于"以 A 的身份替 B 下单",这是越权漏洞。 - 无法约束:下游服务无法判断这个
userId是真是假,调用方说是谁就是谁。 - 新增字段成本爆炸:今天加个
tenantId,明天加个traceId,所有接口都要改一轮。
所以这个方案在生产里几乎没人用。它给我们一个启示:用户上下文必须从业务参数里剥离出来,走专门的通道。
思路一:Token 全链路透传
既然用户登录后拿到了一个 Token(多数情况下是 JWT),最简单的"分布式做法"就是——把这个 Token 一路往下传,每个服务自己解析。
| |
它的好处很明显:
- 无状态:每个服务凭 Token 自证身份,不依赖中心节点。
- 解耦:服务之间不需要知道"上游是谁",只看 Token。
但你很快会撞到这些问题:
- 重复解析:链路上每个服务都要解一遍 JWT,验签一遍。链路越长,重复劳动越多。
- 密钥与公钥的分发:每个服务都得拿到验签公钥,密钥轮换时所有服务都要同步更新。
- Token 过期/撤销难:如果链路中段调用耗时较长,Token 在下游可能已经过期;用户登出后已经下发的 Token 也很难"召回"。
- 安全面被放大:原始 Token 流到了每一个内部服务,任何一个服务的日志泄漏,都等于泄漏了用户凭证。
- 服务间调用不天然带 Token:定时任务、MQ 消费、内部系统触发的调用,根本没有用户 Token,需要专门"造一个"。
所以纯粹的 Token 全链路透传,更适合小规模、链路短、对安全要求中等的场景,不适合作为大型系统的默认方案。
思路二(主线推荐):网关剥离 + 上下文透传
这套思路的核心一句话——鉴权"做一次",上下文"传一路"。
整体架构
| |
- 网关做唯一一次重活:验 Token、查黑名单、拉用户基本信息,然后把"必要的身份字段"以轻量请求头的形式写进下游请求。
- 下游服务不再信任原始 Token,只读那几个固定的请求头。
这个方案为什么好
- 重活只做一次:避免链路上重复解析。
- 下游零负担:业务服务不需要懂 JWT、不需要拿公钥,只关心"当前用户是谁"。
- 安全收敛:原始 Token 被拦在网关后面,不再扩散到内部。
- 上下文可扩展:今天传 userId,明天加个 tenantId、灰度标签、AB 实验分组——都只是多一个请求头的事。
一个绕不开的安全约束
下游服务"无脑信任"请求头,意味着——如果有人能绕过网关直接调用内部服务,他就能伪造任何身份。
所以这套方案有一个硬前提:
内部服务的入口必须封死,只接受来自网关或其他内部服务的流量。
实现手段可以是:内网网络隔离、Service Mesh 的 mTLS、内部调用专用的鉴权 Header(如服务间共享密钥),或者三者结合。这一步偷懒,整套体系就形同虚设。
服务内部:上下文如何"隐式"流转
网关把用户字段塞进了请求头,下游服务怎么用?最糟的写法是这样:
| |
每个业务方法都要从 request 里抠 header,每次调下游又要手动塞回去,这就是把"基础设施的事"塞进了"业务代码"。
正确的做法是建一个用户上下文容器,让请求头进来时自动入,调下游时自动出,业务代码完全无感。
接收侧:入口拦截器
在服务最外层装一个拦截器(不同框架叫法不同:Filter、Interceptor、Middleware),统一做一件事:
| |
业务代码里就只剩一句:
| |
调用侧:出站拦截器
服务调用其他服务时,在 HTTP 客户端的拦截器里把上下文塞回请求头:
| |
这样业务代码写 stockClient.deduct(skuId, count),框架自动把用户上下文带过去。
异步场景的坑
上下文容器一般基于"线程本地存储"实现。一旦发生线程切换,上下文就丢了:
- 把任务扔进线程池
- 用回调 / Future / 协程切换执行线程
- 定时任务由调度线程触发
解决思路是:在任务提交那一刻,把当前线程的上下文"拷贝一份"附在任务上;任务被新线程执行前,再把上下文恢复进去。Java 生态里阿里开源的 TransmittableThreadLocal(TTL)就是干这个的,关键是——包装一次线程池,全局生效,不要让业务代码自己去 copy。
| |
跨进程异步:消息队列怎么传?
HTTP 调用有请求头,消息队列也有"信封"——大多数 MQ 都支持消息 Header / Properties。把用户上下文放进去就行:
| |
同样,这件事应该做在 Producer/Consumer 的拦截器里,对业务代码透明。
一个被忽视的语义陷阱
异步场景里有个微妙的区别:操作发起人 vs 消费执行人。
举个例子:用户 A 下单后,订单服务发了一条消息,积分服务异步消费给 A 加分。
- 这条消息的"操作发起人"是用户 A——给 A 加分用的就是这个 ID。
- 但消息消费时,处理这条消息的"执行身份"可能是一个系统账号——发起后续调用(比如调风控)时,下游需要知道的是"系统在调",不是"A 在调"。
所以在比较复杂的系统里,消息上下文里通常要分清两类字段:
- 业务用户:
X-User-Id,描述"这件事跟谁有关" - 执行主体:
X-Actor-Id,描述"现在是谁在干"
两者不要混。
进阶视角:Service Mesh 方案
把视角再拔高一层——身份认证、上下文注入这些事,到底应该写在业务进程里,还是放到基础设施里?
Service Mesh(Istio 是典型代表)给了第三种答案:让 Sidecar 来管。
| |
每个服务旁边带一个 Sidecar 代理,所有出入流量都被它劫持。
- 进入服务前:Sidecar 校验 mTLS 证书、解析 JWT、把身份字段以请求头注入
- 离开服务时:Sidecar 自动签发 mTLS、附带服务身份
业务进程里彻底不需要任何鉴权代码,连"网关解 Token"那一步都被下沉到了基础设施层。
它的代价也很现实:
- 每个 Pod 多一个代理进程,有性能与资源开销
- 调用链路上多一跳,问题排查复杂度陡增
- 需要团队具备 K8s + Mesh 的运维能力
适用边界大致是:服务数量到了几十上百,团队有专门的基础架构组,且对零信任、跨集群通信、灰度路由这类能力有强需求时,才值得引入。中小规模团队用"网关 + 上下文透传"已经足够好。
横向对比与选型建议
| 方案 | 改造成本 | 安全性 | 性能 | 适用阶段 |
|---|---|---|---|---|
| 朴素参数 | 低 | 极低 | 高 | 几乎不推荐 |
| Token 全链路 | 低 | 中 | 中 | 小型 / 探索期 |
| 网关 + 上下文透传 | 中 | 高 | 高 | 大多数公司主流选择 |
| Service Mesh | 高 | 极高 | 中低 | 大规模 / 多语言 / 强基建团队 |
给一个工程上的建议:
- 初创/小团队:直接做"网关解 Token + 请求头透传",把上下文工具类和拦截器封装好,业务代码无感即可。Mesh 不必上。
- 中型团队:在前者基础上,把内网入口封死(mTLS 或共享密钥),把 MQ 也接入同一套上下文透传,做到"链路里任何一跳都知道当前用户"。
- 大型/多语言团队:考虑把鉴权与上下文注入下沉到 Service Mesh,业务侧只读上下文,不再写任何鉴权代码。
小结
回到开头那个问题——微服务之间到底应如何传递用户信息?
一句话总结:
用户上下文应当像 traceId 一样,在框架层透明流转。业务代码只问"当前是谁",不关心"怎么传过来的"。
具体落到一套清单:
- 不要把用户信息混进业务参数,它应该走专门通道。
- 鉴权只在网关做一次,下游不重复解析 Token。
- 下游通过约定的请求头读取用户上下文,前提是内网入口必须封死。
- 服务内部用上下文容器 + 入口/出口拦截器,实现隐式流转。
- 异步场景(线程池、MQ)单独处理上下文复制,否则一定会丢。
- 规模到了,再考虑用 Service Mesh 把这件事彻底下沉。
把这条链条闭环建好,未来无论加多少个服务、加多少种身份字段,业务代码都不用动。这才是"基础设施服务于业务"应有的样子。