Spring Cloud Gateway 高级实战——自定义过滤器、鉴权集成、动态路由
Spring Cloud Gateway 高级实战——自定义过滤器、鉴权集成、动态路由
适读人群:使用或计划使用 Spring Cloud Gateway 的后端/架构工程师 | 阅读时长:约17分钟 | 核心价值:掌握 Gateway 的核心扩展点,把网关从"流量透传"升级为"智能入口"
网关,不只是路由表
2022 年,我帮一家做在线教育的公司做架构升级。他们原来没有网关,每个微服务各自做鉴权、限流、日志。结果:
- 鉴权逻辑在 12 个微服务里各写了一遍,有的用 JWT,有的用 session,有的两种都有
- 日志格式不统一,排查跨服务问题时需要同时打开 12 个日志文件对比
- 某个服务被爬虫打,只能在那个服务里加限流,治标不治本
他们负责人小陈找到我说:"能不能做一个统一的地方,把这些事情都在一个地方处理?"
这就是网关的价值。
Spring Cloud Gateway 接入后,鉴权、限流、日志、灰度发布全部在网关层统一处理,下游微服务只需要关心业务逻辑。今天把 Gateway 的几个核心高级能力系统过一遍。
一、过滤器链:Gateway 的核心扩展点
Spring Cloud Gateway 的请求处理流程:
客户端请求
↓
Gateway Handler
↓
Pre 过滤器链(按 order 从小到大执行)
↓
代理转发(到下游服务)
↓
Post 过滤器链(按 order 从大到小执行,即逆序)
↓
返回响应给客户端1.1 全局过滤器(GlobalFilter)
全局过滤器对所有路由生效,是放置通用逻辑(鉴权、日志、限流)的最佳位置:
@Component
public class RequestLogFilter implements GlobalFilter, Ordered {
private static final Logger log = LoggerFactory.getLogger(RequestLogFilter.class);
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String requestId = UUID.randomUUID().toString().replace("-", "");
long startTime = System.currentTimeMillis();
// Pre 阶段:请求进来时记录
log.info("[Gateway] 请求进入: requestId={}, method={}, path={}, ip={}",
requestId,
request.getMethod(),
request.getPath().value(),
getClientIp(request));
// 把 requestId 写入请求头,传递给下游
ServerHttpRequest mutatedRequest = request.mutate()
.header("X-Request-Id", requestId)
.build();
ServerWebExchange mutatedExchange = exchange.mutate()
.request(mutatedRequest)
.build();
return chain.filter(mutatedExchange)
.then(Mono.fromRunnable(() -> {
// Post 阶段:响应返回时记录
long duration = System.currentTimeMillis() - startTime;
ServerHttpResponse response = exchange.getResponse();
log.info("[Gateway] 请求完成: requestId={}, status={}, duration={}ms",
requestId,
response.getStatusCode(),
duration);
}));
}
@Override
public int getOrder() {
return -200; // 数字越小,越先执行,日志过滤器要最先运行
}
private String getClientIp(ServerHttpRequest request) {
String ip = request.getHeaders().getFirst("X-Forwarded-For");
if (StringUtils.hasText(ip)) {
return ip.split(",")[0].trim();
}
return Objects.requireNonNull(request.getRemoteAddress()).getAddress().getHostAddress();
}
}二、鉴权集成:JWT 验证的标准实现
2.1 JWT 验证过滤器
@Component
public class JwtAuthFilter implements GlobalFilter, Ordered {
private static final Logger log = LoggerFactory.getLogger(JwtAuthFilter.class);
// 白名单:不需要鉴权的路径
private static final List<String> WHITE_LIST = Arrays.asList(
"/api/auth/login",
"/api/auth/register",
"/api/public/**",
"/actuator/health"
);
@Autowired
private JwtTokenProvider jwtTokenProvider;
@Autowired
private PathMatcher pathMatcher;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String path = exchange.getRequest().getPath().value();
// 白名单路径直接放行
if (isWhitelisted(path)) {
return chain.filter(exchange);
}
// 从请求头获取 Token
String token = extractToken(exchange.getRequest());
if (!StringUtils.hasText(token)) {
return unauthorized(exchange, "缺少认证 Token");
}
// 验证 Token
try {
Claims claims = jwtTokenProvider.parseToken(token);
// 把用户信息注入请求头,传递给下游
ServerHttpRequest mutatedRequest = exchange.getRequest().mutate()
.header("X-User-Id", claims.getSubject())
.header("X-User-Role", claims.get("role", String.class))
.build();
return chain.filter(exchange.mutate().request(mutatedRequest).build());
} catch (ExpiredJwtException e) {
return unauthorized(exchange, "Token 已过期,请重新登录");
} catch (JwtException e) {
log.warn("JWT 验证失败: path={}, error={}", path, e.getMessage());
return unauthorized(exchange, "Token 无效");
}
}
private boolean isWhitelisted(String path) {
return WHITE_LIST.stream()
.anyMatch(pattern -> pathMatcher.match(pattern, path));
}
private String extractToken(ServerHttpRequest request) {
String bearerToken = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
// 也支持从 URL 参数获取(适配某些场景)
return request.getQueryParams().getFirst("token");
}
private Mono<Void> unauthorized(ServerWebExchange exchange, String message) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
String body = String.format("{\"code\":401,\"message\":\"%s\"}", message);
DataBuffer buffer = response.bufferFactory()
.wrap(body.getBytes(StandardCharsets.UTF_8));
return response.writeWith(Mono.just(buffer));
}
@Override
public int getOrder() {
return -100; // 在日志过滤器之后执行
}
}踩坑一:Token 验证通过但下游拿不到用户信息
现象:网关成功验证了 JWT,但下游服务里 request.getHeader("X-User-Id") 返回 null。
原因:exchange.mutate().request(...) 只修改了本次请求在 Gateway 内的视图,如果后续有其他过滤器对 exchange 做了替换(而不是基于已修改的 exchange),修改会丢失。
解法:确保所有需要修改请求头的过滤器都基于同一个 exchange 链式修改,或者把用户信息写入 exchange 的 attributes(不依赖请求头传递):
// 更可靠的方式:用 attributes 传递用户信息
exchange.getAttributes().put("userId", claims.getSubject());
exchange.getAttributes().put("userRole", claims.get("role", String.class));三、动态路由:不重启就能修改路由规则
3.1 基于 Nacos 的动态路由
默认的路由规则写在 YAML 配置文件里,修改需要重启。引入动态路由后,可以在运行时通过 Nacos 配置中心推送新的路由规则,无需重启。
@Component
public class NacosDynamicRouteService implements ApplicationEventPublisherAware {
private static final Logger log = LoggerFactory.getLogger(NacosDynamicRouteService.class);
@Autowired
private RouteDefinitionWriter routeDefinitionWriter;
@Autowired
private RouteDefinitionLocator routeDefinitionLocator;
private ApplicationEventPublisher eventPublisher;
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
this.eventPublisher = publisher;
}
/**
* 更新路由(全量替换)
* 当 Nacos 配置变更时调用此方法
*/
public void updateRoutes(List<RouteDefinition> newRoutes) {
// 1. 删除现有路由
routeDefinitionLocator.getRouteDefinitions()
.flatMap(route -> routeDefinitionWriter.delete(Mono.just(route.getId())))
.collectList()
.block();
// 2. 写入新路由
newRoutes.forEach(route -> {
routeDefinitionWriter.save(Mono.just(route)).block();
log.info("路由已更新: id={}, uri={}", route.getId(), route.getUri());
});
// 3. 发布刷新事件,让 Gateway 重新加载路由
eventPublisher.publishEvent(new RefreshRoutesEvent(this));
}
}
// Nacos 配置监听器
@Component
public class NacosRouteConfigListener implements ApplicationRunner {
@Autowired
private NacosDynamicRouteService dynamicRouteService;
@Autowired
private NacosConfigProperties nacosConfigProperties;
@Autowired
private ObjectMapper objectMapper;
@Override
public void run(ApplicationArguments args) throws Exception {
ConfigService configService = NacosFactory.createConfigService(
nacosConfigProperties.getServerAddr());
// 监听路由配置变化
configService.addListener("gateway-routes.json", "DEFAULT_GROUP",
new Listener() {
@Override
public void receiveConfigInfo(String configInfo) {
try {
List<RouteDefinition> routes = objectMapper.readValue(
configInfo,
new TypeReference<List<RouteDefinition>>() {});
dynamicRouteService.updateRoutes(routes);
log.info("路由配置已从 Nacos 加载,共 {} 条路由", routes.size());
} catch (Exception e) {
log.error("路由配置解析失败", e);
}
}
@Override
public Executor getExecutor() {
return null; // 使用 Nacos 默认线程池
}
});
}
}Nacos 中的路由配置(gateway-routes.json)示例:
[
{
"id": "order-service",
"uri": "lb://order-service",
"predicates": [
{"name": "Path", "args": {"pattern": "/api/orders/**"}}
],
"filters": [
{"name": "StripPrefix", "args": {"parts": "1"}},
{"name": "RequestRateLimiter", "args": {
"redis-rate-limiter.replenishRate": "100",
"redis-rate-limiter.burstCapacity": "200"
}}
],
"order": 1
}
]踩坑二:动态路由更新后偶发 404
现象:通过 Nacos 更新路由后,有几秒钟时间部分请求返回 404。
原因:路由更新是先删除全部旧路由,再写入新路由,两步之间有短暂的"路由空白期"。
解法:改为"写入新路由 + 删除不再需要的旧路由"的增量更新策略,而不是全量替换:
public void updateRoutesDiff(List<RouteDefinition> newRoutes) {
// 对比新旧路由,只增加和删除差异部分
Set<String> newRouteIds = newRoutes.stream()
.map(RouteDefinition::getId).collect(Collectors.toSet());
// 先写入/更新新路由
newRoutes.forEach(route -> routeDefinitionWriter.save(Mono.just(route)).block());
// 再删除不在新配置中的旧路由
routeDefinitionLocator.getRouteDefinitions()
.filter(route -> !newRouteIds.contains(route.getId()))
.flatMap(route -> routeDefinitionWriter.delete(Mono.just(route.getId())))
.collectList().block();
eventPublisher.publishEvent(new RefreshRoutesEvent(this));
}四、限流:基于 Redis 的令牌桶
Spring Cloud Gateway 内置了 RequestRateLimiter 过滤器,基于 Redis 实现了令牌桶算法:
spring:
cloud:
gateway:
routes:
- id: order-service
uri: lb://order-service
predicates:
- Path=/api/orders/**
filters:
- name: RequestRateLimiter
args:
# 令牌桶:每秒补充 50 个令牌,最大容量 100
redis-rate-limiter.replenishRate: 50
redis-rate-limiter.burstCapacity: 100
# 按用户 ID 限流(不同用户独立计数)
key-resolver: "#{@userIdKeyResolver}"@Bean
public KeyResolver userIdKeyResolver() {
return exchange -> {
// 按用户 ID 限流
String userId = exchange.getRequest().getHeaders().getFirst("X-User-Id");
if (StringUtils.hasText(userId)) {
return Mono.just(userId);
}
// 未登录用户按 IP 限流
String ip = exchange.getRequest()
.getRemoteAddress().getAddress().getHostAddress();
return Mono.just("anonymous:" + ip);
};
}Spring Cloud Gateway 把网关从单纯的流量路由工具,升级为了系统的智能入口层。把鉴权、限流、日志、灰度这些横切关注点统一在网关处理,是微服务架构走向成熟的标志。
