Spring Cloud Gateway
Spring Cloud Gateway 作为 Spring Cloud 生态系统中的网关,目标是替代 Netflix Zuul,其不仅提供统一的路由方式,并且基于 Filter 链的方式提供了网关基本的功能,例如:安全,监控/指标,和限流。
Spring Cloud Gateway是基于Spring Boot 2.x, Spring WebFlux和Project Reactor 构建的。因此,在使用Spring Cloud Gateway时,许多不熟悉的同步库(例如,Spring Data和Spring Security)和模式可能不适用。如果您对这些项目不熟悉,建议您在使用Spring Cloud Gateway之前先阅读它们的文档以熟悉一些新概念。
Spring Cloud Gateway需要Spring Boot和Spring Webflux提供的Netty运行时。它不能在传统的Servlet容器中或作为WAR构建。
由于 SCG 是 netty+webflux 实现,webflux 与 web 是冲突的,所以不能引用依赖 spring-boot-starter-web ,否则提示冲突。
相关概念
- Route: Route the basic building block of the gateway. It is defined by an ID, a destination URI, a collection of predicates and a collection of filters. A route is matched if aggregate predicate is true.
- Predicate: This is a Java 8 Function Predicate. The input type is a Spring Framework
ServerWebExchange
. This allows developers to match on anything from the HTTP request, such as headers or parameters.- Filter: These are instances Spring Framework
GatewayFilter
constructed in with a specific factory. Here, requests and responses can be modified before or after sending the downstream request.
本例子 GitHub
快速上手
1.引入依赖,版本:
- Spring Cloud: Greenwich.RC2
- Spring Boot: 2.1.1.RELEASE
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>spring-cloud-gateway-sample</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>spring-cloud-gateway-sample</name>
<description>Demo project for Spring Cloud Gateway</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<spring-cloud.version>Greenwich.RC2</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.10</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.62</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.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>
<repositories>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</pluginRepository>
<pluginRepository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
</project>
2.配置
定义路由断言,意味着匹配这个路径/**
的请求都会转发到127.0.0.1:9090
这个地址。
server:
port: 8000
spring:
application:
name: gateway
cloud:
gateway:
routes:
- id: path_route2
uri: http://127.0.0.1:9090
order: -1
predicates:
- Path=/**
discovery:
locator:
enabled: true
lower-case-service-id: true
logging:
level:
org.springframework.cloud.gateway: TRACE
# org.springframework.http.server.reactive: DEBUG
# org.springframework.web.reactive: DEBUG
# reactor.ipc.netty: DEBUG
# reactor.netty: DEBUG
使用配置 yml 进行配置路由时,多个可以匹配的情况下。可以使用 order 进行优先级排序,这个参数同样适用于使用代码进行配置路由的情况。
其他断言配置方式参考官网文档
整合 Eureka 的默认路由
Zuul 默认会为所有服务都进行转发操作,我们只需要在访问路径上指定要访问的服务即可,通过这种方式就不用为每个服务都去配置转发规则,当新加了服务的时候,不用去配置路由规则和重启网关。
在 Spring Cloud Gateway 中当然也有这样的功能,通过配置即可开启,配置如下:
spring:
cloud:
gateway:
discovery:
locator:
enabled: true
lower-case-service-id: true
routes:
- id: myRoute
uri: lb://service
predicates:
- Path=/service/**
依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
Global Filter and GatewayFilter
GatewayFilter与GlobalFilter的区别
在一个高的角度来看,Global filters会被应用到所有的路由上,而Gateway filter将应用到
单个路由
上或者一个分组的路由
上。在下面的案例中将会进行说明。
Global Filter
当请求进入(并与路由匹配)时,过滤Web处理程序会将的所有实例GlobalFilter
和所有特定GatewayFilter
于路由的实例添加到过滤器链中。该组合的过滤器链按org.springframework.core.Ordered
接口排序,可以通过实现该getOrder()
方法进行设置。
由于Spring Cloud Gateway区分了执行过滤器逻辑的“前”阶段和“后”阶段(请参阅:工作原理),因此优先级最高的过滤器将在“前”阶段中处于第一个阶段,而在“后”阶段中处于最后一个阶段
AuthFilter.java
@Slf4j
public class AuthFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("前执行");
// String token = exchange.getRequest().getQueryParams().getFirst("authToken");
String token = exchange.getRequest().getHeaders().getFirst("authToken");
if (token == null || token.isEmpty()) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
// wrap内返回封装对象即可
return response.writeWith(Mono.just(response.bufferFactory().wrap("无权限".getBytes())));
}
// 这样获取请求参数或则 RequestParamsFilter 这个
Map<String, Object> data = exchange.getAttribute("cachedRequestBodyObject");
return chain.filter(exchange).then(Mono.fromRunnable(() -> log.info("后执行")));
}
@Override
public int getOrder() {
return -100;
}
}
- 多个filter按照order顺序执行,越小(负值)执行越优先。post返回时则相反。执行最后一个前执行后再逆序执行后执行。
- globalFilter 和 gatewayFilter 两条链不是同一个线程。线程参数 ThreadLocal 变量不能共享。包括 pre,post 都是两个线程。
配置注入
/**
* @Description: 配置类的方式加载filter
* @author: LinQin
* @date: 2020/01/11
*/
@Configuration
@Slf4j
public class AutoFilter {
@Bean
public GlobalFilter authFilter(){
return new AuthFilter();
}
}
GatewayFilter
新建类和全局过滤器一致,接口换为GatewayFilter
即可。其他一致。
RouteLocator绑定使用
@Bean
public RouteLocator customerRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route(r -> r.path("/test/prefix/**")
.filters(f -> f.stripPrefix(2)
.filter(new CustomFilter())
.addResponseHeader("X-Response-test", "test"))
.uri("lb://SC-CONSUMER")
.order(0)
.id("test_consumer_service")
)
.build();
}
过滤器中获取请求参数
网上坑甚多,自己尝试了两个可用的。都是官方 issue
提供的。
1.重写ModifyRequestBodyGatewayFilterFactory
RequestParamsFilter.java
/**
* 获取请求体 ModifyRequestBodyGatewayFilterFactory 预言类 ReadBodyPredicateFactory
* @author
*/
@Slf4j
public class RequestParamsFilter implements GlobalFilter, Ordered {
private final List<HttpMessageReader<?>> messageReaders = HandlerStrategies.withDefaults().messageReaders();
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// ServerRequest serverRequest = new DefaultServerRequest(exchange);
ServerRequest serverRequest = new DefaultServerRequest(exchange, messageReaders);
// mediaType
MediaType mediaType = exchange.getRequest().getHeaders().getContentType();
// read & modify body
Mono<String> modifiedBody = serverRequest.bodyToMono(String.class)
.flatMap(body -> {
if (MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(mediaType)) {
// origin body map
Map<String, Object> bodyMap = decodeBody(body);
// decrypt & auth
// new body map
Map<String, Object> newBodyMap = new HashMap<>();
return Mono.just(encodeBody(newBodyMap));
}
if (MediaType.APPLICATION_JSON
.isCompatibleWith(mediaType) || MediaType.APPLICATION_JSON_UTF8
.isCompatibleWith(mediaType)) {
// 获取请求体
log.info(body);
}
return Mono.empty();
});
BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody, String.class);
HttpHeaders headers = new HttpHeaders();
headers.putAll(exchange.getRequest().getHeaders());
// the new content type will be computed by bodyInserter
// and then set in the request decorator
headers.remove(HttpHeaders.CONTENT_LENGTH);
CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange, headers);
return bodyInserter.insert(outputMessage, new BodyInserterContext())
.then(Mono.defer(() -> {
ServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator(
exchange.getRequest()) {
public HttpHeaders getHeaders() {
long contentLength = headers.getContentLength();
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.putAll(super.getHeaders());
if (contentLength > 0) {
httpHeaders.setContentLength(contentLength);
} else {
httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked");
}
return httpHeaders;
}
public Flux<DataBuffer> getBody() {
return outputMessage.getBody();
}
};
return chain.filter(exchange.mutate().request(decorator).build());
}));
}
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
private Map<String, Object> decodeBody(String body) {
return Arrays.stream(body.split("&"))
.map(s -> s.split("="))
.collect(Collectors.toMap(arr -> arr[0], arr -> arr[1]));
}
private String encodeBody(Map<String, Object> map) {
String collect = map.entrySet().stream().map(e -> e.getKey() + "=" + e.getValue())
.collect(Collectors.joining("&"));
log.info(collect);
return collect;
}
}
这个是使用 yml 配置路由最好的获取请求参数的方法。适用版本Spring Cloud: Greenwich.RC2 、Spring Boot: 2.1.1.RELEASE
其他版本可能存在有时不灵的情况。
2.使用预言类 ReadBodyPredicateFactory
直接使用代码路由配置方式使用
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
//@formatter:off
return builder.routes()
/*
route1 是get请求,get请求使用readBody会报错
route2 是post请求,Content-Type是application/x-www-form-urlencoded,readbody为String.class
route3 是post请求,Content-Type是application/json,readbody为Object.class
*/
.route("route1",
r -> r
.method(HttpMethod.GET)
.and()
.path(SERVICE)
.filters(f -> {
// f.filter(requestFilter);
return f;
})
.uri(URI))
.route("route2",
r -> r
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
.and()
.method(HttpMethod.POST)
.and()
.readBody(String.class, readBody -> {
log.info("request method POST, Content-Type is application/x-www-form-urlencoded, body is:{}", readBody);
return true;
})
.and()
.path(SERVICE)
.filters(f -> {
// f.filter(requestFilter);
return f;
})
.uri(URI))
.route("route3",
r -> r
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.and()
.method(HttpMethod.POST)
.and()
.readBody(Object.class, readBody -> {
log.info("request method POST, Content-Type is application/json, body is:{}", readBody);
return true;
})
.and()
.path(SERVICE)
.filters(f -> {
// f.filter(requestFilter);
return f;
})
.uri(URI))
.build();
//@formatter:on
}
经过测试,该方案也可以获取参数,请求参数可以直接在过滤器中获取 。
其他 yml 路由的也可以使用这个方式获取参数。
Map<String, Object> data = exchange.getAttribute("cachedRequestBodyObject");
参考:
https://windmt.com/2019/01/16/spring-cloud-19-spring-cloud-gateway-read-and-modify-request-body/
https://blog.51cto.com/thinklili/2329184
修改接口返回报文
因为网关路由的接口返回报文格式各异,并且网关也有有一些限流、认证、熔断降级的返回报文,为了统一这些报文的返回格式,网关必须要对接口的返回报文进行修改,过滤器代码如下:
public class WrapperResponseFilter implements GlobalFilter, Ordered {
@Override
public int getOrder() {
// -1 is response write filter, must be called before that
return -2;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpResponse originalResponse = exchange.getResponse();
DataBufferFactory bufferFactory = originalResponse.bufferFactory();
ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(originalResponse) {
@Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
if (body instanceof Flux) {
Flux<? extends DataBuffer> fluxBody = (Flux<? extends DataBuffer>) body;
return super.writeWith(fluxBody.map(dataBuffer -> {
// probably should reuse buffers
byte[] content = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(content);
// 释放掉内存
DataBufferUtils.release(dataBuffer);
String rs = new String(content, Charset.forName("UTF-8"));
Response response = new Response();
response.setCode("1");
response.setMessage("请求成功");
response.setData(rs);
byte[] newRs = JSON.toJSONString(response).getBytes(Charset.forName("UTF-8"));
originalResponse.getHeaders().setContentLength(newRs.length);//如果不重新设置长度则收不到消息。
return bufferFactory.wrap(newRs);
}));
}
// if body is not a flux. never got there.
return super.writeWith(body);
}
};
// replace response with decorator
return chain.filter(exchange.mutate().response(decoratedResponse).build());
}
}