完整实现步骤:JWT 网关校验 + 微服务信息传递
JWT网关校验:在网关层使用全局过滤器进行拦截
微服务信息传递:在微服务里面用拦截器拦截到信息,然后统一使用
一、技术选型说明
- JWT 解析库推荐:
推荐使用jjwt
(Java JWT),它是 Java 生态中最流行的 JWT 库,具有以下优势:- API 简洁:生成、解析 JWT 仅需几行代码。
- 安全性高:支持多种签名算法(如 HS512、RS256)。
- 社区活跃:长期维护,文档完善,与 Spring 兼容性好。
二、网关层实现 JWT 校验
1. 添加依赖
在网关模块的 pom.xml
中添加 jjwt
和必要依赖:
<!-- JJWT -->
<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>
2. 实现 JWT 工具类
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.security.Key;
import java.util.Date;
@Component
public class JwtUtils {
@Value("${jwt.secret}") // 从配置文件中读取密钥
private String secret;
@Value("${jwt.expiration}")
private long expiration;
private Key key;
// 初始化密钥
@PostConstruct
public void init() {
this.key = Keys.hmacShaKeyFor(secret.getBytes());
}
// 生成 JWT
public String generateToken(String username, String roles) {
return Jwts.builder()
.setSubject(username)
.claim("roles", roles)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + expiration))
.signWith(key)
.compact();
}
// 解析 JWT
public Claims parseToken(String token) {
return Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
}
// 验证 Token 是否过期
public boolean isTokenExpired(String token) {
return parseToken(token).getExpiration().before(new Date());
}
}
3. 全局过滤器实现 JWT 校验
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
public class JwtAuthGlobalFilter implements GlobalFilter, Ordered {
private final JwtUtils jwtUtils;
public JwtAuthGlobalFilter(JwtUtils jwtUtils) {
this.jwtUtils = jwtUtils;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1. 排除登录接口
String path = exchange.getRequest().getPath().value();
if (path.startsWith("/api/auth/login")) {
return chain.filter(exchange);
}
// 2. 获取 Token
String token = extractToken(exchange.getRequest().getHeaders());
// 3. 验证 Token
if (token == null) {
return buildUnauthorizedResponse(exchange, "Missing token");
}
try {
Claims claims = jwtUtils.parseToken(token);
if (jwtUtils.isTokenExpired(token)) {
return buildUnauthorizedResponse(exchange, "Token expired");
}
// 4. 传递用户信息到下游服务
ServerHttpRequest mutatedRequest = exchange.getRequest().mutate()
.header("X-User-Name", claims.getSubject())
.header("X-User-Roles", claims.get("roles", String.class))
.build();
return chain.filter(exchange.mutate().request(mutatedRequest).build());
} catch (Exception e) {
return buildUnauthorizedResponse(exchange, "Invalid token");
}
}
private String extractToken(HttpHeaders headers) {
String authHeader = headers.getFirst(HttpHeaders.AUTHORIZATION);
return (authHeader != null && authHeader.startsWith("Bearer ")) ? authHeader.substring(7) : null;
}
private Mono<Void> buildUnauthorizedResponse(ServerWebExchange exchange, String message) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
exchange.getResponse().getHeaders().add("Content-Type", "application/json");
String body = String.format("{\"code\": 401, \"msg\": \"%s\"}", message);
return exchange.getResponse().writeWith(Mono.just(exchange.getResponse().bufferFactory().wrap(body.getBytes())));
}
@Override
public int getOrder() {
return -1; // 设置过滤器优先级(数字越小优先级越高)
}
}
4. 配置文件 application.yml
jwt:
secret: "my-ultra-secure-jwt-secret-key-1234567890" # 密钥(至少 32 位)
expiration: 3600000 # Token 有效期(1小时)
spring:
cloud:
gateway:
routes:
- id: auth-service
uri: lb://auth-service
predicates:
- Path=/api/auth/**
filters:
- StripPrefix=1
- id: user-service
uri: lb://user-service
predicates:
- Path=/api/users/**
filters:
- StripPrefix=1
三、微服务信息传递
为了在微服务中统一处理JWT校验并使用UserContext
传递用户信息,可以按照以下步骤实现:
public class UserContext {
private static final ThreadLocal<Map<String, String>> context = new ThreadLocal<>();
public static void setUserInfo(Map<String, String> userInfo) {
context.set(userInfo);
}
public static Map<String, String> getUserInfo() {
return context.get();
}
public static void clear() {
context.remove();
}
// 获取具体字段(示例)
public static String getUserId() {
return getUserInfo() != null ? getUserInfo().get("userId") : null;
}
public static String getUsername() {
return getUserInfo() != null ? getUserInfo().get("username") : null;
}
}
2. 实现JWT校验拦截器
@Component
public class JwtInterceptor implements HandlerInterceptor {
@Autowired
private JwtUtils jwtUtils; // 你的JWT工具类
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 从请求头获取token
String token = request.getHeader("Authorization");
if (StringUtils.isEmpty(token)) {
throw new UnauthorizedException("缺少Token");
}
// 校验并解析Token
Claims claims = jwtUtils.parseToken(token);
if (claims == null) {
throw new UnauthorizedException("Token无效或已过期");
}
// 提取用户信息存入UserContext
Map<String, String> userInfo = new HashMap<>();
userInfo.put("userId", claims.get("userId", String.class));
userInfo.put("username", claims.getSubject());
UserContext.setUserInfo(userInfo);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
// 请求完成后清理ThreadLocal,防止内存泄漏
UserContext.clear();
}
}
3. 注册拦截器
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private JwtInterceptor jwtInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(jwtInterceptor)
.addPathPatterns("/**") // 拦截所有路径
.excludePathPatterns("/auth/login", "/swagger**"); // 排除登录和Swagger
}
}
4. JWT工具类示例(JwtUtils)
@Component
public class JwtUtils {
@Value("${jwt.secret}")
private String secret;
public Claims parseToken(String token) {
try {
return Jwts.parser()
.setSigningKey(secret.getBytes())
.parseClaimsJws(token.replace("Bearer ", ""))
.getBody();
} catch (Exception e) {
return null; // Token解析失败
}
}
}
5. 在微服务中使用UserContext
@RestController
@RequestMapping("/api/user")
public class UserController {
@GetMapping("/profile")
public ResponseEntity<?> getUserProfile() {
String userId = UserContext.getUserId();
String username = UserContext.getUsername();
// 查询数据库或其他业务逻辑...
return ResponseEntity.ok("用户信息: " + username);
}
}
关键点说明:
-
拦截流程:
- 拦截器从
Authorization
头提取JWT。 - 使用
JwtUtils
验证并解析Token。 - 将用户关键信息存入
UserContext
的ThreadLocal中。
- 拦截器从
-
线程安全:
UserContext
使用ThreadLocal
确保每个请求线程独立存储。- 拦截器
afterCompletion
方法中调用UserContext.clear()
,避免内存泄漏。
-
异常处理:
- 如果Token校验失败,直接抛出异常(需配合全局异常处理器返回401状态码)。
-
网关与微服务协作:
- 网关负责路由转发和初步鉴权(如权限校验)。
- 微服务拦截器做二次校验(可选,根据安全级别决定),确保请求合法性。
扩展优化建议:
- Feign传递用户信息:如果微服务间调用需要传递用户身份,可以添加Feign拦截器:
@Bean public RequestInterceptor feignRequestInterceptor() { return requestTemplate -> { String userId = UserContext.getUserId(); if (userId != null) { requestTemplate.header("X-User-Id", userId); } }; }
- 更严格的校验:检查Token的签名、过期时间、黑名单等。
- 结合Spring Security:如果需要更复杂的权限控制,可整合Spring Security + JWT。
四、OpenFeign 传递用户信息(如JWT令牌或用户ID)
根据你的实际业务调整UserContext
存储的字段和校验逻辑即可。
为了在微服务间通过 OpenFeign 传递用户信息(如JWT令牌或用户ID),需通过 Feign拦截器 自动将上下文信息添加到请求头中。以下是完整步骤:
1. 添加Feign拦截器(传递用户信息)
@Component
public class FeignUserInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
// 从UserContext获取用户信息
String userId = UserContext.getUserId();
String token = UserContext.getToken();
// 将信息添加到Feign请求头
if (userId != null) {
template.header("X-User-Id", userId);
}
if (token != null) {
template.header("Authorization", "Bearer " + token);
}
}
}
2. 确保UserContext包含所需字段
public class UserContext {
private static final ThreadLocal<Map<String, String>> context = new ThreadLocal<>();
// 其他方法...
public static String getToken() {
return getUserInfo() != null ? getUserInfo().get("token") : null;
}
// 网关过滤器或拦截器中存入Token
public static void setToken(String token) {
Map<String, String> userInfo = getUserInfo();
if (userInfo == null) {
userInfo = new HashMap<>();
}
userInfo.put("token", token);
context.set(userInfo);
}
}
3. 下游服务接收并校验信息
下游服务需通过拦截器或过滤器读取请求头中的用户信息,并存入本地上下文:
下游服务拦截器示例
@Component
public class DownstreamUserInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 从请求头获取信息
String userId = request.getHeader("X-User-Id");
String token = request.getHeader("Authorization");
// 校验Token(可选)
if (token != null) {
Claims claims = jwtUtils.parseToken(token.replace("Bearer ", ""));
if (claims == null) {
throw new UnauthorizedException("Token无效");
}
}
// 存入当前服务的UserContext
Map<String, String> userInfo = new HashMap<>();
userInfo.put("userId", userId);
userInfo.put("token", token);
UserContext.setUserInfo(userInfo);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
UserContext.clear();
}
}
4. Feign客户端调用示例
在服务A中,通过Feign调用服务B的API,用户信息会自动传递:
@FeignClient(name = "service-b")
public interface ServiceBClient {
@GetMapping("/api/data")
ResponseEntity<String> getData();
}
// 业务代码中直接调用
public class ServiceA {
@Autowired
private ServiceBClient serviceBClient;
public void doSomething() {
// UserContext已在拦截器中填充
ResponseEntity<String> response = serviceBClient.getData();
}
}
关键配置说明
-
拦截器自动生效
Spring会自动发现FeignUserInterceptor
(因标注了@Component
),无需手动注册到Feign客户端。 -
确保UserContext正确填充
- 网关层需将JWT Token存入
UserContext
。 - 微服务自身的拦截器(如
JwtInterceptor
)需在校验Token后,将信息存入UserContext
。
- 网关层需将JWT Token存入
-
下游服务校验(可选)
- 如果下游服务需要严格校验Token,需配置相同的JWT密钥。
- 如果仅需传递用户ID,可省略Token校验步骤。
扩展场景:仅传递必要字段
如果不想传递完整Token,可在网关将用户ID、角色等关键信息注入请求头,微服务直接读取:
网关过滤器添加自定义头
public class GatewayFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 从JWT解析用户信息
String userId = ...;
String role = ...;
// 添加自定义请求头
ServerHttpRequest request = exchange.getRequest().mutate()
.header("X-User-Id", userId)
.header("X-User-Role", role)
.build();
return chain.filter(exchange.mutate().request(request).build());
}
}
Feign拦截器透传这些头
public class FeignUserInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
// 从当前请求的上下文中获取头信息(需配合RequestContextHolder)
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes != null) {
HttpServletRequest request = attributes.getRequest();
String userId = request.getHeader("X-User-Id");
String role = request.getHeader("X-User-Role");
template.header("X-User-Id", userId);
template.header("X-User-Role", role);
}
}
}
注意事项
-
线程池隔离问题
如果使用Hystrix或异步线程,需通过HystrixRequestContext
或自定义线程上下文传递数据。 -
敏感信息安全
- 始终使用HTTPS加密传输。
- 避免在请求头中传递敏感数据(如密码),必要时进行加密。
-
性能影响
频繁解析JWT可能影响性能,可在网关层完成主要校验,微服务按需二次校验。