Gateway网关将登录用户信息传递给下游微服务(完整实现方案)
-
- 一、核心实现流程
- 二、前提准备(规范Token传递)
- 三、网关层实现(核心:全局过滤器)
-
- [3.1 网关依赖(确保已引入,Spring Cloud Gateway)](#3.1 网关依赖(确保已引入,Spring Cloud Gateway))
- [3.2 核心:全局过滤器实现(二选一:JWT解析 / Feign调用用户中心)](#3.2 核心:全局过滤器实现(二选一:JWT解析 / Feign调用用户中心))
- [3.3 网关跨域配置兼容(关键!避免请求头丢失)](#3.3 网关跨域配置兼容(关键!避免请求头丢失))
- 四、下游微服务实现(统一读取用户信息)
-
- [4.1 微服务通用依赖(确保已引入)](#4.1 微服务通用依赖(确保已引入))
- [4.2 步骤1:定义用户信息实体类(与网关传递的字段一致)](#4.2 步骤1:定义用户信息实体类(与网关传递的字段一致))
- [4.3 步骤2:定义全局用户上下文(ThreadLocal存储,保证线程安全)](#4.3 步骤2:定义全局用户上下文(ThreadLocal存储,保证线程安全))
- [4.4 步骤3:实现拦截器,解析请求头并设置用户上下文](#4.4 步骤3:实现拦截器,解析请求头并设置用户上下文)
- [4.5 步骤4:注册拦截器,使其全局生效](#4.5 步骤4:注册拦截器,使其全局生效)
- [4.6 步骤5:业务层直接使用用户信息(无侵入,极简)](#4.6 步骤5:业务层直接使用用户信息(无侵入,极简))
- 五、关键优化与避坑点
-
- [5.1 网关过滤器执行顺序(必对!)](#5.1 网关过滤器执行顺序(必对!))
- [5.2 防止请求头中文/特殊字符乱码](#5.2 防止请求头中文/特殊字符乱码)
- [5.3 微服务ThreadLocal内存泄漏防护](#5.3 微服务ThreadLocal内存泄漏防护)
- [5.4 放行无需登录的接口(网关层必做!)](#5.4 放行无需登录的接口(网关层必做!))
- [5.5 生产环境JWT密钥安全](#5.5 生产环境JWT密钥安全)
- [5.6 下游微服务无需再做登录校验](#5.6 下游微服务无需再做登录校验)
- 六、拓展场景:微服务之间调用传递用户信息
- 七、核心总结
Gateway网关作为请求入口,传递登录用户信息的核心思路 是:网关层统一解析登录凭证(Token)→ 校验并获取完整用户信息 → 将用户信息写入HTTP请求头 → 下游微服务从请求头中读取并使用 ,全程保证请求链路的用户信息一致性,适配JWT、自定义Token等主流登录方式,以下是可直接落地的完整实现方案(含网关配置、代码实现、微服务接收、全局异常处理)。
一、核心实现流程
-
前端请求 :携带登录凭证(如
token,放在请求头Authorization中,规范写法:Bearer {token})调用网关接口; -
网关拦截 :通过全局过滤器(GlobalFilter) 拦截所有请求,提取并解析Token;
-
信息校验 :网关调用用户中心微服务 或直接解析JWT,校验Token有效性并获取用户核心信息(id、mobile、token等);
-
写入请求头 :将用户信息(JSON/拼接字符串/单独字段)写入自定义请求头 (如
X-User-Info),传递给下游微服务; -
微服务接收 :下游所有微服务通过拦截器/过滤器/AOP 统一从请求头中读取用户信息,封装为全局对象供业务使用;
-
异常处理:网关层统一处理Token过期、无效、缺失等异常,直接返回401/403,避免无效请求到达微服务。
二、前提准备(规范Token传递)
前端登录成功后,需将后端返回的token按HTTP规范放在请求头中,网关统一从该位置提取,避免多端传递不一致:
-
请求头键名 :
Authorization(行业通用,推荐) -
值格式 :
Bearer + 空格 + token(如Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...) -
前端请求示例(Axios):
JavaScriptaxios({ url: '/train/main', method: 'get', headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` // 从本地存储取token } })
三、网关层实现(核心:全局过滤器)
3.1 网关依赖(确保已引入,Spring Cloud Gateway)
XML
<!-- Gateway核心依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!-- 若用JWT解析,引入JWT依赖 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<!-- 服务调用(若需调用用户中心校验Token,引入OpenFeign) -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
3.2 核心:全局过滤器实现(二选一:JWT解析 / Feign调用用户中心)
Gateway网关通过GlobalFilter+Ordered实现全局请求拦截,无需修改原有路由配置,过滤器会对所有经过网关的请求生效。
方案1:直接解析JWT(推荐,性能更高,无需调用微服务)
适用于登录时生成JWT Token(用户信息已加密在Token中),网关直接解析Token获取用户信息,无需调用其他服务,性能最优。
Java
package com.jagochan.gateway.filter;
import com.alibaba.fastjson2.JSON;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
/**
* 网关全局过滤器:解析JWT Token,将用户信息写入请求头传递给下游微服务
*/
@Component
public class UserInfoTransferFilter implements GlobalFilter, Ordered {
// 1. JWT配置(与登录生成Token时的密钥、过期时间保持一致)
private static final String JWT_SECRET = "jagochan-train-2026-secret-key"; // 密钥,生产建议放配置中心
private static final String TOKEN_PREFIX = "Bearer "; // Token前缀
private static final String USER_INFO_HEADER = "X-User-Info"; // 传递用户信息的自定义请求头
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
// 2. 放行无需登录的接口(如登录页、静态资源、Swagger)
String path = request.getPath().toString();
if (path.contains("/train/login") || path.contains("/swagger-ui")
|| path.contains("/v3/api-docs") || path.contains("/actuator")) {
return chain.filter(exchange);
}
// 3. 提取请求头中的Token
String authHeader = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
if (!StringUtils.hasText(authHeader) || !authHeader.startsWith(TOKEN_PREFIX)) {
// Token缺失/格式错误,返回401未授权
return buildErrorResponse(response, HttpStatus.UNAUTHORIZED, "请先登录");
}
String token = authHeader.replace(TOKEN_PREFIX, "");
try {
// 4. 解析JWT Token,获取用户信息(Claims中存储了登录时放入的用户信息)
SecretKey secretKey = new SecretKeySpec(Base64.getDecoder().decode(JWT_SECRET), Jwts.SIG.HS256.key().build().getAlgorithm());
Claims claims = Jwts.parser()
.verifyWith(secretKey)
.build()
.parseSignedClaims(token)
.getPayload();
// 5. 提取核心用户信息(与登录时放入的字段一致)
Long userId = claims.get("id", Long.class);
String mobile = claims.get("mobile", String.class);
String email = claims.get("email", String.class);
// 6. 封装用户信息(JSON格式,方便下游微服务解析)
Map<String, Object> userInfo = new HashMap<>();
userInfo.put("id", userId);
userInfo.put("mobile", mobile);
userInfo.put("email", email);
userInfo.put("token", token); // 可选:将token也传递下去,供微服务调用其他服务使用
String userInfoJson = JSON.toJSONString(userInfo);
// 7. 将用户信息写入请求头(Gateway需用mutate重构请求,因为request是不可变的)
ServerHttpRequest newRequest = request.mutate()
.header(USER_INFO_HEADER, userInfoJson)
.build();
// 8. 将重构后的请求传递给下游微服务
return chain.filter(exchange.mutate().request(newRequest).build());
} catch (Exception e) {
// Token过期/无效,返回401
return buildErrorResponse(response, HttpStatus.UNAUTHORIZED, "登录已过期,请重新登录");
}
}
/**
* 构建统一的异常响应
*/
private Mono<Void> buildErrorResponse(ServerHttpResponse response, HttpStatus status, String msg) {
response.setStatusCode(status);
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
Map<String, Object> error = new HashMap<>();
error.put("code", status.value());
error.put("msg", msg);
String errorJson = JSON.toJSONString(error);
DataBuffer buffer = response.bufferFactory().wrap(errorJson.getBytes(StandardCharsets.UTF_8));
return response.writeWith(Mono.just(buffer));
}
/**
* 过滤器执行顺序(数字越小,执行越早)
* 设为-10,保证在网关路由过滤器之前执行,先解析用户信息再转发
*/
@Override
public int getOrder() {
return -10;
}
}
方案2:Feign调用用户中心校验Token(适用于非JWT场景)
若项目未使用JWT,Token为自定义随机字符串(用户信息存储在数据库/Redis中),网关通过OpenFeign调用用户中心微服务,校验Token并获取用户信息。
步骤1:网关编写Feign客户端(调用用户中心)
Java
package com.jagochan.gateway.feign;
import com.jagochan.train.common.resp.Result;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.Map;
/**
* 调用用户中心微服务,校验Token并获取用户信息
*/
@FeignClient(value = "user-service") // 用户中心微服务的服务名(注册中心中的名称)
public interface UserFeignClient {
/**
* 用户中心提供的Token校验接口
* @param token 登录凭证
* @return 包含用户信息的结果集
*/
@GetMapping("/member/verifyToken")
Result<Map<String, Object>> verifyToken(@RequestParam String token);
}
步骤2:修改过滤器,替换JWT解析为Feign调用
Java
// 替换方案1中的try-catch块核心逻辑,其余代码不变
@Resource
private UserFeignClient userFeignClient;
try {
// 4. 调用用户中心校验Token,获取用户信息
Result<Map<String, Object>> result = userFeignClient.verifyToken(token);
if (!result.isSuccess() || result.getData() == null) {
return buildErrorResponse(response, HttpStatus.UNAUTHORIZED, result.getMsg());
}
Map<String, Object> userInfo = result.getData();
// 5. 封装用户信息为JSON(用户中心返回的信息已包含id/mobile/email等)
String userInfoJson = JSON.toJSONString(userInfo);
// 6. 写入请求头,转发请求(与方案1一致)
ServerHttpRequest newRequest = request.mutate()
.header(USER_INFO_HEADER, userInfoJson)
.build();
return chain.filter(exchange.mutate().request(newRequest).build());
} catch (Exception e) {
return buildErrorResponse(response, HttpStatus.UNAUTHORIZED, "登录验证失败,请重新登录");
}
步骤3:用户中心实现verifyToken接口
Java
// 用户中心微服务的控制器
@RestController
@RequestMapping("/member")
public class MemberController {
@Resource
private RedisTemplate<String, Object> redisTemplate;
/**
* 网关调用的Token校验接口
* @param token 登录凭证
* @return 用户信息
*/
@GetMapping("/verifyToken")
public Result<Map<String, Object>> verifyToken(@RequestParam String token) {
// 从Redis中获取用户信息(登录时将token作为key,用户信息作为value存入Redis)
String redisKey = "login:token:" + token;
Map<String, Object> userInfo = (Map<String, Object>) redisTemplate.opsForValue().get(redisKey);
if (userInfo == null) {
return Result.fail("Token无效或已过期");
}
return Result.success(userInfo);
}
}
3.3 网关跨域配置兼容(关键!避免请求头丢失)
若网关已配置跨域(CORS),需显式允许自定义请求头 X-User-Info ,否则浏览器会拦截该请求头,导致下游微服务无法获取用户信息,修改网关application.yml:
YAML
spring:
cloud:
gateway:
globalcors:
cors-configurations:
'[/**]':
allowedOriginPatterns: "*"
allowedHeaders: "*" # 允许所有请求头(包含自定义的X-User-Info)
allowedMethods: "*"
allowCredentials: true
maxAge: 3600
add-to-simple-url-handler-mapping: true
四、下游微服务实现(统一读取用户信息)
下游所有微服务无需重复解析Token ,直接从网关传递的X-User-Info请求头中读取用户信息,通过拦截器(Interceptor) 统一解析并封装为全局可访问的用户对象,业务层可直接注入使用,无需每次从请求头读取。
4.1 微服务通用依赖(确保已引入)
XML
<!-- Web核心依赖(含拦截器支持) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- fastjson2(解析JSON格式的用户信息) -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.32</version>
</dependency>
4.2 步骤1:定义用户信息实体类(与网关传递的字段一致)
Java
package com.jagochan.train.common.entity;
import lombok.Data;
/**
* 全局用户信息实体(网关传递的用户信息)
*/
@Data
public class LoginUser {
private Long id; // 用户ID
private String mobile; // 手机号
private String email; // 邮箱
private String token; // 登录Token
}
4.3 步骤2:定义全局用户上下文(ThreadLocal存储,保证线程安全)
通过ThreadLocal存储当前请求的用户信息,同一请求的所有线程(业务层、服务层、DAO层)均可直接获取,线程安全且无侵入性。
Java
package com.jagochan.train.common.context;
import com.jagochan.train.common.entity.LoginUser;
/**
* 用户上下文:ThreadLocal存储当前登录用户信息
*/
public class UserContext {
// ThreadLocal:每个线程独立存储,避免多线程数据混乱
private static final ThreadLocal<LoginUser> USER_THREAD_LOCAL = new ThreadLocal<>();
/**
* 设置当前用户信息
*/
public static void setLoginUser(LoginUser loginUser) {
USER_THREAD_LOCAL.set(loginUser);
}
/**
* 获取当前登录用户信息
*/
public static LoginUser getLoginUser() {
return USER_THREAD_LOCAL.get();
}
/**
* 获取当前用户ID(常用,封装快捷方法)
*/
public static Long getUserId() {
LoginUser loginUser = getLoginUser();
return loginUser == null ? null : loginUser.getId();
}
/**
* 移除当前用户信息(防止内存泄漏)
*/
public static void remove() {
USER_THREAD_LOCAL.remove();
}
}
4.4 步骤3:实现拦截器,解析请求头并设置用户上下文
Java
package com.jagochan.train.member.interceptor;
import com.alibaba.fastjson2.JSON;
import com.jagochan.train.common.context.UserContext;
import com.jagochan.train.common.entity.LoginUser;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 微服务全局拦截器:解析网关传递的用户信息,设置到UserContext
*/
@Component
public class UserInfoInterceptor implements HandlerInterceptor {
// 与网关定义的自定义请求头一致
private static final String USER_INFO_HEADER = "X-User-Info";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1. 从请求头中获取用户信息JSON
String userInfoJson = request.getHeader(USER_INFO_HEADER);
if (userInfoJson != null && !userInfoJson.isEmpty()) {
// 2. 解析JSON为LoginUser对象
LoginUser loginUser = JSON.parseObject(userInfoJson, LoginUser.class);
// 3. 设置到用户上下文(ThreadLocal)
UserContext.setLoginUser(loginUser);
}
// 4. 放行请求
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 5. 请求结束后移除ThreadLocal中的数据,防止内存泄漏
UserContext.remove();
}
}
4.5 步骤4:注册拦截器,使其全局生效
Java
package com.jagochan.train.member.config;
import com.jagochan.train.member.interceptor.UserInfoInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.annotation.Resource;
/**
* Web配置:注册全局拦截器
*/
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Resource
private UserInfoInterceptor userInfoInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册用户信息拦截器,匹配所有请求
registry.addInterceptor(userInfoInterceptor).addPathPatterns("/**");
}
}
4.6 步骤5:业务层直接使用用户信息(无侵入,极简)
微服务的控制器、服务层、DAO层 均可直接通过UserContext获取当前登录用户信息,无需任何参数传递,示例:
Java
package com.jagochan.train.member.controller;
import com.jagochan.train.common.context.UserContext;
import com.jagochan.train.common.entity.LoginUser;
import com.jagochan.train.common.resp.Result;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/train/main")
public class MainController {
/**
* 业务接口:直接获取登录用户信息
*/
@GetMapping("/info")
public Result<LoginUser> getUserInfo() {
// 1. 快捷获取当前用户ID
Long userId = UserContext.getUserId();
// 2. 获取完整用户信息
LoginUser loginUser = UserContext.getLoginUser();
return Result.success(loginUser);
}
}
五、关键优化与避坑点
5.1 网关过滤器执行顺序(必对!)
-
过滤器
getOrder()返回负数 (如-10),保证在Gateway的路由过滤器、负载均衡过滤器之前执行,先解析用户信息再转发请求; -
若有多个全局过滤器,按order值从小到大执行,确保用户信息过滤器最先执行。
5.2 防止请求头中文/特殊字符乱码
网关传递的用户信息为JSON字符串,若包含中文(如昵称),需确保请求头编码为UTF-8,Gateway默认支持,微服务端无需额外配置(HttpServletRequest默认按UTF-8解析请求头)。
5.3 微服务ThreadLocal内存泄漏防护
-
必须在
afterCompletion中调用UserContext.remove(),因为Tomcat使用线程池,线程执行完后不会销毁,若不移除,ThreadLocal会持有对象引用,导致内存泄漏; -
afterCompletion会在请求处理完成(包括异常) 后执行,确保无论请求成功与否,都会清理数据。
5.4 放行无需登录的接口(网关层必做!)
网关过滤器中必须放行登录接口、静态资源、Swagger、健康检查接口,否则这些接口会被拦截,返回401:
Java
if (path.contains("/login") || path.contains("/swagger-ui")
|| path.contains("/v3/api-docs") || path.contains("/actuator/health")) {
return chain.filter(exchange);
}
5.5 生产环境JWT密钥安全
-
JWT的密钥(
JWT_SECRET)不可硬编码 ,生产环境放入Nacos/Apollo配置中心,通过@Value注入; -
密钥长度至少32位,使用随机字符串生成,避免被破解。
5.6 下游微服务无需再做登录校验
-
所有请求必须经过网关,网关层已统一校验Token有效性,微服务端无需再校验Token ,直接使用
UserContext的信息即可; -
若微服务需对外提供接口(绕过网关),需单独添加Token校验逻辑。
六、拓展场景:微服务之间调用传递用户信息
若下游微服务之间相互调用(如A微服务调用B微服务),需要传递当前登录用户信息,只需在Feign请求中添加请求头即可,实现Feign请求拦截器:
Java
package com.jagochan.train.common.feign;
import com.jagochan.train.common.context.UserContext;
import com.jagochan.train.common.entity.LoginUser;
import com.alibaba.fastjson2.JSON;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.stereotype.Component;
/**
* Feign全局拦截器:微服务之间调用时,传递用户信息到请求头
*/
@Component
public class FeignUserInfoInterceptor implements RequestInterceptor {
private static final String USER_INFO_HEADER = "X-User-Info";
@Override
public void apply(RequestTemplate template) {
// 获取当前用户信息,写入Feign请求头
LoginUser loginUser = UserContext.getLoginUser();
if (loginUser != null) {
template.header(USER_INFO_HEADER, JSON.toJSONString(loginUser));
}
}
}
所有微服务引入该公共拦截器后,相互调用时会自动传递用户信息,保证整个调用链路的用户信息一致性。
七、核心总结
-
核心方案 :网关全局过滤器 解析Token→获取用户信息→写入自定义请求头,下游微服务拦截器 解析请求头→
ThreadLocal封装→业务层直接使用; -
性能优选:使用JWT解析用户信息,无需调用微服务,网关层完成所有校验,性能最高;
-
无侵入性 :微服务通过
UserContext全局获取用户信息,业务代码无需任何修改,符合开闭原则; -
链路一致性:配合Feign拦截器,实现微服务之间调用的用户信息自动传递,全链路无感知;
-
统一异常:网关层统一处理Token相关异常,返回标准401响应,下游微服务无需重复处理。
该方案适配Spring Cloud Gateway主流使用场景,配置简单、扩展性强,生产环境可直接落地,完全解决网关向下游微服务传递登录用户信息的问题。