一、Gataway的基本概念
Spring Cloud Gateway 作为SpringCloud生态系统中的网关,目标是替代Netflix Zuul。
Gateway不仅提供统一路由方式,并且基于Filter链的方式提供网关的基本功能。例如:安全,监控/指标,和限流。
表面上看起来,gateway貌似只是统一接收一个请求进而分发路由到不同的服务而已,实际上gateway的功能远不止于此。
gateway提供了一系列机制供我们实现众多功能,诸如:相关的鉴权,安全控制,日志统一处理,易于监控,限流。这些机制也是我们学习的重点,所以我们不要仅仅停留只会配置一个路由分发,比如集成一个权限控制到gateway中也是很不错的。
二、Gateway的工作原理
整个流程总结如下:
①、Gateway的客户端向Gateway服务发起请求,请求首先会被HttpWebHandlerAdapter进行提取组装成网关的上下文,然后网关的上下文会传递到DispatcherHandler
②、DispatcherHandler是所有请求的分发处理器,它主要负责分发请求对应的处理器。比如将请求分发到对应RoutedPredicatedHandlerMapping(路由断言处理器映射器)
③、路由断言处理器主要用于路由查找,以及找到路由后返回对应的FilteringWebHandler
④、FilteringWebHandler主要负责组装Filter链表并调用Filter执行一系列Filter处理,然后把请求转到后端对应的代理服务器,处理完毕后,将Response返回到Gateway客户端。
三、Gateway路由
Gateway路由配置分为基于配置的静态路由设置和基于代码动态路由配置
假设现在有一个场景:
要实现这个需求,我们逐一演示两种配置方式
1、基于配置的静态路由设置:配置如下
spring:
cloud:
gateway:
#路由配置
routes:
#唯一标识符
- id: hailtaxi-driver
uri: lb://hailtaxi-driver 表示负载均衡到名为hailtaxi-driver的服务
#路由断言
predicates:
- Path=/driver/**
#唯一标识符
- id: hailtaxi-order
uri: lb://hailtaxi-order
#路由断言
predicates:
- Path=/order/**
#唯一标识符
- id: hailtaxi-pay
uri: lb://hailtaxi-pay
#路由断言
predicates:
- Path=/pay/**
2、基于代码的动态路由配置:配置如下
/***
* 路由配置
* @param builder
* @return
*/
@Bean
public RouteLocator routeLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("hailtaxi-driver", r -> r.path("/driver/**").uri("lb://hailtaxi-driver"))
.route("hailtaxi-order", r -> r.path("/order/**").uri("lb://hailtaxi-order"))
.route("hailtaxi-pay", r -> r.path("/pay/**").uri("lb://hailtaxi-pay"))
.build();
}
路由中的断言
断言用于确定请求是否满足特定条件的逻辑组件,比如上面的配置就用到了Path断言,判断是否满足某种请求路径规则,满足的才分发到对应的服务
常见断言配置:
这里大概总结一下有哪些内置实现的断言
断言源码分析
拿Cookie断言来说,首先看它的体系结构
可以发现,其实所有的断言都是继承了AbstractRoutePredicateFactory
import org.springframework.cloud.gateway.support.AbstractRoutePredicateFactory;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.server.ServerWebExchange;
import javax.servlet.http.Cookie;
import javax.validation.constraints.NotEmpty;
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
public class CookieRoutePredicateFactory
extends AbstractRoutePredicateFactory {
/**
* Cookie 名称的键。
*/
public static final String NAME_KEY = "name";
/**
* 正则表达式的键。
*/
public static final String REGEXP_KEY = "regexp";
/**
* 构造函数,调用父类构造函数,指定配置类。
*/
public CookieRoutePredicateFactory() {
super(Config.class);
}
/*
通过shortcutFieldOrder方法设置Config配置类中的属性,需要根据具体的规则来设置
通过shortcutType方法获取具体规则,具体参看:org.springframework.cloud.gateway.support.ShortcutConfigurable.ShortcutType
规则包括以下几种:
DEFAULT : 按照shortcutFieldOrder顺序依次赋值
*/
@Override
public List shortcutFieldOrder() {
return Arrays.asList(NAME_KEY, REGEXP_KEY);
}
@Override
public Predicate apply(Config config) {
return new GatewayPredicate() {
/**
ServerWebExchange:用于表示 HTTP 请求和响应的交换,包含了请求的上下文信息以及响应的构建与发送功能
*/
@Override
public boolean test(ServerWebExchange exchange) {
// 获取请求中的指定名称的 Cookie 列表
List cookies = exchange.getRequest().getCookies()
.get(config.name);
// 如果没有找到 Cookie,返回 false
if (cookies == null) {
return false;
}
// 遍历所有 Cookie,检查其值是否匹配正则表达式
for (HttpCookie cookie : cookies) {
if (cookie.getValue().matches(config.regexp)) {
return true; // 找到匹配的 Cookie,返回 true
}
}
return false; // 未找到匹配的 Cookie,返回 false
}
/**
* 返回当前断言的字符串表示,便于调试。
*
* @return 字符串表示。
*/
@Override
public String toString() {
return String.format("Cookie: name=%s regexp=%s", config.name,
config.regexp);
}
};
}
/*
内部配置类是用来接收在配置文件中配置的参数的
routes:
#唯一标识符
- id: hailtaxi-driver
uri: lb://hailtaxi-driver
#路由断言
predicates:
- Cookie=username,admin
*/
@Validated
public static class Config {
@NotEmpty // 确保名称不能为空
private String name;
@NotEmpty // 确保正则表达式不能为空
private String regexp;
public String getName() {
return name;
}
public Config setName(String name) {
this.name = name;
return this; // 返回当前对象,支持链式调用
}
public String getRegexp() {
return regexp;
}
public Config setRegexp(String regexp) {
this.regexp = regexp;
return this; // 返回当前对象,支持链式调用
}
}
}
在 shortcutFieldOrder中去设置好config对应的属性值,这样配置中的username就赋给了config中的name,配置中的admin就赋给了config中的regexp。
自定义断言规则器
假设现在要完成这么一个需求:
转发带token的请求到hailtaxi-drvier
服务中,这里定义请求带token是指包含某个请求头的请求,至于是什么请求头可以由配置指定
1、修改配置文件
gateway:
#路由配置
routes:
#唯一标识符
- id: hailtaxi-driver
uri: lb://hailtaxi-driver
#路由断言
predicates:
自定义一个Token断言,如果请求包含Authorization的token信息则通过
- Token=Authorization
2、创建 RoutePredicateFactory
断言工厂默认命名规则必须按照"名称"+RoutePredicateFactory,如上TokenRoutePredicateFactory的断言名称为Token
@Slf4j
@Component // 要交给spring容器管理
public class TokenRoutePredicateFactory extends AbstractRoutePredicateFactory {
public TokenRoutePredicateFactory() {
super(Config.class);
}
public Predicate apply(Config config) {
return exchange -> {
// 打印配置文件参数值
String headerName = config.getHeaderName();
HttpHeaders headers = exchange.getRequest().getHeaders();
List header = headers.get(headerName);
log.info("Token Predicate headers:{}", header);
// 断言返回的是boolean值
return header!=null && header.size()>0;
};
}
@Override
public List shortcutFieldOrder() {
return Arrays.asList("headerName");//指定配置文件中加载到的配置信息应填充到Config的哪个属性上
}
@Override
public ShortcutType shortcutType() {
return ShortcutType.DEFAULT;
}
@Data
public static class Config { //static class
private String headerName;//存储从配置文件中加载的配置
}
}
四、Gateway过滤器
Spring Cloud Gateway根据作用范围划分为GatewayFilter和GlobalFilter
GatewayFilter需要在配置中显式的配置才生效
GlobalFilter是全局过滤器,不需要在配置文件中配置,作用在所有的路由上。
这里列举两个常用的内置过滤器:
1)添加响应头
AddResponseHeaderGatewayFilterFactory 属于 GatewayFilter
对输出响应头设置属性,比如对输出的响应设置其头部属性名称为:X-Response-Default-MyName , 值为admin
修改配置文件,配置如下:
spring:
cloud:
gateway:
配置全局默认过滤器 作用在所有路由上,也可单独为某个路由配置
default-filters:
往响应过滤器中加入信息
- AddResponseHeader=X-Response-Default-MyName,itheima
2)前缀处理
在项目中做开发对接接口的时候,我们很多时候需要统一API路径,比如统一以/api开始的请求调用hailtaxi-driver服务,但真实服务接口地址又没有/api路径,我们可以使用Gateway的过滤器处理请求路径。
在gateway中可以通过配置路由的过滤器StripPrefix实现映射路径中的前缀处理,我们来使用一下该过滤器,再进一步做说明。
gateway:
routes:
- id: hailtaxi-driver
uri: lb://hailtaxi-driver
predicates:
- Path=/api/driver/**
filters:
- StripPrefix=1
此处- StripPrefix=1表示真实请求地址是当前用户请求以/api开始的uri中去除第1个路径/api.
自定义GatewayFilter
1、实现GatewayFilter接口
GatewayFilter 一般作用在某一个路由上,需要实例化创建才能使用,局部过滤器需要实现接口GatewayFilter、Ordered。
创建com.demo.filter.PayFilter代码如下:
public class PayFilter implements GatewayFilter,Ordered {
/***
* 过滤器执行拦截
* @param exchange
* @param chain
* @return
*/
@Override
public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("GatewayFilter拦截器执行---pre-----PayFilter");
return chain.filter(exchange).then(Mono.fromRunnable(()->{
System.out.println("GatewayFilter拦截器执行---post-----PayFilter");
}));
}
@Override
public int getOrder() {
return 0;
}
}
2、继承GatewayFilterFactory
如果定义局部过滤器,想在配置文件中进行配置来使用,可以继承AbstractGatewayFilterFactory
过滤器工厂默认命名规则必须按照"名称"+GatewayFilterFactory`,如上StripPrefixGatewayFilterFactory的过滤器名称为StripPrefix
2.1、继承AbstractGatewayFilterFactory
需求:
在网关中统一支付方式,编写一个过滤器:PayMethodGatewayFilterFactory
,
1、编写过滤器
@Slf4j
@Component //一定要将其交给spring容器管理
public class PayMethodGatewayFilterFactory extends AbstractGatewayFilterFactory {
public PayMethodGatewayFilterFactory() {
super(Config.class);
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
String paymethod = config.getPayMethod();
String msg = config.getMsg();
log.info("PayMethodGatewayFilterFactory 加载到的配置信息为:{}---{}",paymethod,msg);
//将paymethod添加到请求头中
exchange.getRequest().mutate().header("paymethod",paymethod);
return chain.filter(exchange);
};
}
@Override
public List shortcutFieldOrder() {
return Arrays.asList("payMethod","msg");//指定从yml中提前出来的配置信息填充到配置类中哪个属性,按规则配置
}
@Override
public ShortcutType shortcutType() {
return ShortcutType.DEFAULT;//默认规则
}
/**
* 加载从yml中提取出来的配置信息
*/
@Data
public static class Config {
private String payMethod;
private String msg;
}
}
2、配置文件中使用如下:
gateway:
#路由配置
routes:
#唯一标识符
- id: hailtaxi-driver
uri: lb://hailtaxi-driver
#路由断言
predicates:
- Path=/driver/**
- Cookie=username,admin
- Header=token,^(?!\d+$)[\da-zA-Z]+$
- Method=GET,POST
- Token=Authorization
filters:
- PayMethod=alipay,业务整合
其实在filter中就可以实现任何你想做的操作了,包括日志记录,认证与授权等等
五、跨域配置
出于浏览器的同源策略限制。同源策略(Sameoriginpolicy)是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能可能都会受到影响。可以说Web是构建在同源策略基础之上的,浏览器只是针对同源策略的一种实现。同源策略会阻止一个域的javascript脚本和另外一个域的内容进行交互。所谓同源(即指在同一个域)就是两个页面具有相同的协议(protocol),主机(host)和端口号(port)。
在Spring Cloud Gateway中配置跨域是非常简单的,如下面application.yml所示:
gateway:
globalcors:
corsConfigurations:
'[/**]':
allowedOrigins: "*"
allowedMethods:
- GET
- POST
- PUT
另外一种写法就需要创建CorsWebFilter过滤器,代码如下:
/**
* 配置跨域
* @return
*/
@Bean
public CorsWebFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
// cookie跨域
config.setAllowCredentials(Boolean.TRUE);
config.addAllowedMethod("*");
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
// 配置前端js允许访问的自定义响应头
config.addExposedHeader("Authorization");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
source.registerCorsConfiguration("/**", config);
return new CorsWebFilter(source);
}