一、前言 最近在辅助实施一个项目,使用nacos做为服务注册中心、配置中心,使用了Spring Cloud GateWay做为网关,由于前期我学习了 《Spring Cloud Zuul服务网关》 ,对GateWay网关不是很熟悉,但是大致明白配置和作用,这里我们就用实践来学习下GateWay。
Spring Cloud GateWay是Spring Cloud的⼀个全新项⽬,⽬标是取代Netflix Zuul,它基于Spring5.0+SpringBoot2.0+WebFlux(基于⾼性能的Reactor模式响应式通信框架Netty,异步⾮阻塞模型)等技术开发,性能⾼于Zuul,官⽅测试,GateWay是Zuul的1.6倍,旨在为微服务架构提供⼀种简单有效的统⼀的API路由管理⽅式。
二、Gateway 介绍 2.1 基础 Spring Cloud GateWay不仅提供统⼀的路由⽅式(反向代理)并且基于 Filter(定义过滤器对请求过滤,完成⼀些功能) 链的⽅式提供了⽹关基本的功能,例如:鉴权、流量控制、熔断、路径重写、⽇志监控等。
网关在架构中的位置,可以看到是请求进来由网关路由分配找到需要请求的服务,其中Nginx是用来做网管高可用的。
主流网关的对比与选型
Zuul1.x 阻塞式IO 2.x 基于Netty Spring Cloud GateWay天⽣就是异步⾮阻塞的,基于Reactor模型; ⼀个请求—>⽹关根据⼀定的条件匹配—匹配成功之后可以将请求转发到指定的服务地址;⽽在这个过程中,我们可以进⾏⼀些⽐较具体的控制(限流、⽇志、⿊⽩名 单)
路由(route) : ⽹关最基础的部分,也是⽹关⽐较基础的⼯作单元。路由由⼀个ID、⼀个⽬标URL(最终路由到的地址)、⼀系列的断⾔(匹配条件判断)和Filter过滤器(精细化控制)组成。如果断⾔为true,则匹配该路由。
断⾔(predicates) :参考了Java8中的断⾔java.util.function.Predicate,开发⼈员可以匹配Http请求中的所有内容(包括请求头、请求参数等)(类似于nginx中的location匹配⼀样),如果断⾔与请求相匹配则路由。
过滤器(filter) :⼀个标准的Spring webFilter,使⽤过滤器,可以在请求之前或者之后执⾏业务逻辑。Predicates断⾔就是我们的匹配条件,⽽Filter就可以理解为⼀个⽆所不能的拦截器,有了这两个元素,结合⽬标URL,就可以实现⼀个具体的路由转发。Spring Cloud GateWay 帮我们内置了很多 Predicates功能,实现了各种路由匹配规则(通过 Header、请求参数等作为条件)匹配到对应的路由。
一般都会使用请求路径正则匹配
1 2 3 4 5 6 7 8 spring: cloud: gateway: routes: # 路由可以有多个 - id: service-xxx-router # 我们⾃定义的路由 ID,保持唯⼀ uri: lb://server-name predicates: #路由条件 - Path=/xx/xxxx/**
2.2 执行流程
执行流程大体如下:
Gateway Client 向 Gateway Server 发送请求 请求首先会被 HttpWebHandlerAdapter 进行提取组装成网关上下文 然后网关的上下文会传递到 DispatcherHandler,它负责将请求分发给 RoutePredicateHandlerMapping RoutePredicateHandlerMapping 负责路由查找,并根据路由断言判断路由是否可用 如果过断言成功,由 FilteringWebHandler 创建过滤器链并调用 请求会一次经过 PreFilter -> 微服务 -> PostFilter 的方法,最终返回响应 三、环境搭建 为了更好的理解上边提到核心概念,我们现用简单的实战案例演示。
这里我们使用 《Spring Cloud Alibaba Nacos注册中心》 章节的代码为基础,新增gateway模块,具体服务如下:
模块名 服务名 端口 作用 provider provider 8900 服务提供者 consumer consumer 8901 服务消费者 gateway gateway 8888 网关
3.1 创建gateway 创建gateway模块
修改pom.xml 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 <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>com.wno704</groupId> <artifactId>spring-cloud-gateway</artifactId> <version>0.0.1-SNAPSHOT</version> <relativePath>../pom.xml</relativePath> </parent> <artifactId>gateway</artifactId> <name>Cloud-GateWay</name> <description>Demo project for Spring Boot</description> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
配置application.yml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 server: port: 8888 spring: application: name: gateway cloud: nacos: discovery: server-addr: 127.0.0.1:8848 username: nacos password: 19920503 gateway: discovery: locator: enabled: true # gateway 可以从 nacos 发现微服务
3.2 调整依赖 修改父项目pom.xml,如下:
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 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.4.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.wno704</groupId> <artifactId>spring-cloud-gateway</artifactId> <version>0.0.1-SNAPSHOT</version> <name>spring-cloud-gateway</name> <description>spring-cloud-gateway</description> <packaging>pom</packaging> <properties> <java.version>1.8</java.version> <spring-boot.version>2.3.4.RELEASE</spring-boot.version> <spring-cloud.version>Hoxton.SR8</spring-cloud.version> <spring-cloud-alibaba.version>2.2.3.RELEASE</spring-cloud-alibaba.version> </properties> <modules> <module>consumer</module> <module>provider</module> <module>gateway</module> </modules> <dependencies> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>${spring-boot.version}</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>${spring-cloud-alibaba.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
将spring-boot-starter-web依赖分别填到consumer和provider依赖中,由于gateway网关配置web依赖会存在问题,此处做调整。
3.3 测试服务网关 启动三个模块,当前注册服务如下:
使用http工具请求: http://localhost:8901/consume/hello/nacos
请求规则:网关地址/微服务应用名/接口http://localhost:8888/consumer/consume/hello/nacos
请求成功,网关项目搭建完成。
简单配置路由规则 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 server: port: 8888 spring: application: name: gateway cloud: nacos: discovery: server-addr: 127.0.0.1:8848 username: nacos password: 19920503 gateway: discovery: locator: enabled: true routes: - id: consumer_route uri: lb://consumer # lb 表示从 nacos 中按照名称获取微服务,并遵循负载均衡策略,consumer 对应用户微服务应用名 predicates: - Path=/consumer-api/** # 使用断言 filters: - StripPrefix=1 # 使用过滤器
其中:
id: 路由标识符,区别于其他 Route uri:路由指向的目的地 uri,即客户端请求最终被转发到的微服务 predicate:断言,用于条件判断,只有断言都返回真,才会真正的执行路由 filter:过滤器用于修改请求和响应信息 添加 routes 相关配置,重启网关项目,请求用户微服务接口。
测试配置路由规则 请求规则:网关地址/断言配置的 Path 路径/接口http://localhost:8888/consumer-api/consume/hello/nacos
路由规则生效。
简单的使用了路由规则,下文将具体介绍路由规则的使用方式。
四、断言 Predicate(断言, 谓词) 用于进行条件判断,只有断言都返回真,才会真正的执行路由。
SpringCloud Gateway 的断言通过继承 AbstractRoutePredicateFactory 类实现,因此我们可以根据自己的需求自定义断言。
当然,开发团队已为使用者提供了一些内置断言工厂,在开发中已足够使用,请继续阅读下文。
4.1 内置断言 Spring Cloud Gateway 包括 11 种内置的断言工厂,所有这些断言都与 HTTP 请求的不同属性匹配。
补充
:断言可以同时使用
AfterRoutePredicateFactory: 接收一个日期参数,判断请求日期是否晚于指定日期 使用方式如下:
1 2 3 predicates: - Path=/consumer-api/** - After=2022-05-15T22:35:00.000+08:00[Asia/Shanghai]
测试如下:
BetweenRoutePredicateFactory: 接收两个日期参数,判断请求日期是否在指定时间段内 使用方式如下:
1 2 3 predicates: - Path=/consumer-api/** - Between=2022-05-15T22:38:00.000+08:00[Asia/Shanghai],2022-05-15T22:40:00.000+08:00[Asia/Shanghai]
测试如下:
BeforeRoutePredicateFactory: 接收一个日期参数,判断请求日期是否早于指定日期 使用方式如下:
1 2 3 predicates: - Path=/consumer-api/** - Before=2022-05-15T22:47:00.000+08:00[Asia/Shanghai]
测试如下:
CookieRoutePredicateFactory: 接收两个参数,cookie 名字和值。 判断请求 cookie 是否具有给定名称且值与正则表达式匹配。 使用方式如下:
1 2 3 predicates: - Path=/consumer-api/** - Cookie=user, wno704
测试如下:
HeaderRoutePredicateFactory:接收两个参数,标题名称和正则表达式。 判断请求 Header 是否具有给定名称且值与正则表达式匹配。 使用方式如下:
1 2 3 predicates: - Path=/consumer-api/** - Header=X-Request-Id, \d+
其中,X-Request-Id 为 header 名称,\d+ 为正则表达式,表示数字。
测试如下:
HostRoutePredicateFactory:接收一个参数,主机名模式。判断请求的 Host 是否满足匹配规则。 使用方式如下:
1 2 3 predicates: - Path=/consumer-api/** - **.somehost.org,**.anotherhost.org
支持 URI 模板变量(例如{sub} .myhost.org),如果请求的主机标头的值为 www.somehost.org 或 beta.somehost.org 或 www.anotherhost.org,则此路由匹配。
MethodRoutePredicateFactory: 接收一个参数,判断请求类型是否跟指定的类型匹配。 使用方式如下:
1 2 3 predicates: - Path=/consumer-api/** - Method=GET,POST
如果请求方法是 GET 或 POST,则此路由匹配
PathRoutePredicateFactory:接收一个参数,判断请求的 URI 部分是否满足路径规则。 使用方式如下:
1 2 predicates: - Path=/consumer-api/**
这个就是我们在上边配置的断言,请求是 /consumer-api/ 开头,则路由到用户微服务上。
QueryRoutePredicateFactory:接收两个参数,请求 param 和正则表达式, 判断请求参数是否具有给定名称且值与正则表达式匹配。 使用方式如下:
1 2 3 predicates: - Path=/consumer-api/** - Query=user, \d+
请求包含名称为 user 的参数,且参数值为数字,则匹配路由。
测试如下:
RemoteAddrRoutePredicateFactory:接收一个 IP 地址段,判断请求主机地址是否在地址段中。 使用方式如下:
1 2 3 predicates: - Path=/consumer-api/** - RemoteAddr=192.168.0.1/16
其中,192.168.0.1 是 IP 地址,而 16 是子网掩码。当请求的远程地址为该值时,匹配路由。
WeightRoutePredicateFactory:接收一个[组名,权重], 然后对于同一个组内的路由按照权重转发。 使用方式如下:
1 2 3 4 5 6 7 8 9 10 11 12 spring: cloud: gateway: routes: - id: weight_high uri: https://weighthigh.org predicates: - Weight=group1, 8 - id: weight_low uri: https://weightlow.org predicates: - Weight=group1, 2
配置多组路由规则时使用。路由会将约 80% 的流量转发至 weighthigh.org,并将约 20% 的流量转发至 weightlow.org。
4.2 自定义断言 当内置的断言不满足我们的业务需求时,我们可以自定义断言工厂。
比如,我们需要判断请求 url 中传过来的 age 值在 18~60 范围才可正常路由;判断请求参数中str的存在配置表中可以通过。
4.2.1 配置断言 1 2 - ExistsCheck=wno-wno704-704 - Age=18, 60
4.2.2 创建断言判断类 我们需要创建两个类继承 AbstractRoutePredicateFactory 类:
注意
:自定义类名有格式要求-> 断言名称 + RoutePredicateFactory。此处断言名称为 Age,对应配置文件中的 Age;断言名称为 ExistsCheck,对应配置文件中的 ExistsCheck。
AgeRoutePredicateFactory.java
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 @Component public class AgeRoutePredicateFactory extends AbstractRoutePredicateFactory<AgeRoutePredicateFactory.Config> { public AgeRoutePredicateFactory() { super(AgeRoutePredicateFactory.Config.class); } @Override public List<String> shortcutFieldOrder() { return Arrays.asList("minAge", "maxAge"); } @Override public Predicate<ServerWebExchange> apply(AgeRoutePredicateFactory.Config config) { return new Predicate<ServerWebExchange>() { @Override public boolean test(ServerWebExchange serverWebExchange) { // 判断逻辑 String ageStr = serverWebExchange.getRequest().getQueryParams().getFirst("age"); if (ageStr == null || ageStr.length() == 0) { return false; } int age = Integer.parseInt(ageStr); return age > config.getMinAge() && age < config.getMaxAge(); } }; } @Data static class Config { private int minAge; private int maxAge; } }
ExistsCheckRoutePredicateFactory.java
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 @Component public class ExistsCheckRoutePredicateFactory extends AbstractRoutePredicateFactory<ExistsCheckRoutePredicateFactory.Config> { public ExistsCheckRoutePredicateFactory() { super(ExistsCheckRoutePredicateFactory.Config.class); } @Override public List<String> shortcutFieldOrder() { return Arrays.asList("containsStr"); } @Override public Predicate<ServerWebExchange> apply(Config config) { return new Predicate<ServerWebExchange>() { @Override public boolean test(ServerWebExchange serverWebExchange) { String optStr = serverWebExchange.getRequest().getQueryParams().getFirst("str"); if (optStr == null || optStr.length() == 0) { return false; } return config.getContainsStr().contains(optStr); } }; } @Data static class Config { private String containsStr; } }
4.2.3 测试 保存,重启网关项目,测试结果如下:
五、过滤器 5.1 局部过滤器 局部过滤器是针对单个路由的过滤器。
Spring Cloud Gateway 也提供了 31 种局部的内置 GatewayFilter 工厂。
5.1.1 使用方式 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 server: port: 8888 spring: application: name: gateway cloud: nacos: discovery: server-addr: 127.0.0.1:8848 username: nacos password: 19920503 gateway: discovery: locator: enabled: true routes: - id: consumer_route uri: lb://consumer predicates: - Path=/consumer-api/** - ExistsCheck=wno-wno704-704 - Age=18, 60 filters: - StripPrefix=1 # 使用过滤器
同样地,当内置的局部过滤器不符合我们的业务需求时,我们也可以自定义过滤器。
比如:我们需要在调用/路由一个接口之前打印一下日志。
5.1.2 配置局部过滤器 5.1.3 创建过滤器类 注意
:自定义类名有格式要求-> 过滤器名称 + GatewayFilterFactory。此处过滤器名称为 Log,对应配置文件中的 Log。
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 @Component public class LogGatewayFilterFactory extends AbstractGatewayFilterFactory<LogGatewayFilterFactory.Config> { public LogGatewayFilterFactory() { super(LogGatewayFilterFactory.Config.class); } @Override public List<String> shortcutFieldOrder() { return Arrays.asList("open"); } @Override public GatewayFilter apply(Config config) { return new GatewayFilter() { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { if (config.open) { System.out.printf("hostname:%s\tip%s:%s\tpath:%s\theader:%s\r\n",exchange.getRequest().getRemoteAddress().getHostName(), exchange.getRequest().getRemoteAddress().getAddress().getHostAddress(), exchange.getRequest().getRemoteAddress().getPort(), exchange.getRequest().getURI().getPath(), exchange.getRequest().getHeaders().toString()); } return chain.filter(exchange); } }; } @Data static class Config { private boolean open; } }
5.1.4 测试 保存,重启网关项目,测试结果如下:
5.2 全局过滤器 全局过滤器作用于所有路由, 无需配置。通过全局过滤器可以实现对权限的统一校验,安全性验证等功能。
同样地,框架也内置了一些全局过滤器,它们都实现 GlobalFilter 和 Ordered 接口。有兴趣的读者可以自行查看 GlobalFilter 的实现类或浏览下文提供的官方文档获取详细信息。
这里我们主要演示自定义全局过滤器。
比如:我们在接受请求时需要验证 token。
由于是全局过滤器,因此无需修改配置文件,需要定义类实现 GlobalFilter 和 Ordered 接口。
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 @Component public class TokenGlobalFilter implements GlobalFilter, Ordered { @SneakyThrows @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { String token = exchange.getRequest().getQueryParams().getFirst("token"); if (token == null || token.length() == 0 || !token.equals("123456")) { System.out.println("鉴权失败"); ServerHttpResponse response = exchange.getResponse(); response.setStatusCode(HttpStatus.OK); response.getHeaders().add("Content-Type", "application/json;charset=UTF-8"); // 鉴权失败,返回的数据结构 Map<String, Object> map = new HashMap<>(); map.put("code", HttpStatus.UNAUTHORIZED.value()); map.put("message", HttpStatus.UNAUTHORIZED.getReasonPhrase()); DataBuffer buffer = response.bufferFactory().wrap(new ObjectMapper().writeValueAsBytes(map)); return response.writeWith(Flux.just(buffer)); } return chain.filter(exchange); } @Override public int getOrder() { return 0; } }
保存,重启网关项目,测试结果如下:
token 验证失败,返回 401,鉴权失败的提示;token 验证成功,返回接口结果。
六、路由失败处理 当请求路由地址不匹配或断言为 false 时,Gateway 会默认返回 Whitelabel Error Page 错误页面,这种错误提示不符合我们业务需求。
我们可以自定义返回一个较为友好的错误提示,需要创建一个类继承 DefaultErrorWebExceptionHandler 类,重写其方法: 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 public class ErrorWebExceptionHandler extends DefaultErrorWebExceptionHandler { public ErrorWebExceptionHandler(ErrorAttributes errorAttributes, ResourceProperties resourceProperties, ErrorProperties errorProperties, ApplicationContext applicationContext) { super(errorAttributes, resourceProperties, errorProperties, applicationContext); } @Override protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) { return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse); } @Override protected Mono<ServerResponse> renderErrorResponse(ServerRequest request) { boolean includeStackTrace = isIncludeStackTrace(request, MediaType.ALL); Map<String, Object> errorMap = getErrorAttributes(request, includeStackTrace); int status = Integer.valueOf(errorMap.get("status").toString()); Map<String, Object> response = response(status, errorMap.get("error").toString(), errorMap); return ServerResponse.status(status).contentType(MediaType.APPLICATION_JSON) .body(BodyInserters.fromValue(response)); } // 我们希望返回的数据结构 public static Map<String, Object> response(int status, String errorMessage, Map<String, Object> errorMap) { Map<String, Object> map = new HashMap<>(); map.put("code", status); map.put("message", errorMessage); map.put("data", errorMap); return map; } }
配置 Bean 实例: 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 @Configuration public class GatewayConfiguration { private final ServerProperties serverProperties; private final ApplicationContext applicationContext; private final ResourceProperties resourceProperties; private final List<ViewResolver> viewResolvers; private final ServerCodecConfigurer serverCodecConfigurer; public GatewayConfiguration(ServerProperties serverProperties, ApplicationContext applicationContext, ResourceProperties resourceProperties, ObjectProvider<List<ViewResolver>> viewResolversProvider, ServerCodecConfigurer serverCodecConfigurer) { this.serverProperties = serverProperties; this.applicationContext = applicationContext; this.resourceProperties = resourceProperties; this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList); this.serverCodecConfigurer = serverCodecConfigurer; } @Bean("errorWebExceptionHandler") @Order(Ordered.HIGHEST_PRECEDENCE) public ErrorWebExceptionHandler errorWebExceptionHandler(ErrorAttributes errorAttributes) { ErrorWebExceptionHandler exceptionHandler = new ErrorWebExceptionHandler( errorAttributes, this.resourceProperties, this.serverProperties.getError(), this.applicationContext); exceptionHandler.setViewResolvers(this.viewResolvers); exceptionHandler.setMessageWriters(this.serverCodecConfigurer.getWriters()); exceptionHandler.setMessageReaders(this.serverCodecConfigurer.getReaders()); return exceptionHandler; } }
保存后重启网关项目,请求一个错误的接口地址,结果如下:
请求的 url 地址不匹配路由规则返回我们定义的错误提示。
七、跨域问题 针对 PC 端的页面请求,如果项目前后端分离,则请求会出现跨域请求问题。为什么呢?接着看。
URL 由协议、域名、端口和路径组成,如果两个 URL 的协议、域名和端口相同,则表示它们同源,否则反之。
浏览器提供同源策略,限制了来自不同源的 document 或脚本,对当前 document 读取或设置某些属性。其目的是为了保证用户信息的安全,防止恶意的网站窃取数据。
下面演示跨域问题,编写一个简单页面:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8"> </head> <body> <button id="sendBtn">发送请求</button> <script src="./js/jquery.min.js"></script> <script type="text/javascript"> $(function() { $("#sendBtn").on("click", function() { $.ajax({ type: "GET", url: "http://localhost:8888/consumer-api/consume/hello/nacos?str=wno705&age=33&token=123456", success: function(resp) { console.log(resp); } }) }); }); </script> </body> </html>
启动一个服务容器(我们本地采用了Hbuilder),分配了8849端口,请求结果如下:
由于请求端的端口与网关端口不一致,不是同源,因此出现跨域问题。
解决方案有两种,如下:
修改配置文件 1 2 3 4 5 6 7 8 9 spring: cloud: gateway: globalcors: cors-configurations: '[/**]': allowedOrigins: "*" allowedMethods: "*" allowedHeaders: "*"
配置 CorsWebFilter 过滤器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Configuration public class CorsConfig { @Bean public CorsWebFilter corsFilter() { CorsConfiguration config = new CorsConfiguration(); config.addAllowedMethod("*"); config.addAllowedOrigin("*"); config.addAllowedHeader("*"); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser()); source.registerCorsConfiguration("/**", config); return new CorsWebFilter(source); } }
八、整合 Sentinel 网关作为微服务,我们也可以对其进行限流和降级操作。不熟悉 Sentinel 的读者可以先打开 传送门 浏览相关文章。
注意
:配置前记得启动 Sentinel 控制台,具体可参考 《Spring Cloud Alibaba Sentinel控制台详解》 。
8.1 基础整合 8.1.1 添加依赖 1 2 3 4 5 6 7 8 9 <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency> <dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-spring-cloud-gateway-adapter</artifactId> </dependency>
8.2 修改配置文件,连接 Sentinel 控制台 1 2 3 4 5 6 spring: cloud: sentinel: transport: port: 8889 dashboard: localhost:8080
8.3 配置 Sentinel Filter 实例 1 2 3 4 5 6 7 8 9 @Configuration public class GatewayConfiguration { @Bean @Order(-1) public GlobalFilter sentinelGatewayFilter() { return new SentinelGatewayFilter(); } }
最后,重启网关微服务,在 Sentinel 控制台查看或配置规则即可。
8.2 异常处理器 在 Sentinel 控制台配置规后,服务出现限流或降级时,我们需要服务端返回友好的异常信息,而不是一个简单的错误页面。
在上篇文章中介绍了自定义异常处理器,即实现 BlockExceptionHandler 接口来完成功能。但是,Gateway 整合 Sentienl 后,该方案就失效了。
我们需要配置 BlockRequestHandler 实例。
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 @Configuration public class GatewayConfiguration { @Bean @Order(-1) public GlobalFilter sentinelGatewayFilter() { return new SentinelGatewayFilter(); } @Bean(name = "myBlockRequestHandler") public BlockRequestHandler myBlockRequestHandler() { BlockRequestHandler blockRequestHandler = new BlockRequestHandler() { @SneakyThrows @Override public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) { Result result; if (throwable instanceof FlowException) { result = Result.builder().code(100).msg("接口限流了").build(); } else if (throwable instanceof DegradeException) { result = Result.builder().code(101).msg("服务降级了").build(); } else if (throwable instanceof ParamFlowException) { result = Result.builder().code(102).msg("热点参数限流了").build(); } else if (throwable instanceof SystemBlockException) { result = Result.builder().code(103).msg("触发系统保护规则").build(); } else if (throwable instanceof AuthorityException) { result = Result.builder().code(104).msg("授权规则不通过").build(); } else { result = Result.builder().code(105).msg("sentinel 未知异常").build(); } return ServerResponse.status(HttpStatus.BAD_GATEWAY) .contentType(MediaType.APPLICATION_JSON) .body(BodyInserters.fromValue(new ObjectMapper().writeValueAsString(result))); } }; return blockRequestHandler; } @Bean @Order(Ordered.HIGHEST_PRECEDENCE) public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler(BlockRequestHandler myBlockRequestHandler) { //重定向bloack处理 //GatewayCallbackManager.setBlockHandler(new RedirectBlockRequestHandler("https://www.extlight.com")); //自定义bloack处理 GatewayCallbackManager.setBlockHandler(myBlockRequestHandler); return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer); } }
Result.java
1 2 3 4 5 6 7 8 @Data @Builder public class Result { private int code; private String msg; }
注意:当多个 Bean 上都配置 @Order 注解时,要多留意 order 值,否则接口请求后达不到预期效果。
参考 《官方文档》