大纲 前言 版本说明 在本文中,默认使用的 Spring Cloud 版本是 Finchley.RELEASE,对应的 Spring Boot 版本是 2.0.3,特别声明除外。
Gateway 基于服务发现的路由规则 Gateway 服务发现路由的概述 Spring Cloud 对 Zuul 进行封装处理之后,当通过 Zuul 访问后端微服务时,基于服务发现的默认路由规则是:http://zuul_host:zuul_port/微服务在Eureka上的ServiceId/**
。Spring Cloud Gateway 在设计的时候考虑了从 Zuul 迁移到 Gateway 的兼容性和迁移成本等,Gateway 基于服务发现的路由规则和 Zuul 的设计类似,但是也有很大差别。Spring Cloud Gateway 基于服务发现的路由规则,在不同注册中心下其差异如下:
如果把 Gateway 注册到 Consul 上,通过网关转发服务调用,服务名称默认小写,不需要做任何处理 如果把 Gateway 注册到 Zookeeper 上,通过网关转发服务调用,服务名称默认小写,不需要做任何处理 如果把 Gateway 注册到 Eureka 上,通过网关转发服务调用,访问网关的 URL 是 http://Gateway_HOST:Gateway_PORT/大写的ServiceId/**
,其中服务名称默认必须是大写,否则会抛 404 错误;如果服务名要用小写访问,可以在属性配置文件里面加 spring.cloud.gateway.discovery.locator.lowerCaseServiceId=true
配置解决 特别注意
将 Gateway 注册到服务注册中心后,为了在外部可以通过服务名称让 Gateway 转发服务调用,需要在配置文件中设置 spring.cloud.gateway.discovery.locator.enabled
为 true
,表示 Gateway 需要与服务发现组件进行结合使用。 配置之后就可以通过服务名称(serviceId
)将请求转发到具体的服务实例。比如 http://localhost:8080/cloud-payment-service/pay/list
,其中 http://localhost:8080
是 Gateway 的访问地址,cloud-payment-service
是服务名称。 Gateway 服务发现路由的使用 下面将使用 Eureka 作为注册中心来剖析 Gateway 服务发现的路由规则,其中各个模块的说明如下,由于篇幅有限,这里只给出核心的配置和代码,点击下载 完整的案例代码。
模块 端口 说明 micro-service-gateway-route N/A 聚合父 Maven 工程 micro-service-eureka 9000 Eureka 注册中心 micro-service-gateway 9001 基于 Spring Cloud Gateway 的网关服务 micro-service-provider 9002 服务提供者 micro-service-consumer 9003 服务消费者
1. 创建 Maven 父级 Pom 工程 在父工程里面配置好工程需要的父级依赖,目的是为了更方便管理与简化配置,具体 Maven 配置如下:
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 <properties > <java.version > 1.8</java.version > <project.build.sourceEncoding > UTF-8</project.build.sourceEncoding > <project.reporting.outputEncoding > UTF-8</project.reporting.outputEncoding > </properties > <parent > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-parent</artifactId > <version > 2.0.3.RELEASE</version > </parent > <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > </dependency > </dependencies > <dependencyManagement > <dependencies > <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-dependencies</artifactId > <version > Finchley.RELEASE</version > <type > pom</type > <scope > import</scope > </dependency > </dependencies > </dependencyManagement > <repositories > <repository > <id > spring-milestones</id > <name > Spring Milestones</name > <url > https://repo.spring.io/libs-milestone</url > <snapshots > <enabled > false</enabled > </snapshots > </repository > </repositories > <build > <plugins > <plugin > <groupId > org.apache.maven.plugins</groupId > <artifactId > maven-compiler-plugin</artifactId > <configuration > <source > ${java.version}</source > <target > ${java.version}</target > </configuration > </plugin > </plugins > </build >
2. 创建 Micro Service Eureka 工程 创建 Micro Service Eureka 的 Maven 工程,配置工程里的 pom.xml 文件:
1 2 3 4 <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-netflix-eureka-server</artifactId > </dependency >
创建 Micro Service Eureka 的启动主类:
1 2 3 4 5 6 7 8 @EnableEurekaServer @SpringBootApplication public class EurekaServerApplication { public static void main (String[] args) { SpringApplication.run(EurekaServerApplication.class, args); } }
创建 Micro Service Eureka 的 application.yml
配置文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 server: port: 9000 spring: application: name: eureka-server eureka: instance: hostname: localhost client: register-with-eureka: false fetch-registry: false service-url: defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
3. 创建 Micro Service Gateway 工程 创建 Micro Service Gateway 的 Maven 工程,配置工程里的 pom.xml
文件,由于需要将 Gateway 服务注册到 Eureka,因此需要引入 Eureka Client;同时为了避免 Gateway 的依赖冲突,排除引入 spring-webmvc
、spring-boot-starter-tomcat
:
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 <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-gateway</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-actuator</artifactId > </dependency > <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-netflix-eureka-client</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > <exclusions > <exclusion > <groupId > org.springframework</groupId > <artifactId > spring-webmvc</artifactId > </exclusion > <exclusion > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-tomcat</artifactId > </exclusion > </exclusions > </dependency >
创建 Micro Service Gateway 的启动主类:
1 2 3 4 5 6 7 @SpringBootApplication public class GatewayServerApplication { public static void main (String[] args) { SpringApplication.run(GatewayServerApplication.class, args); } }
创建 Micro Service Gateway 的 application.yml
配置文件,其中 spring.cloud.gateway.discovery.locator.enabled
表示是否与服务发现组件进行结合,默认为 false
。如果设置为 true
,则表示 Gateway 开启基于服务发现的路由规则,即可以通过服务名称(serviceId
)将请求转发到具体的服务实例。spring.cloud.gateway.discovery.locator.lowerCaseServiceId
设置为 true
,表示当注册中心为 Eureka 时,让 Gateway 开启用小写的服务名称(serviceId
)进行基于服务路由的转发。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 server: port: 9001 spring: application: name: gateway-server cloud: gateway: discovery: locator: enabled: true lower-case-service-id: true eureka: client: service-url: defaultZone: http://127.0.0.1:9000/eureka instance: instance-id: gateway-server-${server.port} prefer-ip-address: true
4. 创建 Micro Service Provider 工程 创建 Micro Service Provider 的 Maven 工程,配置工程里的 pom.xml 文件:
1 2 3 4 <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-netflix-eureka-client</artifactId > </dependency >
创建 Micro Service Provider 的启动主类:
1 2 3 4 5 6 7 8 @EnableDiscoveryClient @SpringBootApplication public class ProviderApplication { public static void main (String[] args) { SpringApplication.run(ProviderApplication.class, args); } }
创建 Micro Service Provider 的测试控制类:
1 2 3 4 5 6 7 8 9 10 11 12 @RestController @RequestMapping("/provider") public class ProviderController { @Value("${server.port}") private String port; @GetMapping("/sayHello/{name}") public String sayHello (@PathVariable("name") String name) { return "from port: " + port + ", hello " + name; } }
创建 Micro Service Provider 的 application.yml
配置文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 server: port: 9002 spring: application: name: provider-service eureka: client: service-url: defaultZone: http://127.0.0.1:9000/eureka instance: instance-id: provider-service-${server.port} prefer-ip-address: true
5. 创建 Micro Service Consumer 工程 创建 Micro Service Consumer 的 Maven 工程,配置工程里的 pom.xml 文件:
1 2 3 4 5 6 7 8 <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-netflix-eureka-client</artifactId > </dependency > <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-openfeign</artifactId > </dependency >
创建 Micro Service Consumer 的启动主类:
1 2 3 4 5 6 7 8 9 @EnableFeignClients @EnableDiscoveryClient @SpringBootApplication public class ConsumerApplication { public static void main (String[] args) { SpringApplication.run(ConsumerApplication.class, args); } }
创建 Micro Service Consumer 的服务调用接口:
1 2 3 4 5 6 @FeignClient("provider-service") public interface ProviderService { @RequestMapping(value = "/provider/sayHello/{name}", method = RequestMethod.GET) public String sayHello (@PathVariable("name") String name) ; }
创建 Micro Service Consumer 的测试控制类:
1 2 3 4 5 6 7 8 9 10 11 12 @RestController @RequestMapping("/consumer") public class ConsumerController { @Autowired private ProviderService providerService; @GetMapping("/sayHello/{name}") public String sayHello (@PathVariable("name") String name) { return providerService.sayHello(name); } }
创建 Micro Service Consumer 的 application.yml
配置文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 server: port: 9003 spring: application: name: consumer-service eureka: client: service-url: defaultZone: http://127.0.0.1:9000/eureka instance: instance-id: consumer-service-${server.port} prefer-ip-address: true
6. 测试案例代码 (1) 依次启动 micro-service-eureka、micro-service-gateway、micro-service-provider、micro-service-consumer 应用 (2) 访问 http://127.0.0.1:9000/
,查看各个服务是否都成功注册到 Eureka (3) 访问 http://127.0.0.1:9001/consumer-service/consumer/sayHello/Peter
,其中的 consumer-service
是 Consumer 服务的名称,查看是否可以通过 Gateway 正常调用 Consumer 服务的接口 Gateway Filter 和 Global Filter Spring Cloud Gateway 中的 Filter 从接口实现上分为两种:一种是 Gateway Filter,另外一种是 Global Filter。下面将给出这两种 Filter 的自定义使用示例,点击下载 完整的案例代码。
Gateway Filter 和 Global Filter 的概述 Gateway Filter :
从 Web Filter 中复制过来的,相当于一个 Filter 过滤器,可以对访问的 URL 过滤,进行横切处理(切面处理),应用场景包括异常处理、权限控制等。 Gateway Filter 也称为网关过滤器,主要作用于单一路由或者一个路由分组上。 Global Filter :
Global Filter 是一个全局的 Filter,作用于所有路由。 Global Filter 是 Gateway 出厂默认已有的,直接用即可,不需要在配置文件中配置。 Spring Cloud Gateway 定义了 Global Filter 的接口,可以让开发者自定义实现自己的 Global Filter。 Gateway Filter 和 Global Filter 的区别 从路由的作用范围来看,Global Filter 会被应用到所有的路由上,而 Gateway Filter 则应用到单个路由或者一个分组的路由上。从源码设计来看,Gateway Filter 和 Global Filter 两个接口中定义的方法一样,都是 Mono filter()
,唯一的区别就是 Gateway Filter 继承了 ShortcutConfigurable
,而 Global Filter 没有任何继承。
自定义 Gateway Filter 的案例 自定义 Gateway Filter 有以下两种实现方式:
(1) 实现 GatewayFilter 和 Ordered 接口 (2) 继承 AbstractGatewayFilterFactory 类 自定义案例一 这里通过实现 GatewayFilter 和 Ordered 接口的方式来自定义的 Gateway Filter,用于对匹配上(满足路由断言)的请求统计调用耗时情况,后续可以用于接口的性能分析。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public class CustomGatewayFilter implements GatewayFilter , Ordered { private static final Logger logger = LoggerFactory.getLogger(CustomGatewayFilter.class); private static final String COUNT_START_TIME = "countProcessTime" ; @Override public Mono<Void> filter (ServerWebExchange exchange, GatewayFilterChain chain) { exchange.getAttributes().put(COUNT_START_TIME, System.currentTimeMillis()); return chain.filter(exchange).then( Mono.fromRunnable(() -> { Long startTime = exchange.getAttribute(COUNT_START_TIME); if (startTime != null ) { Long countTime = System.currentTimeMillis() - startTime; logger.info(exchange.getRequest().getURI().getRawPath() + ": " + countTime + " ms" ); } })); } @Override public int getOrder () { return Ordered.LOWEST_PRECEDENCE; } }
将自定义的 Gateway Filter 配置到路由上,由于 Gateway Filter 是作用于单个路由或者一个分组的路由上的,因此这里需要使用 Java 的流式 API 绑定 Gateway Filter 和路由 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Configuration public class CommonConfiguration { @Bean public RouteLocator customGatewayFilter (RouteLocatorBuilder builder) { return builder.routes() .route(r -> r.path("/custom/gateway/filter" ) .filters(f -> f.filter(new CustomGatewayFilter())) .uri("http://127.0.0.1:9090/provider/sayHello/Jim/" ) .order(0 ) .id("custom-gateway-filter" ) ) .build(); } }
自定义案例二 这里通过继承 AbstractGatewayFilterFactory 的方式来自定义的 Gateway Filter,用于对匹配上(满足路由断言)的请求进行 URL 校验,也就是判断在请求的参数列表中是否有指定的参数,如果没有指定的参数,则视为无效请求。
特别注意
当使用继承 AbstractGatewayFilterFactory 类的方式来自定义 Gateway Filter 时,其类名必须以 GatewayFilterFactory
为后缀结尾。
创建自定义的 Gateway Filter,建议参考 Gateway 默认提供的 SetPathGatewayFilterFactory 过滤器工厂类的写法 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 import jakarta.validation.constraints.NotEmpty;import org.springframework.cloud.gateway.filter.GatewayFilter;import org.springframework.cloud.gateway.filter.GatewayFilterChain;import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;import org.springframework.http.HttpStatus;import org.springframework.stereotype.Component;import org.springframework.validation.annotation.Validated;import org.springframework.web.server.ServerWebExchange;import reactor.core.publisher.Mono;import java.util.Collections;import java.util.List;@Component public class CustomGatewayFilterFactory extends AbstractGatewayFilterFactory <CustomGatewayFilterFactory .Config > { public static final String PARAM_KEY = "param" ; public CustomGatewayFilterFactory () { super (Config.class); } @Override public GatewayFilter apply (Config config) { return new GatewayFilter() { @Override public Mono<Void> filter (ServerWebExchange exchange, GatewayFilterChain chain) { String value = exchange.getRequest().getQueryParams().getFirst(config.getParam()); if (null == value || value.isEmpty()) { exchange.getResponse().setStatusCode(HttpStatus.BAD_REQUEST); return exchange.getResponse().setComplete(); } return chain.filter(exchange); } }; } @Override public List<String> shortcutFieldOrder () { return Collections.singletonList(PARAM_KEY); } @Validated public static class Config { @NotEmpty private String param; public String getParam () { return param; } public void setParam (String param) { this .param = param; } } }
使用快捷方式来配置自定义的 Gateway Filter 1 2 3 4 5 6 7 8 9 10 spring: cloud: gateway: routes: - id: custom_filter_route uri: lb://cloud-payment-service predicates: - Path=/pay/info/** filters: - Custom=authToken
或者使用完全展开的方式来配置自定义的 Gateway Filter 1 2 3 4 5 6 7 8 9 10 11 12 spring: cloud: gateway: routes: - id: custom_filter_route uri: lb://cloud-payment-service predicates: - Path=/pay/info/** filters: - name: Custom args: param: authToken
自定义 Global Filter 的案例 自定义 Global Filter,只需要实现 CustomGlobalFilter 和 Ordered 这两个接口,然后将自定义 Golball Filter 的实例注入进 Spring 的 IOC 容器内即可。值得一提的是,自定义的 Global Filter 默认是作用在所有的路由上(即全局生效)。
自定义案例一 这里定义一个名为 CustomGlobalFilter 的全局过滤器,对请求到网关的 URL 进行权限校验,也就是判断请求的 URL 是否为合法请求。全局过滤器处理的逻辑是通过从 Gateway 的 上下文 ServerWebExchange 对象中获取 authToken
对应的值进行判 Null 处理,也可以根据需求定制开发更复杂的校验逻辑。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Component public class CustomGlobalFilter implements GlobalFilter , Ordered { @Override public Mono<Void> filter (ServerWebExchange exchange, GatewayFilterChain chain) { String token = exchange.getRequest().getQueryParams().getFirst("authToken" ); if (null == token || token.isEmpty()) { exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); } return chain.filter(exchange); } @Override public int getOrder () { return -400 ; } }
自定义案例二 这里通过自定义的 Global Filter,统计所有接口的调用耗时情况,后续可以用于接口的性能分析。
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 import lombok.extern.slf4j.Slf4j;import org.springframework.cloud.gateway.filter.GatewayFilterChain;import org.springframework.cloud.gateway.filter.GlobalFilter;import org.springframework.core.Ordered;import org.springframework.stereotype.Component;import org.springframework.web.server.ServerWebExchange;import reactor.core.publisher.Mono;@Slf4j @Component public class CustomGlobalFilter implements GlobalFilter , Ordered { public static final String BEGIN_VISIT_TIME = "begin_visit_time" ; @Override public Mono<Void> filter (ServerWebExchange exchange, GatewayFilterChain chain) { exchange.getAttributes().put(BEGIN_VISIT_TIME, System.currentTimeMillis()); return chain.filter(exchange).then(Mono.fromRunnable(() -> { Long beginVisitTime = exchange.getAttribute(BEGIN_VISIT_TIME); if (beginVisitTime != null ) { log.info("主机: " + exchange.getRequest().getURI().getHost()); log.info("端口: " + exchange.getRequest().getURI().getPort()); log.info("URL: " + exchange.getRequest().getURI().getPath()); log.info("URL参数: " + exchange.getRequest().getURI().getRawQuery()); log.info("处理耗时: " + (System.currentTimeMillis() - beginVisitTime) + "ms" ); log.info("=============================" ); } })); } @Override public int getOrder () { return 0 ; } }
Gateway 实战场景 Gateway 自定义路由断言工厂 自定义路由断言的实现方式 如果 Gateway 提供的路由断言工厂不能满足业务需求,那么就可以自定义自己的路由断言工厂。通常有以下两种实现方式:
(1) 实现 RoutePredicateFactory 接口 (2) 继承 AbstractRoutePredicateFactory 抽象类 特别注意
自定义的路由断言工厂的类名必须以 RoutePredicateFactory
为后缀结尾。
自定义路由断言的实现案例 这里将自定义会员等级(userType
),然后按照请求参数里的会员等级(如钻、金、银)和 YML 配置文件里的会员等级进行比较,当两者的会员等级一致才可以访问目标接口。
创建自定义的路由断言工厂类,建议参考 Gateway 默认提供的 AfterRoutePredicateFactory 路由断言工厂类的写法 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 import jakarta.validation.constraints.NotEmpty;import org.springframework.cloud.gateway.handler.predicate.AbstractRoutePredicateFactory;import org.springframework.http.server.reactive.ServerHttpRequest;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 UserTypeRoutePredicateFactory extends AbstractRoutePredicateFactory <UserTypeRoutePredicateFactory .Config > { public static final String USERTYPE_KEY = "userType" ; public UserTypeRoutePredicateFactory () { super (Config.class); } @Override public List<String> shortcutFieldOrder () { return Collections.singletonList(USERTYPE_KEY); } @Override public Predicate<ServerWebExchange> apply (Config config) { return new Predicate<ServerWebExchange>() { @Override public boolean test (ServerWebExchange serverWebExchange) { ServerHttpRequest request = serverWebExchange.getRequest(); String userType = request.getQueryParams().getFirst("userType" ); if (userType == null || userType.trim().length() == 0 ) { return false ; } return userType.equals(config.getUserType()); } }; } @Validated public static class Config { @NotEmpty private String userType; public String getUserType () { return userType; } public void setUserType (String userType) { this .userType = userType; } } }
1 2 3 4 5 6 7 8 spring: cloud: gateway: routes: - id: baidu_route uri: https://www.baidu.com predicates: - UserType=gold
1 2 3 4 5 6 7 8 9 10 spring: cloud: gateway: routes: - id: baidu_route uri: https://www.baidu.com predicates: - name: UserType args: userType: gold
Gateway 实现权重路由 WeightRoutePredicateFactory 是一个路由断言工厂,在 Spring Cloud Gateway 中可以使用它对 URL 进行权重路由,只需在配置时指定分组和权重值即可。
权重路由的使用场景 在开发、测试的时候,或者线上发布、线上服务多版本控制的时候,需要对服务进行权重路由。最常见的使用场景就是一个服务有两个版本:旧版本 V1、新版本 V2。在线上灰度发布的时候,需要通过网关动态实时推送路由权重信息。比如 95% 的流量走服务 V1 版本,5% 的流量走服务 V2 版本。
权重路由的实现案例 下面的案例中,Spring Cloud Gateway 会根据权重路由规则,针对特定的服务,把 95% 的请求流量分发给服务的 V1 版本,把剩余 5% 的流量分发给服务的 V2 版本,由此进行权重路由,点击下载 完整的案例代码。
创建 Gateway Server 工程里的 pom.xml
配置文件:
1 2 3 4 5 6 7 8 <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-gateway</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-actuator</artifactId > </dependency >
创建 Gateway Server 工程里的启动主类:
1 2 3 4 5 6 7 @SpringBootApplication public class GatewayServerApplication { public static void main (String[] args) { SpringApplication.run(GatewayServerApplication.class, args); } }
创建 Gateway Server 工程里的 application.yml
配置文件,添加两个针对 /test
路径转发的路由定义配置,这两个路由属于同一个权重分组,权重的分组名称为 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 29 30 31 32 33 34 server: port: 9090 spring: application: name: gateway-server cloud: gateway: routes: - id: provider-service-v1 uri: http://127.0.0.1:9091/v1/ predicates: - Path=/test - Weight=group, 95 - id: provider-service-v2 uri: http://127.0.0.1:9091/v2/ predicates: - Path=/test - Weight=group, 5 logging: level: org.springframework.cloud.gateway: TRACE org.springframework.http.server.reactive: DEBUG org.springframework.web.reactive: DEBUG reactor.ipc.netty: DEBUG management: endpoints: web: exposure: include: '*' security: enabled: false
创建 Provider Service 工程里的测试控制器:
1 2 3 4 5 6 7 8 9 10 11 12 13 @RestController public class ProviderController { @GetMapping("/v1") public String v1 () { return "version: v1" ; } @GetMapping("/v2") public String v2 () { return "version: v2" ; } }
创建 Provider Service 工程里的启动主类:
1 2 3 4 5 6 7 @SpringBootApplication public class ProviderApplication { public static void main (String[] args) { SpringApplication.run(ProviderApplication.class, args); } }
创建 Provider Service 工程里的 application.yml
配置文件:
1 2 3 4 5 6 server: port: 9091 spring: application: name: provider-service
测试结果:
依次启动 gateway-server、provider-service 应用 多次访问 http://127.0.0.1:9090/test
,会发现按权重配置返回对应的请求内容 Gateway 使用 HTTPS 大型互联网应用的生产环境基本是全站 HTTPS,常规的做法是通过 Nginx 来配置 SSL 证书。如果使用 Spring Cloud Gateway 作为 API 网关,统一管理所有 API 请求的入口和出口,此时 Spring Cloud Gateway 就需要支持 HTTPS。由于 Spring Cloud Gateway 是基于 Spring Boot 2.0 构建的,所以只需要将生成的 HTTPS 证书放到 Spring Cloud Gateway 应用的类路径下面即可。
HTTPS 案例 下面将介绍如何在 Spring Cloud Gateway 中使用 HTTPS,其中各个模块的说明如下。由于本案例是基于上面的 Gateway 服务发现的路由规则案例 改造而来的,因此 micro-service-eureka、micro-service-provider-1、micro-service-provider-2 工程里的配置和代码不再累述,点击下载 完整的案例代码。
模块 端口 说明 micro-service-gateway-https N/A 聚合父 Maven 工程 micro-service-eureka 9000 Eureka 注册中心 micro-service-gateway 9001 带有 HTTPS 证书的网关服务,使用 HTTPS 协议访问 micro-service-provider-1 9002 服务提供者,使用 HTTP 协议 micro-service-provider-2 9003 服务提供者,使用 HTTP 协议
创建 Micro Service 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 <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-gateway</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-actuator</artifactId > </dependency > <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-netflix-eureka-client</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > <exclusions > <exclusion > <groupId > org.springframework</groupId > <artifactId > spring-webmvc</artifactId > </exclusion > <exclusion > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-tomcat</artifactId > </exclusion > </exclusions > </dependency >
创建 Micro Service Gateway 工程里的启动主类:
1 2 3 4 5 6 7 @SpringBootApplication public class GatewayServerApplication { public static void main (String[] args) { SpringApplication.run(GatewayServerApplication.class, args); } }
创建 Micro Service Gateway 工程里的 application.yml
配置文件,通过 key-store
指定 HTTPS 证书的路径:
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 server: port: 9001 ssl: enabled: true key-alias: spring key-password: spring key-store: classpath:self-signed.jks key-store-type: JKS key-store-provider: SUN key-store-password: spring spring: application: name: gateway-server cloud: gateway: discovery: locator: enabled: true lower-case-service-id: true eureka: client: service-url: defaultZone: http://127.0.0.1:9000/eureka instance: instance-id: gateway-server-${server.port} prefer-ip-address: true
测试结果:
依次启动 micro-service-eureka、micro-service-provider-1、micro-service-provider-2、micro-service-gateway 应用 通过 HTTPS 协议访问 https://127.0.0.1:9001/provider-service/provider/sayHello/Jim
,会出现如下的错误: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 io.netty.handler.ssl.NotSslRecordException: not an SSL/TLS record: 485454502f312e3120343030200d0a5472616e736665722d456e636f646 ... at io.netty.handler.ssl.SslHandler.decodeJdkCompatible(SslHandler.java:1156) [netty-handler-4.1.25.Final.jar:4.1.25.Final] at io.netty.handler.ssl.SslHandler.decode(SslHandler.java:1221) [netty-handler-4.1.25.Final.jar:4.1.25.Final] at io.netty.handler.codec.ByteToMessageDecoder.decodeRemovalReentryProtection(ByteToMessageDecoder.java:489) ~[netty-codec-4.1.25.Final.jar:4.1.25.Final] at io.netty.handler.codec.ByteToMessageDecoder.callDecode(ByteToMessageDecoder.java:428) ~[netty-codec-4.1.25.Final.jar:4.1.25.Final] at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:265) ~[netty-codec-4.1.25.Final.jar:4.1.25.Final] at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362) ~[netty-transport-4.1.25.Final.jar:4.1.25.Final] at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348) ~[netty-transport-4.1.25.Final.jar:4.1.25.Final] at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340) ~[netty-transport-4.1.25.Final.jar:4.1.25.Final] at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1434) ~[netty-transport-4.1.25.Final.jar:4.1.25.Final] at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362) ~[netty-transport-4.1.25.Final.jar:4.1.25.Final] at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348) ~[netty-transport-4.1.25.Final.jar:4.1.25.Final] at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:965) ~[netty-transport-4.1.25.Final.jar:4.1.25.Final] at io.netty.channel.epoll.AbstractEpollStreamChannel$EpollStreamUnsafe.epollInReady(AbstractEpollStreamChannel.java:808) ~[netty-transport-native-epoll-4.1.25.Final-linux-x86_64.jar:4.1.25.Final] at io.netty.channel.epoll.EpollEventLoop.processReady(EpollEventLoop.java:408) ~[netty-transport-native-epoll-4.1.25.Final-linux-x86_64.jar:4.1.25.Final] at io.netty.channel.epoll.EpollEventLoop.run(EpollEventLoop.java:308) ~[netty-transport-native-epoll-4.1.25.Final-linux-x86_64.jar:4.1.25.Final] at io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:884) ~[netty-common-4.1.25.Final.jar:4.1.25.Final] at java.lang.Thread.run(Thread.java:745) ~[na:1.8.0_102]
HTTPS 转 HTTP 的问题 上述错误出现的原因是通过 Spring Cloud Gateway 请求进来的协议是 HTTPS,而后端被代理的服务是 HTTP 协议的请求,所以当 Gateway 用 HTTPS 请求转发调用 HTTP 协议的服务时,就会出现 not an SSL/TLS record
的错误。本质上这是一个 Spring Cloud Gateway 将 HTTPS 请求转发调用 HTTP 服务的问题。由于服务的拆分,在微服务的应用集群中会存在很多服务提供者和服务消费者,而这些服务提供者和服务消费者基本都是部署在企业内网中,没必要全部加 HTTPS 进行调用。因此 Spring Cloud Gateway 对外的请求是 HTTPS,对后端代理服务的请求可以是 HTTP。通过 Debug 调试源码分析,LoadBalancerClientFilter.filter()
方法如下:
1 2 3 4 5 6 7 8 9 URI uri = exchange.getRequest().getURI(); String overrideScheme = null ; if (schemePrefix != null ) { overrideScheme = url.getScheme(); } URI requestUrl = this .loadBalancer.reconstructURI(new LoadBalancerClientFilter.DelegatingServiceInstance(instance, overrideScheme), uri); log.trace("LoadBalancerClientFilter url chosen: " + requestUrl); exchange.getAttributes().put(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR, requestUrl);
从上面的代码可以看出,LoadBalancer 对 HTTP 请求进行封装,如果从 Spring Cloud Gateway 进来的请求是 HTTPS,它就用 HTTPS 封装,如果是 HTTP 就用 HTTP 封装,而且没有预留 任何扩展修改的接口,只能通过自定义 Global Filter 的方式对其修改。下面介绍两种修改方法,在实践中任选其中一种即可。
官方 Issues 说明
https://github.com/spring-cloud/spring-cloud-gateway/issues/378 https://github.com/spring-cloud/spring-cloud-gateway/issues/160 第一种解决方案 在 LoadBalancerClientFilter 执行之前将 HTTPS 修改为 HTTP 协议:
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 @Component public class HttpsToHttpFilter implements GlobalFilter , Ordered { private static final int HTTPS_TO_HTTP_FILTER_ORDER = 10099 ; @Override public Mono<Void> filter (ServerWebExchange exchange, GatewayFilterChain chain) { URI originalUri = exchange.getRequest().getURI(); ServerHttpRequest request = exchange.getRequest(); ServerHttpRequest.Builder mutate = request.mutate(); String forwardedUri = request.getURI().toString(); if (forwardedUri != null && forwardedUri.startsWith("https" )) { try { URI mutatedUri = new URI("http" , originalUri.getUserInfo(), originalUri.getHost(), originalUri.getPort(), originalUri.getPath(), originalUri.getQuery(), originalUri.getFragment()); mutate.uri(mutatedUri); } catch (Exception e) { throw new IllegalStateException(e.getMessage(), e); } } ServerHttpRequest build = mutate.build(); return chain.filter(exchange.mutate().request(build).build()); } @Override public int getOrder () { return HTTPS_TO_HTTP_FILTER_ORDER; } }
第二种解决方案 在 LoadBalancerClientFilter 执行之后将 HTTPS 修改为 HTTP,拷贝 RibbonUtils 中的 upgradeconnection
方法来自定义全局过滤器:
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 @Component public class HttpSchemeFilter implements GlobalFilter , Ordered { private static final int HTTPS_TO_HTTP_FILTER_ORDER = 10101 ; @Override public Mono<Void> filter (ServerWebExchange exchange, GatewayFilterChain chain) { Object uriObj = exchange.getAttributes().get(GATEWAY_REQUEST_URL_ATTR); if (uriObj != null ) { URI uri = (URI) uriObj; uri = this .upgradeConnection(uri, "http" ); exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, uri); } return chain.filter(exchange); } private URI upgradeConnection (URI uri, String scheme) { UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUri(uri).scheme(scheme); if (uri.getRawQuery() != null ) { uriComponentsBuilder.replaceQuery(uri.getRawQuery().replace("+" , "%20" )); } return uriComponentsBuilder.build(true ).toUri(); } @Override public int getOrder () { return HTTPS_TO_HTTP_FILTER_ORDER; } }
Gateway 集成 Swagger Swagger 是一个可视化 API 测试工具,可以和应用完美融合。通过声明接口注解的方式,可以方便快捷地获取 API 调试界面进行测试。Zuul 可以很方便地与 Swagger 整合在一起,由于 Spring Cloud Finchley 版是基于 Spring Boot 2.0 的,而 Spring Cloud Gateway 的底层是基于 WebFlux 实现的,且经验证,WebFlux 和 Swagger 不兼容。如果按照 Zuul 集成 Swagger 的方式,应用启动的时候会报错。下面将介绍 Spring Cloud Gateway 如何集成 Swagger,其中各个模块的说明如下。由于本案例是基于上面的 “Gateway 服务发现的路由规则案例 “ 改造而来的,因此 micro-service-eureka 工程里的配置和代码不再累述,点击下载 完整的案例代码。
模块 端口 说明 micro-service-gateway-swagger N/A 聚合父 Maven 工程 micro-service-eureka 9000 Eureka 注册中心 micro-service-gateway 9001 基于 Spring Cloud Gateway 的网关服务 micro-service-provider-1 9002 服务提供者 micro-service-provider-2 9003 服务提供者
1. 创建 Micro Service Gateway 工程 创建 Micro Service 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 31 32 33 34 35 36 <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-gateway</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-actuator</artifactId > </dependency > <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-netflix-eureka-client</artifactId > </dependency > <dependency > <groupId > io.springfox</groupId > <artifactId > springfox-swagger-ui</artifactId > <version > 2.9.2</version > </dependency > <dependency > <groupId > io.springfox</groupId > <artifactId > springfox-swagger2</artifactId > <version > 2.9.2</version > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > <exclusions > <exclusion > <groupId > org.springframework</groupId > <artifactId > spring-webmvc</artifactId > </exclusion > <exclusion > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-tomcat</artifactId > </exclusion > </exclusions > </dependency >
创建 Micro Service Gateway 工程里的 SwaggerProvider 类,因为 Swagger 暂不支持 WebFlux 项目,所以不能在 Gateway 中配置 SwaggerCoufig,需要编写 GatewaySwaggerProvider 实现 SwaggerResourcesProvider 接口,用于获取 SwaggerResources:
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 @Primary @Component public class GatewaySwaggerProvider implements SwaggerResourcesProvider { private final RouteLocator routeLocator; private final GatewayProperties gatewayProperties; public static final String API_URI = "/v2/api-docs" ; public GatewaySwaggerProvider (RouteLocator routeLocator, GatewayProperties gatewayProperties) { this .routeLocator = routeLocator; this .gatewayProperties = gatewayProperties; } @Override public List<SwaggerResource> get () { List<SwaggerResource> resources = new ArrayList<>(); List<String> routes = new ArrayList<>(); routeLocator.getRoutes().subscribe(route -> routes.add(route.getId())); gatewayProperties.getRoutes().stream().filter(routeDefinition -> routes.contains(routeDefinition.getId())) .forEach(routeDefinition -> routeDefinition.getPredicates().stream() .filter(predicateDefinition -> ("Path" ).equalsIgnoreCase(predicateDefinition.getName())) .forEach(predicateDefinition -> resources.add(swaggerResource(routeDefinition.getId(), predicateDefinition.getArgs().get(NameUtils.GENERATED_NAME_PREFIX + "0" ) .replace("/**" , API_URI))))); return resources; } private SwaggerResource swaggerResource (String name, String location) { SwaggerResource swaggerResource = new SwaggerResource(); swaggerResource.setName(name); swaggerResource.setLocation(location); swaggerResource.setSwaggerVersion("2.0" ); return swaggerResource; } }
创建 Micro Service Gateway 工程里的 Swagger-Resource 端点,因为没有在 Gateway 中配置 SwaggerConfig,但是运行 Swagger-UI 的时候需要依赖一些接口,所以需要建立相应的 Swagger-Resource 端点:
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 @RestController @RequestMapping("/swagger-resources") public class SwaggerHandler { @Autowired(required = false) private SecurityConfiguration securityConfiguration; @Autowired(required = false) private UiConfiguration uiConfiguration; private final SwaggerResourcesProvider swaggerResources; @Autowired public SwaggerHandler (SwaggerResourcesProvider swaggerResources) { this .swaggerResources = swaggerResources; } @GetMapping("/configuration/security") public Mono<ResponseEntity<SecurityConfiguration>> securityConfiguration() { return Mono.just(new ResponseEntity<>( Optional.ofNullable(securityConfiguration).orElse(SecurityConfigurationBuilder.builder().build()), HttpStatus.OK)); } @GetMapping("/configuration/ui") public Mono<ResponseEntity<UiConfiguration>> uiConfiguration() { return Mono.just(new ResponseEntity<>( Optional.ofNullable(uiConfiguration).orElse(UiConfigurationBuilder.builder().build()), HttpStatus.OK)); } @GetMapping("") public Mono<ResponseEntity> swaggerResources () { return Mono.just((new ResponseEntity<>(swaggerResources.get(), HttpStatus.OK))); } }
创建 Micro Service Gateway 工程里的 GwSwaggerHeaderFilter 类,由于在路由规则为 admin/test/{a}/{b}
时,Swagger 界面上会显示为 test/{a}/{b}
,缺少了 /admin
这个路由节点。通过 Debug 断点调试发现,Swagger 会根据 X-Forwarded-Prefix
这个 Header 来获取 BasePath,因此需要将它添加到接口路径与 Host 之间才能正常工作。但是 Gateway 在做转发的时候并没有将这个 Header 添加到 Request 上,从而导致接口调试出现 404 错误。为了解决该问题,需要在 Gateway 中编写一个过滤器来添加这个 Header。特别注意,Spring Boot 版本为 2.0.6 以上的可以跳过这一步骤,最新源码里 Spring Boot 修复了该 Bug,已经默认添加上了这个 Header。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Component public class GwSwaggerHeaderFilter extends AbstractGatewayFilterFactory { private static final String HEADER_NAME = "X-Forwarded-Prefix" ; @Override public GatewayFilter apply (Object config) { return (exchange, chain) -> { ServerHttpRequest request = exchange.getRequest(); String path = request.getURI().getPath(); if (!StringUtils.endsWithIgnoreCase(path, GatewaySwaggerProvider.API_URI)) { return chain.filter(exchange); } String basePath = path.substring(0 , path.lastIndexOf(GatewaySwaggerProvider.API_URI)); ServerHttpRequest newRequest = request.mutate().header(HEADER_NAME, basePath).build(); ServerWebExchange newExchange = exchange.mutate().request(newRequest).build(); return chain.filter(newExchange); }; } }
创建 Micro Service Gateway 工程里的 application.yml
配置文件,添加上面编写的 GwSwaggerHeaderFilter 过滤器, URI 指定为 lb://provider-service-1
,表示负载均衡到 provider-service-1 服务。由于 Swagger 发出请求 的 URL 都是以 /xxxx
开头,因此需要使用 StripPrefix 过滤器将第一个路由节点(/xxxx
)去掉。
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 server: port: 9001 spring: application: name: gateway-server cloud: gateway: discovery: locator: enabled: true lower-case-service-id: true routes: - id: provider-service-1 uri: lb://provider-service-1 predicates: - Path=/provider1/** filters: - GwSwaggerHeaderFilter - StripPrefix=1 - id: provider-service-2 uri: lb://provider-service-2 predicates: - Path=/provider2/** filters: - GwSwaggerHeaderFilter - StripPrefix=1 eureka: client: service-url: defaultZone: http://127.0.0.1:9000/eureka instance: instance-id: gateway-server-${server.port} prefer-ip-address: true management: endpoints: web: exposure: include: '*' security: enabled: false
2. 创建 Micro Service Provider 1 工程 创建 Micro Service Provider 1 工程里的 pom.xml
配置文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-netflix-eureka-client</artifactId > </dependency > <dependency > <groupId > io.springfox</groupId > <artifactId > springfox-swagger-ui</artifactId > <version > 2.9.2</version > </dependency > <dependency > <groupId > io.springfox</groupId > <artifactId > springfox-swagger2</artifactId > <version > 2.9.2</version > </dependency >
创建 Micro Service Provider 1 工程里的 SwaggerConfig 类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @Configuration @EnableSwagger2 public class SwaggerConfig { @Bean public Docket createRestApi () { return new Docket(DocumentationType.SWAGGER_2) .apiInfo(apiInfo()) .select() .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class)) .paths(PathSelectors.any()) .build(); } private ApiInfo apiInfo () { return new ApiInfoBuilder() .title("Swagger API" ) .description("验证 Gateway 集成 Swagger 的效果" ) .termsOfServiceUrl("" ) .version("2.0" ) .build(); } }
创建 Micro Service Provider 1 工程里的测试控制类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @RestController @Api("provider-service-1 接口测试") @RequestMapping("/provider1") public class ProviderOneController { @ApiOperation(value = "计算+", notes = "加法") @ApiImplicitParams({ @ApiImplicitParam(name = "a", value = "数字a", required = true, dataType = "Long"), @ApiImplicitParam(name = "b", value = "数字b", required = true, dataType = "Long") }) @GetMapping("/{a}/{b}") public String get (@PathVariable Integer a, @PathVariable Integer b) { return "from provider service 1, the result is: " + (a + b); } }
创建 Micro Service Provider 1 工程里的 application.xml
配置文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 server: port: 9002 spring: application: name: provider-service-1 eureka: client: service-url: defaultZone: http://127.0.0.1:9000/eureka instance: instance-id: provider-service-1-${server.port} prefer-ip-address: true
3. 创建 Micro Service Provider 2 工程 由于 Micro Service Provider 2 工程 与 Micro Service Provider 1 工程里的配置和代码都差不多,这里不再累述。
4. 测试案例代码 (1) 依次启动 micro-service-eureka、micro-service-provider-1、micro-service-provider-2、micro-service-gateway 应用 (2) 访问 http://127.0.0.1:9000/
,查看各个服务是否都成功注册到 Eureka (3) 访问 http://127.0.0.1:9001/swagger-ui.html
,查看 Swagger 的界面是否正常工作,查看截图 (4) 在 Swagger 的界面上打开对应的 URL,输入测试数据,验证 Swagger 经过 Gateway 是否可以正常访问 Provider1 和 Provider2 服务的接口,查看截图