微服务安全

返回 Spring Cloud

微服务安全的核心问题:用户身份如何在多个服务间传递,以及服务之间如何互信


整体架构

用户
  │  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: STRICT

Token 刷新策略

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 做细粒度

相关链接

本目录

Spring Boot

架构