微服务安全
微服务安全的核心问题:用户身份如何在多个服务间传递,以及服务之间如何互信。
整体架构
用户
│ 1. 登录,获取 JWT(Access Token)
▼
认证服务(Auth Service)
│ 颁发 JWT,包含用户 ID、角色等 Claims
用户携带 JWT 发起请求
▼
API Gateway(Spring Cloud Gateway)
│ 2. 验证 JWT 有效性
│ 3. 提取用户信息,写入请求头 X-User-Id / X-User-Roles
│ 4. 转发到下游服务(不再携带原始 JWT,而是传递解析后的头)
▼
业务服务(Order / User / Product ...)
│ 5. 从请求头读取用户上下文,无需再验 JWT
▼
服务间调用(Feign)
│ 6. 传递用户上下文头(拦截器注入)
Gateway 统一鉴权
全局 JWT 过滤器
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {
@Value("${auth.jwt.secret}")
private String secret;
// 白名单(无需鉴权的路径)
private final List<String> whitelist = List.of("/api/auth/login", "/api/auth/register");
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String path = exchange.getRequest().getPath().value();
// 白名单直接放行
if (whitelist.stream().anyMatch(path::startsWith)) {
return chain.filter(exchange);
}
String token = resolveToken(exchange.getRequest());
if (token == null) {
return unauthorized(exchange);
}
try {
Claims claims = Jwts.parserBuilder()
.setSigningKey(Keys.hmacShaKeyFor(secret.getBytes()))
.build()
.parseClaimsJws(token)
.getBody();
// 将用户信息注入请求头,下游服务直接读取
ServerHttpRequest mutatedRequest = exchange.getRequest().mutate()
.header("X-User-Id", claims.getSubject())
.header("X-User-Roles", claims.get("roles", String.class))
.build();
return chain.filter(exchange.mutate().request(mutatedRequest).build());
} catch (JwtException e) {
return unauthorized(exchange);
}
}
private String resolveToken(ServerHttpRequest request) {
String bearer = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
if (StringUtils.hasText(bearer) && bearer.startsWith("Bearer ")) {
return bearer.substring(7);
}
return null;
}
private Mono<Void> unauthorized(ServerWebExchange exchange) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
@Override
public int getOrder() {
return -100; // 优先级最高
}
}Gateway 路由级权限控制
spring:
cloud:
gateway:
routes:
- id: admin-route
uri: lb://admin-service
predicates:
- Path=/api/admin/**
filters:
- name: RoleCheck
args:
roles: ADMIN # 自定义过滤器,检查 X-User-Roles 头@Component
public class RoleCheckGatewayFilterFactory
extends AbstractGatewayFilterFactory<RoleCheckGatewayFilterFactory.Config> {
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
String roles = exchange.getRequest().getHeaders().getFirst("X-User-Roles");
if (roles == null || !roles.contains(config.getRoles())) {
exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
};
}
@Data
public static class Config {
private String roles;
}
}下游服务读取用户上下文
// 用 ThreadLocal 存储当前请求的用户信息
public class UserContext {
private static final ThreadLocal<UserInfo> HOLDER = new ThreadLocal<>();
public static void set(UserInfo info) { HOLDER.set(info); }
public static UserInfo get() { return HOLDER.get(); }
public static void clear() { HOLDER.remove(); }
}
// 拦截器:从请求头提取用户信息
@Component
public class UserContextInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String userId = request.getHeader("X-User-Id");
String roles = request.getHeader("X-User-Roles");
if (userId != null) {
UserContext.set(new UserInfo(userId, roles));
}
return true;
}
@Override
public void afterCompletion(HttpServletRequest req, HttpServletResponse res, Object h, Exception ex) {
UserContext.clear(); // 必须清理,防止内存泄漏
}
}Feign 服务间传递用户上下文
下游服务通过 Feign 调用其他服务时,需要把用户头带上:
@Component
public class FeignUserContextInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
UserInfo user = UserContext.get();
if (user != null) {
template.header("X-User-Id", user.getId());
template.header("X-User-Roles", user.getRoles());
}
}
}全局生效:
feign:
client:
config:
default:
requestInterceptors:
- com.example.FeignUserContextInterceptor服务间身份认证(内部调用)
防止绕过 Gateway 直接访问内部服务:
方式一:内部请求头校验
// Gateway 在转发时加入内部标记头
.header("X-Internal-Token", internalToken)
// 业务服务校验
@Component
public class InternalTokenFilter extends OncePerRequestFilter {
@Value("${internal.token}")
private String expectedToken;
@Override
protected void doFilterInternal(HttpServletRequest request, ...) {
// 非 Gateway 转发的请求必须有合法的 Token
if (!request.getRequestURI().startsWith("/internal/")) {
filterChain.doFilter(request, response);
return;
}
String token = request.getHeader("X-Internal-Token");
if (!expectedToken.equals(token)) {
response.setStatus(403);
return;
}
filterChain.doFilter(request, response);
}
}方式二:mTLS(双向 TLS)
在 K8s 环境通过 Istio 实现,无需代码层介入:
# Istio PeerAuthentication — 强制服务间 mTLS
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
name: default
spec:
mtls:
mode: STRICTToken 刷新策略
Access Token:有效期短(15min ~ 2h),携带在每次请求中
Refresh Token:有效期长(7d ~ 30d),仅用于换取新 Access Token,存 HttpOnly Cookie
刷新流程:
1. Access Token 过期 → 前端静默调用 /auth/refresh
2. 携带 Refresh Token → 认证服务验证并颁发新 Access Token
3. 如果 Refresh Token 也过期 → 要求重新登录
安全要点
| 问题 | 处理方式 |
|---|---|
| Token 泄露 | 短有效期 + Refresh Token 轮换 |
| 网关绕过 | 业务服务校验 X-Internal-Token 或 mTLS |
| CSRF | 微服务通常无状态(JWT),CSRF 威胁低;若用 Cookie 需加 SameSite |
| 敏感日志 | 日志脱敏,不打印 Token 原文 |
| 权限粒度 | Gateway 做粗粒度(角色),服务内用 @PreAuthorize 做细粒度 |
相关链接
本目录
- API 网关(过滤器、路由)
- 服务调用(Feign 拦截器)
- Spring Cloud 基础
- 灰度发布(流量染色与 Gateway 联动)
- 服务网格(mTLS 零代码方案)
- Spring Cloud 目录