配置一个最基础路由规则
关于 Spring Cloud Gateway 的原理和核心组件,之前已经有过介绍,可以参考《Spring Cloud Gateway - 从 0 到 1 构建一个网关》。这里我们直接聚焦 Spring Cloud Gateway 的常见的使用套路。首先我们先跑通一个最简单的网关配置。为方便起见,我们在本地起两个测试微服务:
- 用户微服务:内部访问路径为 localhost:8180
- 订单微服务:内部访问路径为 localhost:8081、localhost:8082
以上两个服务的构建可以参考 演示项目源码。 我们的目标是通过新搭建的 SGC,去访问这两个内部服务。
搭建基础 SGC 服务
父项目的 pom 配置可以参考 《Spring Cloud Alibaba 基础案例》 。
依赖配置:
1 2 3 4
| <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency>
|
启动类:
1 2 3 4 5 6
| @SpringBootApplication public class GatewayApp { public static void main(String[] args) { SpringApplication.run(GatewayApp.class, args); } }
|
直接开动 GatewayApp,我们就拥有了最简单的 gateway 服务。
配置目标服务的路由规则
编写 SGC 的 application.yml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| server: port: 8080
spring: application: name: zdemo-gateway cloud: gateway: routes: - id: user-route uri: http://localhost:8180 predicates: - Path=/user-serv/** filters: - StripPrefix=1
- id: order-route-high uri: http://localhost:8081 predicates: - Weight=order_group, 8 - Path=/order-serv/** filters: - StripPrefix=1 - id: user-route-low uri: http://localhost:8082 predicates: - Weight=order_group, 2 - Path=/order-serv/** filters: - StripPrefix=1 logging: level: org.springframework.cloud.gateway: debug
|
以上,我们使用到了SGC 的内置断言和过滤器:
- 内部用户服务对外暴露的访问路径:sgc_host:8080/user-serv/**。
- 内部订单服务对外暴露的访问路径:sgc_host:8080/order-serv/**,order 的多节点按权重分配访问。
关于 SGC 的所有内置和过滤器的说明,可以参考官网 Route Predicate Factories、GatewayFilter Factories。
整合 nacos
整合 nacos 注册中心
关于 nacos 的安装和配置,请参考之前文章片段 使用 zk 充当服务注册中心。
继续引入依赖:
1 2 3 4 5 6 7 8 9 10
| <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-loadbalancer</artifactId> </dependency>
|
配置文件的修改:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| server: port: 8080
spring: application: name: zdemo-gateway cloud: nacos: discovery: server-addr: 192.168.1.149:8848 username: nacos password: nacos namespace: prod-zdemo cluster-name: DEFAULT gateway: routes: - id: user-route uri: lb://zdemo-scloud-user predicates: - Path=/user-serv/** filters: - StripPrefix=1
- id: order-route uri: lb://zdemo-scloud-order predicates: - Weight=order_group, 8 - Path=/order-serv/** filters: - StripPrefix=1
logging: level: org.springframework.cloud.gateway: debug org.springframework.cloud.loadbalancer: debug
|
nacos 还有一个更加简化的约定大于配置的高级玩法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| spring: application: name: zdemo-gateway cloud: nacos: discovery: server-addr: 192.168.1.149:8848 username: nacos password: nacos namespace: prod-zdemo cluster-name: DEFAULT gateway: discovery: locator: enabled: true
|
这样即使你不配置具体的路由规则,也可以采用 nacos 提供的默认的路由规则进行服务的转发。不过实际生产不建议这样的配置,可读性和灵活性都不太好。
整合 nacos 配置中心
关于 nacos 作为配置中心的使用,可以参考之前的文章 Nacos 作为配置中心的使用探究。
继续引入依赖:
1 2 3 4
| <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency>
|
本地 application.yml 配置文件的修改:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| server: port: 8080
spring: application: name: zdemo-gateway config: import: - optional:nacos:${spring.application.name}.${spring.cloud.nacos.config.file-extension}?refresh=true cloud: nacos: discovery: server-addr: 192.168.1.149:8848 username: nacos password: nacos namespace: prod-zdemo cluster-name: DEFAULT config: server-addr: 192.168.1.149:8848 username: nacos password: nacos file-extension: yml namespace: prod-zdemo cluster-name: DEFAULT group: DEFAULT_GROUP
|
路由规则的详细配置,直接交给 nacos 进行托管。在 nacos 配置中心新建 zdemo-gateway.yml:
- 命名空间:prod-zdemo
- Data ID:zdemo-gateway.yml
- Group:DEFAULT_GROUP
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| spring: cloud: gateway: routes: - id: user-route uri: http://localhost:8180 predicates: - Path=/user-serv/** filters: - StripPrefix=1
- id: order-route-high uri: http://localhost:8081 predicates: - Weight=order_group, 8 - Path=/order-serv/** filters: - StripPrefix=1 - id: user-route-low uri: http://localhost:8082 predicates: - Weight=order_group, 2 - Path=/order-serv/** filters: - StripPrefix=1
|
上述准备工作完成之后,访问如下接口,依然OK,说明整合成功。
1
| $ curl http://localhost:8080/user-serv/dubbo/user/order?orderNo=1
|
自定义断言
SCG 自定义断言非常简单,但它严格遵循一套 “约定大于配置” 的命名规范。实现自定义断言的需要遵循的规范:
- 类名命名规范:必须以
RoutePredicateFactory 结尾。例如我们的断言叫 UserLevel,那么类名必须叫 UserLevelRoutePredicateFactory。
- 继承基类:必须继承官方的
AbstractRoutePredicateFactory,并在构造方法中将自定义的 Config 内部类传给 super。
- 注册为 Bean:必须将该类加上 @Component 注解(或者其他注册方式),让 Spring 容器接管。
下面我们就遵循这样一套规则,来定义一个自己的断言工厂:只有当请求头(或参数)中携带的 user-level 达到配置的级别时,网关才允许放行路由。
编写断言工厂类
新增 UserLevelRoutePredicateFactory 类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
| import org.springframework.cloud.gateway.handler.predicate.AbstractRoutePredicateFactory; import org.springframework.cloud.gateway.handler.predicate.GatewayPredicate; import org.springframework.stereotype.Component; import org.springframework.validation.annotation.Validated; import org.springframework.web.server.ServerWebExchange; import java.util.Collections; import java.util.List; import java.util.function.Predicate;
@Component public class UserLevelRoutePredicateFactory extends AbstractRoutePredicateFactory<UserLevelRoutePredicateFactory.Config> {
@Validated public static class Config { private String userLevel;
public String getUserLevel() { return userLevel; }
public void setUserLevel(String userLevel) { this.userLevel = userLevel; } }
public UserLevelRoutePredicateFactory() { super(Config.class); }
@Override public List<String> shortcutFieldOrder() { return Collections.singletonList("userLevel"); }
@Override public Predicate<ServerWebExchange> apply(Config config) { return new GatewayPredicate() { @Override public boolean test(ServerWebExchange exchange) { String clientUserLevel = exchange.getRequest().getHeaders().getFirst("user-level");
if (config.getUserLevel().equalsIgnoreCase(clientUserLevel)) { System.out.println("[Custom-Predicate] 匹配用户级别成功,放行! "); return true; } else { System.out.println("[Custom-Predicate] 匹配用户级别失败,期望: " + config.getUserLevel() + ", 实际: " + clientUserLevel); }
return false; } }; } }
|
使用自定义断言
现在,我们可以在 user-route 中加入这个刚写好的自定义断言了。在 nacos 配置文件 application.yml 修改:
1 2 3 4 5 6 7 8 9 10 11 12
| spring: cloud: gateway: routes: - id: user-route uri: lb://zdemo-scloud-user predicates: - Path=/user-serv/** - UserLevel=gold filters: - StripPrefix=1
|
注意:- UserLevel=gold 的名字必须去掉代码中的 RoutePredicateFactory 后缀。SCG 会在底层自动通过前缀名字去匹配。
测试验证
配置完成后重启网关服务,使用 curl 命令行进行双向测试:
1 2 3 4 5 6 7 8
|
$ curl http://192.168.1.5:8080/user-serv/dubbo/user/order?orderNo=1 $ curl -H "user-level: silver" http://192.168.1.5:8080/user-serv/info
$ curl -H "user-level: gold" http://192.168.1.5:8080/user-serv/dubbo/user/order?orderNo=1
|
如果想要自定义其它的断言工厂,也可以参考 AbstractRoutePredicateFactory 常见的内置断言的实现,这里不再赘述。
自定义过滤器
比如我们要实现的业务场景:自定义一个黑名单过滤器。如果请求头中携带的 source-ip 或者用户的 client-id 在黑名单中,我们直接在过滤器中予以拦截,拒绝该请求(返回 403 Forbidden),不再转发给下游服务。
在 Spring Cloud Gateway 中实现自定义过滤器,同样有一套极约定命名规范:
- 类名命名规范:必须以
GatewayFilterFactory 结尾。例如,你的过滤器叫 Blacklist,那么类名必须叫 BlacklistGatewayFilterFactory。
- 继承基类:必须继承官方的
AbstractGatewayFilterFactory,并在构造方法中将自定义的 Config 内部类传给 super。
编写自定义过滤器工厂类
BlacklistGatewayFilterFactory
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
| @Component public class BlacklistGatewayFilterFactory extends AbstractGatewayFilterFactory<BlacklistGatewayFilterFactory.Config> {
public static class Config { private List<String> blockUsers;
public List<String> getBlockUsers() { return blockUsers; }
public void setBlockUsers(List<String> blockUsers) { this.blockUsers = blockUsers; } }
public BlacklistGatewayFilterFactory() { super(Config.class); }
@Override public List<String> shortcutFieldOrder() { return Collections.singletonList("blockUsers"); }
@Override public ShortcutType shortcutType() { return ShortcutType.GATHER_LIST; }
@Override public GatewayFilter apply(Config config) { return (exchange, chain) -> { System.out.println("[Custom-Filter] -> 进入多黑名单检查过滤器...");
String clientId = exchange.getRequest().getHeaders().getFirst("client-id");
List<String> blackList = config.getBlockUsers().stream() .map(String::trim) .toList();
if (clientId != null && blackList.contains(clientId)) { System.out.println("[Custom-Filter] 拦截成功!用户 [" + clientId + "] 属于黑名单成员: " + blackList);
ServerHttpResponse response = exchange.getResponse(); response.setStatusCode(HttpStatus.FORBIDDEN); return response.setComplete(); }
return chain.filter(exchange).then(Mono.fromRunnable(() -> { System.out.println("[Custom-Filter] -> 离开多黑名单检查过滤器(后置阶段)"); })); }; } }
|
使用自定义拦截器
1 2 3 4 5 6 7 8 9 10 11 12
| spring: cloud: gateway: routes: - id: user-route uri: lb://zdemo-scloud-user predicates: - Path=/user-serv/** filters: - StripPrefix=1 - Blacklist=badGuy, tom, jack, hacker
|
测试验证
重启网关服务,在终端使用 curl 挨个测试这个新写的过滤器:
1 2 3 4 5 6 7
| $ curl -H "client-id: badGuy" http://192.168.1.5:8080/user-serv/dubbo/user/order?orderNo=1 $ curl -H "client-id: jack" http://192.168.1.5:8080/user-serv/dubbo/user/order?orderNo=1 -v
$ curl -H "client-id: rose" http://192.168.1.5:8080/user-serv/dubbo/user/order?orderNo=1 $ curl http://192.168.1.5:8080/user-serv/dubbo/user/order?orderNo=1
|
全局过滤器
什么是全局过滤器
在 SCG)中,过滤器按作用范围可以分为两大类:普通过滤器 (GatewayFilter) 和 全局过滤器 (GlobalFilter)。全局过滤器的最大特点是无须配置。 只要你把它注入到 Spring 容器中,它就会自动拦截网关接收到的所有路由请求。通常用于 全局通用业务,例如全局日志统计、分布式链路追踪、全局统一 JWT 鉴权等。
其实我们之前一直在享受全局过滤器的服务,SCG 核心功能大半都是由内置全局过滤器完成的,官方内置的常用全局过滤器:
- NettyRoutingFilter(最核心):负责底层的反应式网络转发,把请求通过 HttpClient 发送给下游服务。
- GatewayMetricsFilter:负责收集网关的监控指标(如请求耗时、成功率),对外对接 Prometheus。
- ForwardPathFilter:负责微服务内部的 forward:// 转发。
- LoadBalancerClientFilter:负责结合 Nacos 解析 lb:// 协议,动态选择一个健康的服务实例。
生效的顺序
当全局过滤器和普通过滤器同时存在时,SCG 会把它们融合进同一个过滤器链条中。决定谁先执行的唯一标准是:Order 值。
- Order 值越小,优先级越高(Pre 阶段越先执行,Post 阶段越后执行)。
- 全局过滤器可以通过实现 Ordered 接口或加上 @Order 注解来指定数字。
- 普通过滤器的 Order 值默认由它在 YML 中从上到下的配置顺序决定(从 1 开始递增)。
自定义全局过滤器
我们来实现一个生产环境最常用的全局过滤器:统一 JWT 鉴权与全局耗时统计过滤器。
- 拦截所有请求,检查 Header 里有没有携带有合法的 Authorization 令牌。
- 如果没有,直接拒绝(返回 401 Unauthorized)。
- 如果有,放行,并在请求结束后打印出本次请求的全局总耗时。
GlobalAuthAndLogFilter:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.core.Ordered; import org.springframework.http.HttpStatus; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono;
@Component public class GlobalAuthAndLogFilter implements GlobalFilter, Ordered {
@Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { String path = exchange.getRequest().getPath().value(); long startTime = System.currentTimeMillis(); System.out.println("[Global-Filter] ===> 开始拦截全局流量,请求路径: " + path);
if (path.contains("/admin/") || path.contains("/login")) { return chain.filter(exchange); }
String token = exchange.getRequest().getHeaders().getFirst("Authorization"); if (token == null || token.isEmpty()) { System.out.println("[Global-Filter] 拒绝访问!缺少 Authorization 令牌。路径: " + path);
ServerHttpResponse response = exchange.getResponse(); response.setStatusCode(HttpStatus.UNAUTHORIZED); return response.setComplete(); }
return chain.filter(exchange).then(Mono.fromRunnable(() -> { long costTime = System.currentTimeMillis() - startTime; System.out.println("[Global-Filter] <=== 请求结束,路径: " + path + ",总耗时: " + costTime + "ms"); })); }
@Override public int getOrder() { return -1; } }
|
测试验证:
1 2 3 4 5 6 7
|
$ curl http://192.168.1.5:8080/user-serv/dubbo/user/order?orderNo=1 -v
$ curl -H "Authorization: my-jwt-token-xxx" http://192.168.1.5:8080/user-serv/dubbo/user/order?orderNo=1
|
断言和过滤器的生效机制
一句话总结:断言之间是 AND(与)关系,而过滤器之间是 THEN(串联流式)关系。关于其中更透彻的机制,可以参考之前额的文章 《Spring Cloud Gateway - 从 0 到 1 构建一个网关》。
断言的逻辑
路由断言(Predicates)的逻辑是 “绝对的 AND” 关系。
1 2 3 4 5 6 7
|
predicates: - Path=/user-serv/** - UserLevel=gold - Method=GET
|
如果你想在断言里实现 OR(比如:路径是 /user/ 或者 请求头包含 Admin=true 都能通过),在现有的 YML 配置下是无法直接在一个 - 列表里写 OR 的。
实现 OR 的标准做法是:配置两条 id 不同、但 uri 完全相同的路由规则,让它们分别承载不同的断言。因为网关在匹配路由时,多条路由之间是 OR 的关系(只要匹配中一条就行)。
过滤器的逻辑
过滤器之间是纯粹的 THEN(串联流式)关系。
1 2 3 4 5 6 7
|
filters: - StripPrefix=1 - AddRequestHeader=X-My-Header, hello
|
跨域的设置
什么是跨域问题?
跨域(Cross-Origin)是由浏览器的同源策略引起的。当一个网页去请求另一个服务器的资源时,只要以下三者中有任何一个不同,即为跨域:
- 协议不同(如 http 与 https)
- 域名不同(如 a.com 与 b.com,或者 api.koohub.com 与 web.koohub.com)
- 端口不同(如 8080 与 8081)
同源策略不是为了刁难开发者,而是浏览器最核心的安全防护机制。它解决的根本问题是:防止恶意网站窃取或冒用你在信任网站上的隐私数据(防范 CSRF 跨站请求伪造攻击)。如果没有同源策略,你登录了网银网站 bank.com 留下了 Cookie,此时你又打开了一个恶意钓鱼网站 evil.com,该钓鱼网站的 JS 代码就可以肆无忌惮地异步请求 bank.com/transfer 接口。因为在同一个浏览器下,浏览器会自动带上 bank.com 的 Cookie,你的钱可能就被悄悄转走了。
微服务跨域设置标准姿势
在微服务架构中,跨域配置 千万不要网关配一套、后端服务又配一套!标准做法是只在网关层统一配置跨域,下游的目标服务必须全部关闭跨域配置。原因是如果网关和目标服务同时配置了跨域,HTTP 响应头中就会出现重复的 “Access-Control-Allow-Origin: *”,浏览器检测到重复的跨域头会直接报错拒绝访问。
网关层的统一配置
在生产环境中,千万不要盲目写 allow-credentials: true 配合 allowed-origins: “*”(新版 Spring 会直接报错)。如果需要携带 Cookie 等凭证,必须精准指定合法的域名白名单。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| spring: cloud: gateway: globalcors: cors-configurations: '[/**]': allowed-origin-patterns: - "https://*.koohub.com" - "http://localhost:[*]" allowed-methods: - "GET" - "POST" - "PUT" - "DELETE" - "OPTIONS" allowed-headers: - "Authorization" - "Content-Type" - "client-id" - "user-level" exposed-headers: - "Set-Cookie" allow-credentials: true max-age: 3600
|
目标服务层清理
因为网关层已经做完了全盘的 CORS 握手,下游的目标微服务什么都不需要配。务必排查并删除下游微服务代码中的以下两类配置,确保它们干干净净。
删除下游服务的配置类:
1 2 3 4 5 6 7 8
| @Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**").allowedOrigins("*"); } }
|
删除下游 Controller 上的注解:
1 2 3 4 5 6 7
| @RestController @RequestMapping("/user")
public class UserController { @GetMapping("/info") public String info() { return "user info"; } }
|
兼容策略
如果目标服务必须保留跨域,网关该怎么退让?比如如果你的目标服务是个老系统,里面的 @CrossOrigin 注解多得数不清、根本不敢乱删,那么我们可以利用 Gateway 的一个极其强大的过滤器,在网关转发给浏览器之前,强行把下游服务产生的重复跨域头给裁剪掉。在网关的 application.yml 的 default-filters(全局默认过滤器)中配置:
1 2 3 4 5 6
| spring: cloud: gateway: default-filters: - DedupeResponseHeader=Access-Control-Allow-Origin Access-Control-Allow-Credentials RETAIN_UNIQUE
|
通过这个配置,即使下游服务偷偷返回了跨域头,网关也会帮它擦干净屁股,确保浏览器接收到的是唯一且合法的 CORS 响应头。
整合 sentinel 流控降级
关于 sentinel 的各种玩法,可以参考我之前的文章 《Spring Cloud Alibaba 微服务系列 - Sentinel》。这里直接进入整合的正题。
网关应用需要的依赖
在网关模块 zdemo-gateway 的 pom.xml 中引入 Sentinel 的核心适配依赖和持久化数据源(以 Nacos 为例)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency>
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId> </dependency>
<dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-datasource-nacos</artifactId> </dependency>
|
基础配置
编辑配置文件 application.yml 或者 nacos 对应的配置,在网关的配置文件中,指定 Sentinel 控制台的连接地址,并开启网关维度的流控支持。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| server: port: 8080
spring: application: name: zdemo-gateway config: import: - optional:nacos:${spring.application.name}.${spring.cloud.nacos.config.file-extension}?refresh=true cloud: nacos: discovery: server-addr: 192.168.1.149:8848 username: nacos password: nacos namespace: prod-zdemo cluster-name: DEFAULT config: server-addr: 192.168.1.149:8848 username: nacos password: nacos file-extension: yml namespace: prod-zdemo cluster-name: DEFAULT group: DEFAULT_GROUP sentinel: transport: dashboard: 192.168.1.149:8858 port: 8719 client-ip: 192.168.1.5 scg: fallback: mode: response response-status: 429 response-body: '{"code":429,"message":"访问人数过多,请稍后再试!"}'
|
访问几次 zdemo-gatewa,这时候我们就可以访问 http://192.168.1.149:8858/ sentinel 的 dashboard,在显示出的 zdemo-sentinel 下去定义自己的流控或降级规则了(注意现在的流控或降级规则还只是存储在 sentinel 服务的内存中,并没有持久化)。
流控降级规则的持久化
Sentinel 默认的规则是保存在网关内存中的,网关一重启规则全部丢失。在生产环境中,必须将规则托管到 Nacos 中进行持久化。
在网关中配置 Nacos 数据源。修改 application.yml,增加 datasource 配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| server: port: 8080
spring: application: name: zdemo-gateway config: import: - optional:nacos:${spring.application.name}.${spring.cloud.nacos.config.file-extension}?refresh=true cloud: nacos: discovery: server-addr: 192.168.1.149:8848 username: nacos password: nacos namespace: prod-zdemo cluster-name: DEFAULT config: server-addr: 192.168.1.149:8848 username: nacos password: nacos file-extension: yml namespace: prod-zdemo cluster-name: DEFAULT group: DEFAULT_GROUP sentinel: transport: dashboard: 192.168.1.149:8858 port: 8719 client-ip: 192.168.1.5 scg: fallback: mode: response response-status: 429 response-body: '{"code":429,"message":"访问人数过多,请稍后再试!"}' datasource: gw-low-rules: nacos: server-addr: 192.168.1.149:8848 username: nacos password: nacos data-id: ${spring.application.name}-flow-rules.json groupId: SENTINEL_GROUP namespace: prod-zdemo data-type: json rule-type: gw-flow
|
在 Nacos 中创建规则 JSON 文件。登录 Nacos 控制台,在 prod-zdemo 命名空间下新建一个 Data ID 为 zdemo-gateway-flow-rules.json(对应你的应用名),Group 为 SENTINEL_GROUP 的 JSON 配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
[ { "resource": "user-route", "resourceMode": 0, "grade": 1, "count": 2, "interval": 1, "controlBehavior": 0 } ]
|
也可以先在测试环境的 sentinel 配置好,导出流控降级配置,并将其配置到 nacos 配置中心。这部分技巧可以参考 《实际 Sentinel Deshboard 玩法 - 可以优化的地方》。接下来就可以进行验证:
- 启动 Nacos 和 Sentinel 控制台。
- 启动网关服务 zdemo-gateway。
- 访问 xxx:8080/user-serv/dubbo/user/order?orderNo=1。
- 打开 Sentinel 控制台,在“请求链路”或“网关流控规则”中,你会发现持久化的 Nacos 规则已经被网关自动拉取并实时生效。
- 狂点刷新接口,当 1 秒内 QPS 超过 2 时,接口会瞬间切断并返回由 WebFlux 异步写回的流控响应。
这种结合了 WebFlux 非阻塞优势加 Nacos 动态持久化的方案,就是最稳健的网关防御术。
自定义限流异常回调
当触发限流或熔断时,网关默认会返回 Sentinel 内置的简陋文本。生产环境下,我们可以自定义非阻塞的回调处理器,使其返回与前端契合的标准 JSON 格式。
新建配置类:GatewaySentinelConfig
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.BlockRequestHandler; import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager; import org.springframework.http.MediaType; import jakarta.annotation.PostConstruct; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpStatus; import org.springframework.web.reactive.function.server.ServerResponse; import java.util.HashMap; import java.util.Map;
@Configuration public class GatewaySentinelConfig {
@PostConstruct public void initBlockHandlers() { BlockRequestHandler blockRequestHandler = (exchange, t) -> { Map<String, Object> errorResult = new HashMap<>(); errorResult.put("code", 429); errorResult.put("message", "系统繁忙,已被网关统一限流熔断!"); errorResult.put("path", exchange.getRequest().getPath().value());
return ServerResponse.status(HttpStatus.TOO_MANY_REQUESTS) .contentType(MediaType.APPLICATION_JSON) .bodyValue(errorResult); };
GatewayCallbackManager.setBlockHandler(blockRequestHandler); } }
|
并且去掉刚才我们定义的:
1 2 3 4 5 6 7 8
| spring: cloud: sentinel: scg: fallback: mode: response response-status: 429 response-body: '{"code":429,"message":"访问人数过多,请稍后再试!"}'
|
这样我们自定义的流控和降级响应就会生效。
标题:
Spring Cloud Gateway - 使用测试案例