学完这篇,你能从零跑起一个 Spring Cloud Gateway 网关服务,用 YAML 配出能用的路由,并写出自己的过滤器来处理鉴权、日志这类横切逻辑。
第一步:对齐版本、补齐依赖
Spring Cloud Gateway 的版本必须和 Spring Boot 对齐,否则启动就会报错。常用组合是:
- Spring Boot 3.2.x + Spring Cloud 2023.0.x + JDK 17
- Spring Boot 2.7.x + Spring Cloud 2021.0.x + JDK 8/11
pom.xml 里先引 BOM,再引两个 starter:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2023.0.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
</dependencies>
注意:网关是基于 WebFlux 的响应式栈,pom.xml 里绝不能出现 spring-boot-starter-web(或者被其他依赖间接带进来)。Servlet 栈和 WebFlux 同时存在时,网关会启动失败或路由 404,这是新手最常踩的坑。用 mvn dependency:tree | grep spring-webmvc 确认一下。
第二步:写第一条路由
路由的三个要素是:id(唯一标识)、uri(转发目标)、predicates(匹配条件)。最小可用配置:
server:
port: 8080
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://user-service
predicates:
- Path=/api/user/**
filters:
- StripPrefix=2
lb://user-service 表示走注册中心负载均衡;如果暂时没有注册中心,直接写 http://127.0.0.1:8081 也行。StripPrefix=2 会砍掉路径前两段,所以 /api/user/profile 转发到下游时变成 /profile。
第三步:组合断言,把路由分细
断言可以叠加,多个条件之间是“与”的关系:
predicates:
- Path=/api/order/**
- Method=GET,POST
- Header=X-Client, app-*
- Query=debug
- After=2025-01-01T00:00:00+08:00[Asia/Shanghai]
常用断言还有 Host、Cookie、RemoteAddr、Before/Between。灰度发布场景常把 Header 断言和权重结合,比如同一路径按 Weight=group,80 / Weight=group,20 拆两条路由。断言之间的顺序不影响结果,但路由之间是从上到下匹配,第一条命中的就返回,所以把精确路径的写在通配路径前面。
第四步:用内置过滤器做改造
内置过滤器基本覆盖了日常需求:
filters:
- AddRequestHeader=X-Gateway, clara
- RemoveRequestHeader=Cookie
- AddResponseHeader=X-Response-From, gateway
- PrefixPath=/v1
- RewritePath=/api/old/(?<seg>.*), /api/new/$\{seg}
- SetStatus=200
限流和熔断也用过滤器实现:RequestRateLimiter(需要配 Redis 和 KeyResolver)、CircuitBreaker(需要引 spring-cloud-starter-circuitbreaker-reactor-resilience4j)。
第五步:写自定义 GatewayFilter
只作用于某条路由的过滤器,继承 AbstractGatewayFilterFactory,类名必须叫 XxxGatewayFilterFactory,配置里写 Xxx:
@Component
public class TokenCheckGatewayFilterFactory
extends AbstractGatewayFilterFactory<TokenCheckGatewayFilterFactory.Config> {
public TokenCheckGatewayFilterFactory() { super(Config.class); }
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
String token = exchange.getRequest()
.getHeaders().getFirst(config.getHeaderName());
if (token == null || token.isBlank()) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
};
}
public static class Config {
private String headerName = "Authorization";
public String getHeaderName() { return headerName; }
public void setHeaderName(String h) { this.headerName = h; }
}
}
配置里两种写法都行:- TokenCheck 走默认值,或者 - name: TokenCheck + args: {headerName: X-Token}。
第六步:全局过滤器与执行顺序
对所有路由生效的,实现 GlobalFilter + Ordered:
@Component
public class AccessLogFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
long start = System.currentTimeMillis();
return chain.filter(exchange).then(Mono.fromRunnable(() ->
log.info("{} {} status={} cost={}ms",
exchange.getRequest().getMethod(),
exchange.getRequest().getPath(),
exchange.getResponse().getStatusCode(),
System.currentTimeMillis() - start)));
}
@Override
public int getOrder() { return -100; }
}
order 越小越先执行;请求阶段按正序,响应阶段按倒序回调。鉴权类过滤器给 -100 到 0,日志统计给大正数。
常见坑速查
注意:网关统一配 CORS 后,下游服务就不要再配 @CrossOrigin 或全局跨域,否则响应头里会出现两个 Access-Control-Allow-Origin,浏览器直接拒绝。
注意:在过滤器里读请求体(exchange.getRequest().getBody())默认只能读一次,下游就拿不到数据了。需要改写 body 时先做 DataBufferUtils.join 缓存,再用 ServerHttpRequestDecorator 回写。
注意:lb:// 前缀必须有 spring-cloud-starter-loadbalancer,缺了会报 503 找不到实例;注册中心里服务名大小写也要一致。
注意:超时别只调下游,网关自己也要配:spring.cloud.gateway.httpclient.connect-timeout=2000 和 response-timeout=5s(后者是 Duration 格式)。
小结
- 依赖上只引
spring-cloud-starter-gateway,绝不能混入 spring-boot-starter-web。
- 一条路由 = id + uri + predicates + filters,
lb:// 走负载均衡,http:// 直连。
- 断言多个条件是“与”关系,路由按声明顺序匹配,精确路径写在通配前面。