Spring Cloud 微服务中的 JWT 上下文传递完整链路实践

引言

在 Spring Cloud 微服务架构中,认证与用户信息的传递是一个高频问题:

  • JWT 应该在哪一层解析?
  • 用户 ID、角色、租户信息如何在服务间传递?
  • 为什么不能使用 Request Attribute?
  • Feign 调用为什么会丢失用户上下文?
  • ThreadLocal 在整个链路中扮演什么角色?

本文围绕“JWT 用户上下文传播”这一主线,梳理 Spring Cloud 项目中的完整实现方案。


一、整体链路设计

典型调用链路:

Client
  ↓
Gateway
  ↓
User Service
  ↓
Order Service
  ↓
Product Service

完整上下文传播流程:

客户端
  ↓ JWT
Gateway
  ↓
解析 JWT
  ↓
写入 Header
  ↓
Service A
  ↓
MVC Interceptor
  ↓
ThreadLocal(UserContext)
  ↓
业务代码
  ↓
Feign Interceptor
  ↓
Header
  ↓
Service B

核心思想:

  • 跨服务传递 → Header
  • 服务内共享 → ThreadLocal

二、为什么选择 Header 而不是 Attribute

很多人最开始会想到:

request.setAttribute("userId", 1001L);

然后:

Long userId = (Long) request.getAttribute("userId");

这种方式只能在当前 JVM 内部有效。

原因是:

request.setAttribute(...)

本质只是给当前 HttpServletRequest 对象增加一个属性。

它不会进入:

  • HTTP Header
  • HTTP Body
  • URL
  • Cookie

因此:

Service A
   ↓ Feign
Service B

当 Feign 发起新的 HTTP 请求时,Attribute 会完全丢失。

所以:

  • Attribute → 服务内共享
  • Header → 服务间传播

三、JWT 应该在哪一层解析

方案一:每个服务都解析 JWT

Gateway
   ↓
UserService解析JWT
   ↓
OrderService解析JWT
   ↓
ProductService解析JWT

缺点:

  • 验签重复
  • 代码重复
  • 权限逻辑分散

方案二:Gateway 统一解析(推荐)

Client
  ↓ JWT
Gateway
  ↓
校验JWT
解析JWT
  ↓
Header
  ↓
业务服务

优点:

  • 统一认证
  • 减少重复计算
  • 权限逻辑集中管理

四、Gateway:JWT → Header

4.1 解析 JWT

@Component
public class AuthGlobalFilter implements GlobalFilter {

    @Override
    public Mono<Void> filter(
            ServerWebExchange exchange,
            GatewayFilterChain chain) {

        String token = exchange.getRequest()
                .getHeaders()
                .getFirst("Authorization");

        Long userId = JwtUtil.getUserId(token);
        String role = JwtUtil.getRole(token);

        ServerHttpRequest request = exchange.getRequest()
                .mutate()
                .header("X-User-Id", String.valueOf(userId))
                .header("X-Role", role)
                .build();

        return chain.filter(
                exchange.mutate()
                        .request(request)
                        .build());
    }
}

4.2 防止 Header 伪造

客户端可能主动传:

X-User-Id: 1
X-Role: ADMIN

因此必须先删除再写入:

.headers(headers -> {
    headers.remove("X-User-Id");
    headers.remove("X-Role");
})

原则:

外部 Header 不可信,JWT 解析结果可信。

同时,业务服务不应直接暴露公网,只允许 Gateway 调用。


五、服务内上下文管理

5.1 获取 Header

Controller 中可以直接读取:

@GetMapping("/orders")
public String list(
        @RequestHeader("X-User-Id")
        Long userId) {

    return userId.toString();
}

或者:

@GetMapping("/orders")
public String list(HttpServletRequest request) {

    return request.getHeader("X-User-Id");
}

5.2 为什么需要 ThreadLocal

如果业务代码频繁依赖:

request.getHeader(...)

会导致业务层与 Web 层耦合。

更好的设计:

Header
 ↓
