客户端直接请求服务的方式存在以下问题:
- 当服务数量多, 客户端需要维护大量的服务地址
- 在某些场景下存在跨域请求
- 身份认证难度大
可以通过API网关解决
一、API 网关
- 概述
- 是一个搭建在客户端与微服务之间的服务模块
- 网关中可以处理非业务逻辑(验证,监控,缓存,请求路由等)
- 就像门, 系统对外的入口
- 使用好处
- 不需要维护大量的服务地址, 简化了服务端的开发
- 减少客户端与服务端的交互次数, 降低耦合度
- 节省流量, 提高性能, 提高用户体验
- 提供了安全,流控,过滤,缓存,计费,监控等api管理功能
常⻅的 API 网关实现⽅案主要有以下几种:
-
Spring Cloud Gateway:Spring公司为了替换Zuul而开发的网关服务,底层为Netty。
-
Spring Cloud Netflix Zuul:Zuul2 采用了Netty实现异步非阻塞编程模型,每个 CPU 核一个线程,处理所有的请求和响应,请求和响应的生命周期是通过事件和回调来处理的,这种方式减少了线程数量,因此开销较小。
-
Kong:基于Nginx+Lua开发,性能高,稳定,有多个可用的插件(限流、鉴权等等)可以开箱即用。 但是只支持Http协议;二次开发,自由扩展困难;提供管理API,缺乏更易用的管控、配置方式。
-
Nginx+Lua:使用nginx的反向代理和负载均衡可实现对api服务器的负载均衡及高可用,lua是一种脚本语言,可以来编写一些简单的逻辑, nginx支持lua脚本,但是无法融入到微服务架构中。
二、Spring Cloud Gateway
Spring Cloud Gateway 是 Spring Cloud 团队基于 Spring 5.0、Spring Boot 2.0 和 Project Reactor等技术开发的高性能 API 网关组件。
Spring Cloud Gateway 旨在提供一种简单而有效的途径来发送 API,并为它们提供横切关注点,例如:安全性,监控/指标和弹性。
Spring Cloud Gateway 是基于 WebFlux 框架实现的,而 WebFlux 框架底层则使用了高性能的Reactor 模式通信框架 Netty。
1、Spring Cloud Gateway 核心概念
Spring Cloud GateWay 最主要的功能就是路由转发,而在定义转发规则时主要涉及了以下三个核⼼概念,如下表。
注意:其中 Route 和 Predicate 必须同时声明。
2、Spring Cloud Gateway 的特征
Spring Cloud Gateway 具有以下特性:
-
基于 Spring Framework 5、Project Reactor 和 Spring Boot 2.0 构建。
-
能够在任意请求属性上匹配路由。
-
predicates(断⾔) 和 filters(过滤器)是特定于路由的。
-
集成了 Hystrix 熔断器。
-
集成了 Spring Cloud DiscoveryClient(服务发现客户端)。
-
易于编写断⾔和过滤器。
-
能够限制请求频率。
-
能够重写请求路径。
3、Gateway 的工作流程
Spring Cloud Gateway 工作流程如下图:
Spring Cloud Gateway 工作流程说明如下:
-
客户端将请求发送到 Spring Cloud Gateway 上。
-
Spring Cloud Gateway 通过 Gateway Handler Mapping 找到与请求相匹配的路由,将其发送给Gateway Web Handler。
-
Gateway Web Handler 通过指定的过滤器链(Filter Chain),将请求转发到实际的服务节点中,执行业务逻辑返回响应结果。
过滤器之间用虚线分开是因为过滤器可能会在转发请求之前(pre)或之后(post)执行业务逻辑。
-
过滤器(Filter)可以在请求被转发到服务端前,对请求进行拦截和修改,例如参数校验、权限校验、流量监控、日志输出以及协议转换等。
-
过滤器可以在响应返回客户端之前,对响应进行拦截和再处理,例如修改响应内容或响应头、日志输出、流量监控等。
-
响应原路返回给客户端。
总而⾔之,客户端发送到 Spring Cloud Gateway 的请求需要通过一定的匹配条件,才能定位到真正的服务节点。在将请求转发到服务进行处理的过程前后(pre 和 post),我们还可以对请求和响应进行一些精细化控制。
三、GateWay部署
1、自动路由转发
创建新应用,引入以下依赖:
Nacos服务发现: (网关也需要注册到服务中心中)
网关:
负载均衡: (网关也是将请求分配给多个服务器, 所以需要负载均衡)
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-loadbalancer</artifactId>
</dependency>
书写全局配置文件:
spring:
application:
name: GateWayDemo
cloud:
nacos: # nacos配置,gateway也需要注册至nacos
discovery:
server-addr: 192.168.10.111:8848
username: nacos
password: nacos
gateway: # gateway配置
discovery:
locator:
enabled: true # 是否启用服务发现,为true让gateway通过nacos实现自动路由转发
lower-case-service-id: true # 将服务 ID 转换为小写
server:
port: 9000
启动video-service、article-service,然后再启动路由,在浏览器中输入以下URL进行测试
http://网关的IP:网关的端口号/nacos中已注册的服务名/服务的资源路径?参数
http://localhost:9000/video-service/video?articleId=1648
http://localhost:9000/article-service/list
2、配置路由规则的三个关键名词
三个关键名词
Route(路由):网关的转发规则
例如localhost:9000/article-service/list转发至localhost:8100/list
Predicate(谓词):路由的生效条件
After=2020-10-04T00:00:00.000+08:00[Asia/Shanghai] 代表2020年10月4日凌晨路由规则生效
Filter(过滤器):允许修改请求以及响应的内容
例如:路由转发时在请求头附加"auth"信息等操作
3、谓词
谓词是⼀种用于匹配请求的逻辑规则。它可以根据请求的方法、路径、头信息等进行匹配,并将请求路由到相应的目标服务。Spring Cloud Gateway 内置了多个谓词,例如 Path、Method、Header、Cookie、After、Before 等,同时也支持自定义谓词。多个谓词工厂可以通过逻辑与进行组合。
常见谓词示例:
(1)After 路由谓词工厂
After 路由谓词工厂采用一个 datetime 类型的参数。此谓词匹配当前日期时间之后发生的请求。
spring:
cloud:
gateway:
routes:
- id: after_route # nacos中注册的服务名
uri: http://example.org # 服务所在的URL站点路径
predicates:
- After=2020-10-13T21:57:33.993+08:00[Asia/Shanghai]
当系统时间(上海时区)大于2020年10月13日21点57分30秒后 ,才会将请求进行路由
(2)Before 路由谓词工厂
Before 路由谓词工厂采用一个 datetime 类型的参数。此谓词匹配当前日期时间之前发生的请求。
spring:
cloud:
gateway:
routes:
- id: after_route # nacos中注册的服务名
uri: http://example.org # 服务所在的URL站点路径
predicates:
- Before=2020-10-13T21:57:33.993+08:00[Asia/Shanghai]
当系统时间(上海时区)小于2020年10月13日21点57分30秒 ,才会将请求进行路由
(3)Between 路由谓词工厂
Between 路由谓词工厂采用两个参数,datetime1 和datetime2。此谓词匹配datetime1 之后和 datetime2 之前发生的请求。datetime2 参数必须在 datetime1 之后。
spring:
cloud:
gateway:
routes:
- id: between_route
uri: http://example.org
predicates:
- Between=2017-01-20T17:42:47.789+08:00[Asia/Shanghai], 2017-01-21T17:42:47.789+08:00[Asia/Shanghai]
此路由与 2017 年 1 月 20 日 17:42之后和 2017 年 1 月 21 日17:42之前的所有请求相匹配,这对维护窗口很有用。
(4)Cookie 路由谓词工厂
Cookie 路由谓词工厂采用两个参数,cookie 名称和正则表达式。此谓词匹配具有给定名称的 cookie,值与正则表达式匹配。
spring:
cloud:
gateway:
routes:
- id: cookie_route
uri: http://example.org
predicates:
- Cookie=chocolate, ch.p
此路由与请求匹配的 cookie 名称为 chocolate,其值与 CH.P 正则表达式匹配。
(5)Header 路由谓词工厂
Header 路由谓词工厂采用两个参数,Header 名称和正则表达式。此谓词与具有给定名称且值与正则表达式匹配的 Header 匹配。
spring:
cloud:
gateway:
routes:
- id: header_route
uri: http://example.org
predicates:
- Header=X-Request-Id, \d+
如果请求具有名为 X-Request-Id 的 Header,其值与\d+正则表达式匹配(具有一个或多个数字的值),则该路由匹配。
(6)Host 路由谓词工厂
Host 路由谓词工厂采用一个参数:主机名模式。该模式是一种 Ant 样式模式作为分隔符。此谓词与匹配该模式的主机头部匹配。
spring:
cloud:
gateway:
routes:
- id: host_route
uri: http://example.org
predicates:
- Host=**.somehost.org
如果请求的主机头部具有值www.somehost.org或beta.somehost.org,则此路由将匹配。
(7)Method 路由谓词工厂
Method 路由谓词工厂采用一个参数:要匹配的 HTTP 方法。
spring:
cloud:
gateway:
routes:
- id: method_route
uri: http://example.org
predicates:
- Method=GET
如果请求方法是 GET,则此路由将匹配。
(8)Path 路由谓词工厂
Path 路由谓词工厂采用一个参数:Spring PurthMatter 模式。
spring:
cloud:
gateway:
routes:
- id: host_route
uri: http://example.org
predicates:
- Path=/foo/{segment}
如果请求路径如:/foo/1 或/foo/bar,则此路由将匹配。
此谓词提取 URI 模板变量(如上面示例中定义的 segment)作为名称和值的映射,并将其放置在 ServerWebExchange.getAttributes()中,其中键定义在PathRoutePre.e.URL_PREDICATE_VARS_ATTR 中。这些值随后可供网关过滤器工厂使用。
(9)Query 路由谓词工厂
Query 路由谓词工厂采用两个参数:一个必需的参数和一个可选的正则表达式。
spring:
cloud:
gateway:
routes:
- id: query_route
uri: http://example.org
predicates:
- Query=baz
如果请求包含 baz 查询参数,此路由将匹配。
spring:
cloud:
gateway:
routes:
- id: query_route
uri: http://example.org
predicates:
- Query=foo, ba.
如果请求包含一个 foo 查询参数,它的值与 ba.正则表达式匹配,则此路由将匹配,所以bar 和 baz 都将匹配。
(10)RemoteAddr 路由谓词工厂
RemoteAddr 路由谓词工厂采用 CIDR 符号(IPv4 或 IPv6)字符串的列表(最小值为1),例如 192.168.0.1/16(其中 192.168.0.1 是 IP 地址,16 是子网掩码)。
spring:
cloud:
gateway:
routes:
- id: remoteaddr_route
uri: http://example.org
predicates:
- RemoteAddr=192.168.1.1/24
如果请求的远程地址为192.1681.10,则该路由将匹配。
4、GatewayFilter 过滤器
过滤器则是用于在请求被转发到⽬标服务之前或之后对请求进行处理的组件。它可以对请求和响应进行修改、增强和过滤,例如添加请求头、修改请求体、限流、重试等。Spring Cloud Gateway 内置了多个过滤器,例如 AddRequestHeader、RewritePath、RequestRateLimiter 等,同时也⽀持⾃定义过滤器。
路由过滤器的作用域是特定的路由。
使⽤谓词和过滤器,可以实现以下功能:
-
路由:根据请求的特定条件将请求转发到不同的⽬标服务。
-
重定向:将请求重定向到不同的URL或服务。
-
过滤:对请求和响应进行修改、增强或过滤,例如添加请求头、修改请求体、限流、重试等。
-
鉴权:对请求进行认证和授权。
-
监控:记录请求的日志和统计信息。
Spring Cloud Gateway 包括许多内置的网关过滤器工厂,具体如下。
(1)AddRequestHeader 过滤器
AddRequestHeader过滤器使用两个参数(name && value )
spring:
cloud:
gateway:
routes:
- id: add_request_header_route
uri: https://example.org
filters:
- AddRequestHeader=X-Request-red, blue
这个路由所有匹配请求的下游请求的头部中添加了 X-Request-red: blue。
AddRequestHeader通常用户匹配请求路径或者host中的变量。下面的例子中会将诸如 https://xxx.org/red/snzz 中的 snzz 放入 X-Request-red的value中。
spring:
cloud:
gateway:
routes:
- id: add_request_header_route
uri: https://example.org
predicates:
- Path=/red/{segment}
filters:
- AddRequestHeader=X-Request-Red, Blue-{segment}
(2)AddRequestParameter 添加参数过滤器
AddRequestParameter过滤器接受 name和 value参数
spring:
cloud:
gateway:
routes:
- id: add_request_parameter_route
uri: https://example.org
filters:
- AddRequestParameter=red, blue
上面的例子中为所有请求的下游请求的查询字符串red=blue。
通常用于为指定请求路径或者域名的请求中添加请求参数,下面的例子中。会为myhost.org域名下的所有请求加上一个参数,假如访问的是dev.myhost.org,那么请求中就会加上一个foo=bar-dev的参数。
spring:
cloud:
gateway:
routes:
- id: add_request_parameter_route
uri: https://example.org
predicates:
- Host: {segment}.myhost.org
filters:
- AddRequestParameter=foo, bar-{segment}
(3)AddResponseheader 过滤器
AddResponseHeader 采用 name和 value 参数:
spring:
cloud:
gateway:
routes:
- id: add_response_header_route
uri: https://example.org
filters:
- AddResponseHeader=X-Response-Red, Blue
这将为所有匹配请求的下游响应头添加 X-Response-Red: Bar 头。
通常用于为指定请求路径或者域名的请求响应的消息头添加参数,下面的例子中。会为myhost.org域名下的所有请求响应体的头部加上一个参数,假如访问的是dev.myhost.org,那么响应头中就会加上一个foo=bar-dev的参数。
spring:
cloud:
gateway:
routes:
- id: add_response_header_route
uri: https://example.org
predicates:
- Host: {segment}.myhost.org
filters:
- AddResponseHeader=foo, bar-{segment}
(4)DedupeResponseHeader 去重响应过滤器
DedupeResponseHeader接受一个name参数和一个可选的strategy 参数。Name可以包含以空格分隔的header名称列表。
application.yml
spring:
cloud:
gateway:
routes:
- id: dedupe_response_header_route
uri: https://example.org
filters:
- DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin
在网关 CORS 逻辑和下游逻辑都添加了Access-Control-Allow-Credentials和Access-Control-Allow-Origin响应头的情况下,这将删除重复的值。
DedupeResponseHeader过滤器还接受一个可选的策略参数。接受的值是 RETAIN_FIRST (默认值:保留第一个)、 RETAIN_FIRST (保留最后一个)和 RETAIN_UNIQUE(保留唯一的)。
(5)Spring Cloud CircuitBreaker
Spring Cloud 断路器网关过滤器工厂使用 Spring Cloud 断路器 api 将网关路由包裹在断路器中。 支持多个可以与 Spring 云网关一起使用的库。 Spring Cloud 开箱即用地支持Resilience4J。
要启用 Spring Cloud CircuitBreaker 过滤器,需要将 Spring-Cloud-starter-CircuitBreaker-reactor-resilience4j 放在类路径上。
spring:
cloud:
gateway:
routes:
- id: circuitbreaker_route
uri: https://example.org
filters:
- CircuitBreaker=myCircuitBreaker
(6)默认过滤器
你可以声明spring.cloud.gateway.default-filters来为所有的路由添加过滤器,这个属性是一个list。
spring:
cloud:
gateway:
default-filters:
- AddResponseHeader=X-Response-Default-Red, Default-Blue
- PrefixPath=/httpbin
上面的配置会为httpbin请求路径下的请求的ResponseHeader中加入参数X-Response-Default-Red=Default-Blue。
具体过滤器总结如下表:
四、高级功能
1、自定义谓词
为了编写路由谓词,您需要将RoutePredicateFactory实现为bean,并继承⼀个名为AbstractRoutePredicateFactory的抽象类。
注意:路由谓词工厂必须以RoutePredicateFactory作为后缀,类名前缀就是谓词的名称
比如:TokenHeaderRoutePredicateFactory,TokenHeader就是谓词的名称
案例:自定义一个谓词,用于检测请求头中是否含有Token字段,若有则路由将该请求进行转发,反之则过滤。
定义一个配置类,用于表示请求头名称:
public class HeaderNameConfig {
private String headerName;
public String getHeaderName() {
return headerName;
}
public void setHeaderName(String headerName) {
this.headerName = headerName;
}
}
书写自定义谓词工厂类
/**
* 自定义路由谓词工厂
* 路由谓词工厂必须以RoutePredicateFactory结尾
*/
@Component
public class TokenHeaderRoutePredicateFactory
extends AbstractRoutePredicateFactory<HeaderNameConfig> {
public TokenHeaderRoutePredicateFactory() {
super(HeaderNameConfig.class);
}
/**
* 请求的匹配规则(即:路由转发请求的条件)
* @param config
* @return
*/
@Override
public Predicate<ServerWebExchange> apply(HeaderNameConfig config) {
Predicate<ServerWebExchange> predicate = new Predicate<ServerWebExchange>() {
@Override
public boolean test(ServerWebExchange exchange) {
Set<Map.Entry<String, List<String>>> headers = exchange.getRequest().getHeaders().entrySet();
for(Map.Entry me : headers){
if(me.getKey().equals(config.getHeaderName())){
return true;
}
}
return false;
}
};
return predicate;
}
/**
* 用于将 全局配置文件中的属性 与 配置类中的属性 进行映射
* @return
*/
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("headerName");
}
}
修改全局配置文件:
gateway: # gateway配置
discovery:
locator:
enabled: false # 是否启用服务发现,为true让gateway通过nacos实现自动路由转发
lower-case-service-id: true # 将服务 ID 转换为小写
routes:
- id: video-service # nacos中注册的服务名
uri: http://localhost:8001 # 服务所在的URL站点路径
predicates:
- Path=/video/**
- TokenHeader=Token
使用ApiFox测试:
(1)未携带Token请求头字段,会报404
(2)反之,正常
2、自定义过滤器
为了编写路由过滤器,您需要将GatewayFilterFactory实现为bean,并继承⼀个名为AbstractGatewayFilterFactory的抽象类。
注意:路由过滤器工厂必须以GatewayFilterFactory作为后缀,类名前缀就是过滤器的名称
比如:RequestLoggingGatewayFilterFactory,RequestLogging就是谓词的名称
案例:实现一个自定义过滤器,用于输出请求、响应信息。
请求信息的配置类:
@Data
public class RequestInfoConfig {
private String method;
}
请求相关的过滤器
@Slf4j
@Component
public class RequestLoggingGatewayFilterFactory extends AbstractGatewayFilterFactory<RequestInfoConfig> {
public RequestLoggingGatewayFilterFactory() {
super(RequestInfoConfig.class);
}
/**
* 过滤规则
* @param config
* @return
*/
@Override
public GatewayFilter apply(RequestInfoConfig config) {
return new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
if ( request.getMethod().toString().equalsIgnoreCase(config.getMethod()) ){
log.info("【请求过滤器】 - Method: {}, URI: {}, Headers: {}",
request.getMethod(), request.getURI(), request.getHeaders());
}else{
log.info("请求方法不匹配,不输出请求过滤日志!!!");
}
return chain.filter(exchange);
}
};
}
/**
* 用于将 全局配置文件中的属性 与 配置类中的属性 进行映射
* @return
*/
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("RequestLogging");
}
}
响应信息相关的配置类
public class ResponseInfoConfig {
}
响应相关的过滤器
@Slf4j
@Component
public class ResponseLoggingGatewayFilterFactory extends AbstractGatewayFilterFactory<ResponseInfoConfig> {
public ResponseLoggingGatewayFilterFactory() {
super(ResponseInfoConfig.class);
}
/**
* 过滤规则
* @param config
* @return
*/
@Override
public GatewayFilter apply(ResponseInfoConfig config) {
return (exchange, chain) -> chain.filter(exchange).doFinally(
signalType -> {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
log.info("Response Filter - Method: {}, URI: {}, Status code: {}, Headers: {}", request.getMethod(), request.getURI(), response.
getStatusCode(), response.getHeaders());
}
);
}
}
3、全局过滤器
全局过滤器必须实现两个接口:GlobalFilter、Ordered
-
GlobalFilter是全局过滤器接口,实现类要实现filter()方法进行功能扩展
-
Ordered接口用于排序,通过实现getOrder()方法返回整数代表执行当前过滤器的前后顺序
案例:统计/video数据接口的处理时间
@Component //⾃动实例化并被Spring IOC容器管理
public class ElapsedFilter implements GlobalFilter, Ordered {
//基于slf4j.Logger实现⽇志输出
private static final Logger logger = LoggerFactory.getLogger(ElapsedFilter.class);
//起始时间属性名
private static final String ELAPSED_TIME_BEGIN = "elapsedTimeBegin";
/**
* 实现filter()⽅法记录处理时间
* @param exchange ⽤于获取与当前请求、响应相关的数据,以及设置过滤器间传递的上
下⽂数据
* @param chain Gateway过滤器链对象
* @return Mono对应⼀个异步任务,因为Gateway是基于Netty Server异步处理的,Mon
o对就代表异步处理完毕的情况。
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//Pre前置处理部分
//在请求到达时,往ServerWebExchange上下⽂环境中放⼊了⼀个属性elapsedTimeBegin,保存请求执⾏前的时间戳
exchange.getAttributes().put(ELAPSED_TIME_BEGIN, System.currentTimeMillis());
//chain.filter(exchange).then()对应Post后置处理部分
//当响应产⽣后,记录结束与elapsedTimeBegin起始时间⽐对,获取RESTful API的实际执⾏时间
return chain.filter(exchange).then(
Mono.fromRunnable(() -> { //当前过滤器得到响应时,计算并打印时间
Long startTime = exchange.getAttribute(ELAPSED_TIME_BEGIN);
if (startTime != null) {
logger.info(exchange.getRequest().getRemoteAddress() //远程访问的⽤户地址
+ " | " + exchange.getRequest().getPath() //Gateway URI
+ " | cost " + (System.currentTimeMillis() - startTime) + "ms"); //处理时间
}
})
);
}
//设置为最⾼优先级,最先执⾏ElapsedFilter过滤器
//return Ordered.LOWEST_PRECEDENCE; 代表设置为最低优先级
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
}
spring:
application:
name: gateway
cloud:
gateway: #让gateway通过nacos实现自动路由转发
...
globalcors: #网关CORS跨域设置
cors-configurations:
'[/**]': #gateway网关上所有URI都应用下面的跨域设计
allowed-credentials: true #允许携带认证信息
allowed-origins:
- "*" #允许所有来源进行跨域访问 ,http://localhost
allowed-headers: "*" #允许跨域请求里的head字段,设置*为全部
allowed-methods: # 允许的跨域ajax的请求方式
- GET
- POST
- PUT
- DELETE
- OPTIONS
max-age: 3600 # 本次跨域检测的有效期