微服务网关 Spring Cloud GateWay
微服务 Spring Cloud GateWay
概述
- SpringCloud Gateway使用的是Webflux中的reactor-netty****响应式编程组件,底层使用了Netty通讯框架(非堵塞式IO)
- Zuul1.x 是基于Servlet2.x 实现的使用的是堵塞式IO,很慢。
- 作用:反向代理,鉴权,流量控制,熔断,服务监控。
- gateway 的核心功能就是 路由规则(route),断言(predicate),过滤器(filter)
- 架构图
gateway 的配置
- 依赖
<!--gateway无需web和actuator,会出错--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency>
- 配置文件
# 主要就是配置 网关的路由(就是对请求的转发) server: port: 9527 eureka: instance: hostname: cloud-gateway-service client: fetch-registry: true register-with-eureka: true service-url: defaultZone: http://eureka7001.com:7001/eureka/ # 这种方式的缺点:不能实现负载均衡,因为转发的ip 写死了 spring: application: name: cloud-gateway cloud: gateway: routes: - id: payment_route # 路由的id,没有规定规则但要求唯一,建议配合服务名 #匹配后提供服务的路由地址 uri: http://localhost:8001 predicates: - Path=/payment/get/** # 断言,路径相匹配的进行路由 - id: payment_route2 uri: http://localhost:8001 predicates: Path=/payment/lb/** #断言,路径相匹配的进行路由
- 主启动类
// 什么都没有,注册进 注册中心即可 @SpringBootApplication @EnableEurekaClient public class GatewayMain9527 { public static void main(String[] args) { SpringApplication.run(GatewayMain9527.class, args); } }
- 使用方式
- 原先我们通过微服务的ip 访问微服务,启动了gateway 之后,我们访问gateway,又gateway 转发到我们的微服务。
- 编码的方式配置路由规则
/** * <p> * 编码的方式定制路由规则.挺麻烦的还不如直接在配置文件里面写 */ @Configuration public class GateWayConfig { /** * 配置了一个id为route-name的路由规则 * 当访问localhost:9527/guonei的时候,将会转发至https://news.baidu.com/guonei * * @param routeLocatorBuilder * @return */ @Bean public RouteLocator customRouteLocator(RouteLocatorBuilder routeLocatorBuilder) { RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes(); routes.route("path_route_atguigu", r -> r.path("/guonei") .uri("https://news.baidu.com/guonei")).build(); return routes.build(); } @Bean public RouteLocator customRouteLocator2(RouteLocatorBuilder routeLocatorBuilder) { RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes(); return routes.route("path_route_atguigu2", r -> r.path("/guoji").uri("https://news.baidu.com/guoji")).build(); } }
- 其他的predicate (断言)
predicates: - Path=/payment/get/** # 断言,路径相匹配的进行路由 - After=2020-05-30T15:13:10.896+08:00[Asia/Shanghai] # 必须在这个时间之后才能范文(游戏开服) - Before=2017-01-20T17:42:47.789-07:00[America/Denver] # 必须在这个时间之前才能访问 (游戏停服) - Cookie=username,zzyy # cookie 校验 - Header=X-Request-Id, \d+ #请求头要有X-Request-Id属性,并且值为正数 - Host=**.atguigu.com - Method=GET - Query=username, \d+ # 要有参数名username并且值还要是正整数才能路由
- 过滤器
# 还有很多过滤器,用到的时候自己找吧 routes: - id: payment_route # 路由的id,没有规定规则但要求唯一,建议配合服务名 predicates: - Path=/payment/get/** # 断言,路径相匹配的进行路由 # 过滤,断言都成功之后会经过过滤器,我们可以在这里加东西 filters: - AddRequestHeader=X-Request-red, blue
- 路由的时候使用负载均衡
# 核心区别就是 cloud.gateway.discovery.locator.enabled: true # 还有 uri 写成 lb://微服务名 cloud: gateway: discovery: locator: enabled: true # 开启从注册中心动态创建路由的功能,利用微服务名称j进行路由 routes: - id: payment_route # 路由的id,没有规定规则但要求唯一,建议配合服务名 # 匹配后提供服务的路由地址 # uri: http://localhost:8001 uri: lb://CLOUD-PAYMENT-SERVICE # 匹配提供服务的路由地址 predicates: - Path=/payment/get/** # 断言,路径相匹配的进行路由 # 过滤,断言都成功之后会经过过滤器,我们可以在这里加东西 filters: - AddRequestHeader=X-Request-red, blue - id: payment_route2 # uri: http://localhost:8001 uri: lb://CLOUD-PAYMENT-SERVICE # 匹配提供服务的路由地址 predicates: - Path=/payment/lb/** #断言,路径相匹配的进行路由
- 定义全局的过滤器
/** * User: haitao * Date: 2020/5/30 * <p> * 自定义 gateway 的全局过滤器,实现 GlobalFilter, Ordered 两个接口,然后注入IOC 容器中, * 就能发挥全局过滤器的功能 */ @Component // 添加到IOC 容器中 @Slf4j public class MyLogGateWayFilter implements GlobalFilter, Ordered { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { log.info("***********come MyLogGateWayFilter in : " + new Date()); String uname = exchange.getRequest().getQueryParams().getFirst("uname"); if (uname == null) { log.info("*******用户名为null,非法用户"); exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE); return exchange.getResponse().setComplete(); } return chain.filter(exchange); } @Override public int getOrder() { return 0; } }
- curl 的高级使用
# 不带任何参数 curl http://localhost:9527/payment/lb # 携带cookie curl http://localhost:9527/payment/lb --cookie "username=spectrumrpc" # 携带请求头 curl http://localhost:9527/payment/lb -H "X-Request-Id:123456" # host curl http://localhost:9527/payment/lb -H "Host: www.baidu.com" # 携带参数 curl http://localhost:9527/payment/lb\?uname\=1
路由过滤
路由规则的定义语法如下:
spring:
cloud:
gateway:
routes:
- id: item
uri: lb://item-service
predicates:
- Path=/items/**,/search/**
其中routes对应的类型如下:
四个属性含义如下:
id
:路由的唯一标示predicates
:路由断言,其实就是匹配条件filters
:路由过滤条件,后面讲uri
:路由目标地址,lb://
代表负载均衡,从注册中心获取目标微服务的实例列表,并且负载均衡选择一个访问。
这里我们重点关注 predicates
,也就是路由断言。SpringCloudGateway中支持的断言类型有很多:
名称 | 说明 | 示例 |
---|---|---|
After | 是某个时间点后的请求 | - After=2037-01-20T17:42:47.789-07:00[America/Denver] |
Before | 是某个时间点之前的请求 | - Before=2031-04-13T15:14:47.433+08:00[Asia/Shanghai] |
Between | 是某两个时间点之前的请求 | - Between=2037-01-20T17:42:47.789-07:00[America/Denver], 2037-01-21T17:42:47.789-07:00[America/Denver] |
Cookie | 请求必须包含某些cookie | - Cookie=chocolate, ch.p |
Header | 请求必须包含某些header | - Header=X-Request-Id, \d+ |
Host | 请求必须是访问某个host(域名) | - Host=****.somehost.org,.anotherhost.org |
Method | 请求方式必须是指定方式 | - Method=GET,POST |
Path | 请求路径必须符合指定规则 | - Path=/red/{segment},/blue/** |
Query | 请求参数必须包含指定参数 | **- Query=name, Jack或者- Query=name** |
RemoteAddr | 请求者的ip必须是指定范围 | - RemoteAddr=192.168.1.1/24 |
weight | 权重处理 |
网关过滤器
登录校验必须在请求转发到微服务之前做,否则就失去了意义。而网关的请求转发是Gateway内部代码实现的,要想在请求转发之前做登录校验,就必须了解Gateway内部工作的基本原理。
如图所示:
- 客户端请求进入网关后由HandlerMapping对请求做判断,找到与当前请求匹配的路由规则(Route),然后将请求交给WebHandler去处理。
- WebHandler则会加载当前路由下需要执行的过滤器链(Filter chain),然后按照顺序逐一执行过滤器(后面称为Filter)。
- 图中Filter被虚线分为左右两部分,是因为Filter内部的逻辑分为pre和post两部分,分别会在请求路由到微服务之前和之后被执行。
- 只有所有Filter的pre逻辑都依次顺序执行通过后,请求才会被路由到微服务。
- 微服务返回结果后,再倒序执行Filter的post逻辑。
- 最终把响应结果返回。
如图中所示,最终请求转发是有一个名为NettyRoutingFilter的过滤器来执行的,而且这个过滤器是整个过滤器链中顺序最靠后的一个。
网关过滤器链中的过滤器有两种:
- **GatewayFilter:路由过滤器,作用范围比较灵活,可以是任意指定的路由Route. **
- GlobalFilter:全局过滤器,作用范围是所有路由,不可配置。
过滤器链之外还有一种过滤器,HttpHeadersFilter,用来处理传递到下游微服务的请求头。例如org.springframework.cloud.gateway.filter.headers.XForwardedHeadersFilter可以传递代理请求原本的host头到下游微服务。
其实GatewayFilter和GlobalFilter这两种过滤器的方法签名完全一致:
/**
* 处理请求并将其传递给下一个过滤器
* @param exchange 当前请求的上下文,其中包含request、response等各种数据
* @param chain 过滤器链,基于它向下传递请求
* @return 根据返回值标记当前请求是否被完成或拦截,chain.filter(exchange)就放行了。
*/
Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);
FilteringWebHandler在处理请求时,会将GlobalFilter装饰为GatewayFilter,然后放到同一个过滤器链中,排序以后依次执行。
Gateway内置的GatewayFilter过滤器使用起来非常简单,无需编码,只要在yaml文件中简单配置即可。而且其作用范围也很灵活,配置在哪个Route下,就作用于哪个Route. 例如,有一个过滤器叫做AddRequestHeaderGatewayFilterFacotry,顾明思议,就是添加请求头的过滤器,可以给请求添加一个请求头并传递到下游微服务。 使用的使用只需要在application.yaml中这样配置:
spring:
cloud:
gateway:
routes:
- id: test_route
uri: lb://test-service
predicates:
-Path=/test/**
filters:
- AddRequestHeader=key, value # 逗号之前是请求头的key,逗号之后是value
如果想要让过滤器作用于所有的路由,则可以这样配置:
spring:
cloud:
gateway:
default-filters: # default-filters下的过滤器可以作用于所有路由
- AddRequestHeader=key, value
routes:
- id: test_route
uri: lb://test-service
predicates:
-Path=/test/**
自定义过滤器
无论是GatewayFilter还是GlobalFilter都支持自定义,只不过编码方式、使用方式略有差别。
自定义GatewayFilter
自定义GatewayFilter不是直接实现GatewayFilter,而是实现AbstractGatewayFilterFactory。最简单的方式是这样的:
@Component
public class PrintAnyGatewayFilterFactory extends AbstractGatewayFilterFactory<Object> {
@Override
public GatewayFilter apply(Object config) {
return new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 获取请求
ServerHttpRequest request = exchange.getRequest();
// 编写过滤器逻辑
System.out.println("过滤器执行了");
// 放行
return chain.filter(exchange);
}
};
}
}
注意:该类的名称一定要以GatewayFilterFactory为后缀!
然后在yaml配置中这样使用:
spring:
cloud:
gateway:
default-filters:
- PrintAny # 此处直接以自定义的GatewayFilterFactory类名称前缀类声明过滤器
另外,这种过滤器还可以支持动态配置参数,不过实现起来比较复杂,示例:
@Component
public class PrintAnyGatewayFilterFactory // 父类泛型是内部类的Config类型
extends AbstractGatewayFilterFactory<PrintAnyGatewayFilterFactory.Config> {
@Override
public GatewayFilter apply(Config config) {
// OrderedGatewayFilter是GatewayFilter的子类,包含两个参数:
// - GatewayFilter:过滤器
// - int order值:值越小,过滤器执行优先级越高
return new OrderedGatewayFilter(new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 获取config值
String a = config.getA();
String b = config.getB();
String c = config.getC();
// 编写过滤器逻辑
System.out.println("a = " + a);
System.out.println("b = " + b);
System.out.println("c = " + c);
// 放行
return chain.filter(exchange);
}
}, 100);
}
// 自定义配置属性,成员变量名称很重要,下面会用到
@Data
static class Config{
private String a;
private String b;
private String c;
}
// 将变量名称依次返回,顺序很重要,将来读取参数时需要按顺序获取
@Override
public List<String> shortcutFieldOrder() {
return List.of("a", "b", "c");
}
// 返回当前配置类的类型,也就是内部的Config
@Override
public Class<Config> getConfigClass() {
return Config.class;
}
}
然后在yaml文件中使用:
spring:
cloud:
gateway:
default-filters:
- PrintAny=1,2,3 # 注意,这里多个参数以","隔开,将来会按照shortcutFieldOrder()方法返回的参数顺序依次复制
上面这种配置方式参数必须严格按照shortcutFieldOrder()方法的返回参数名顺序来赋值。 还有一种用法,无需按照这个顺序,就是手动指定参数名:
spring:
cloud:
gateway:
default-filters:
- name: PrintAny
args: # 手动指定参数名,无需按照参数顺序
a: 1
b: 2
c: 3
自定义GlobalFilter
自定义GlobalFilter则简单很多,直接实现GlobalFilter即可,而且也无法设置动态参数:
@Component
public class PrintAnyGlobalFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 编写过滤器逻辑
System.out.println("未登录,无法访问");
// 放行
// return chain.filter(exchange);
// 拦截
ServerHttpResponse response = exchange.getResponse();
response.setRawStatusCode(401);
return response.setComplete();
}
@Override
public int getOrder() {
// 过滤器执行顺序,值越小,优先级越高
return 0;
}
}