Interceptor
 ↓
ThreadLocal
 ↓
Business

5.3 UserContext

public class UserContext {

    private static final ThreadLocal<UserInfo>
            LOCAL = new ThreadLocal<>();

    public static void set(UserInfo user) {
        LOCAL.set(user);
    }

    public static UserInfo get() {
        return LOCAL.get();
    }

    public static Long getUserId() {
        return LOCAL.get().getUserId();
    }

    public static void clear() {
        LOCAL.remove();
    }
}

5.4 MVC Interceptor

@Component
public class UserContextInterceptor
        implements HandlerInterceptor {

    @Override
    public boolean preHandle(
            HttpServletRequest request,
            HttpServletResponse response,
            Object handler) {

        UserContext.set(
                new UserInfo(
                        Long.valueOf(
                            request.getHeader("X-User-Id")),
                        request.getHeader("X-Role"),
                        null));

        return true;
    }

    @Override
    public void afterCompletion(
            HttpServletRequest request,
            HttpServletResponse response,
            Object handler,
            Exception ex) {

        UserContext.clear();
    }
}

5.5 注册拦截器

@Configuration
public class WebMvcConfig
        implements WebMvcConfigurer {

    @Override
    public void addInterceptors(
            InterceptorRegistry registry) {

        registry.addInterceptor(
                new UserContextInterceptor());
    }
}

六、Feign:继续传播上下文

为什么需要 Feign 拦截器

当 Service A 调用 Service B 时:

Service A
   ↓
Feign
   ↓
Service B

Feign 会创建新的 HTTP 请求。

此时必须把当前用户上下文重新写入 Header。

Feign RequestInterceptor

@Configuration
public class FeignConfig {

    @Bean
    public RequestInterceptor
    userContextInterceptor() {

        return template -> {

            UserInfo user = UserContext.get();

            if (user == null) {
                return;
            }

            template.header(
                    "X-User-Id",
                    String.valueOf(
                            user.getUserId()));

            template.header(
                    "X-Role",
                    user.getRole());
        };
    }
}

这样:

orderFeignClient.list();

自动变成:

GET /orders

X-User-Id: 1001
X-Role: ADMIN

MVC Interceptor 与 Feign Interceptor 的职责

MVC Interceptor:

Header
 ↓
ThreadLocal

Feign Interceptor:

ThreadLocal
 ↓
Header

一个负责接收上下文,一个负责传播上下文。


七、生产环境中的关键问题

ThreadLocal 必须清理

请求结束后必须:

UserContext.clear();

否则线程池复用时可能出现用户串号问题。

异步线程上下文丢失

以下场景不会自动继承 ThreadLocal:

@Async
CompletableFuture
线程池
MQ消费者
定时任务

如果异步逻辑需要用户信息,需要额外做上下文传递。

Header 内容控制

推荐传递:

X-User-Id
X-Role
X-Tenant-Id
X-Trace-Id

不推荐传递:

密码
手机号
身份证号
完整用户对象

Header 应仅承载必要上下文。

用户上下文与链路上下文分离

通常存在两类上下文:

用户上下文:

UserId
Role
TenantId

链路上下文:

TraceId
SpanId
RequestId

前者用于鉴权,后者用于日志追踪。


总结

Spring Cloud 中最常见的 JWT 上下文传播方案可以概括为:

Gateway
  ↓ JWT解析
Header
  ↓
MVC Interceptor
  ↓
ThreadLocal(UserContext)
  ↓
Business
  ↓
Feign Interceptor
  ↓
Header
  ↓
下游服务

职责划分如下:

组件职责
GatewayJWT 校验与解析
Header跨服务传播上下文
MVC InterceptorHeader → ThreadLocal
UserContext服务内共享用户信息
Feign InterceptorThreadLocal → Header
Business消费用户上下文

一句话总结:

Gateway 负责解析 JWT,Header 负责跨服务传播,ThreadLocal 负责服务内共享,Feign Interceptor 负责继续传播上下文,共同构成完整的微服务认证链路。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