专栏目录
- 1-Spring Cloud Alibaba实战-系统架构演变与微服务常见问题
- 2-Spring Cloud Alibaba实战-简介与组件说明
- 3-Spring Cloud Alibaba实战-全组件项目搭建手册
- 4-Spring Cloud Alibaba实战-服务治理
- 5-Spring Cloud Alibaba实战-Nacos Config–配置中心
- 6-Spring Cloud Alibaba实战-Gateway网关核心架构
- 7-Spring Cloud Alibaba实战-Gateway网关核心原理
- 8-Spring Cloud Alibaba实战-分布式服务治理机制
- 9-Spring Cloud Alibaba实战-Sentinel原理
- 10-Spring Cloud Alibaba实战-Sleuth–链路追踪
- 11-Spring Cloud Alibaba实战-Nacos核心原理
Gateway核心架构
基本概念
路由(Route) 是 gateway 中最基本的组件之一,表示一个具体的路由信息载体。主要定义了下面的几个信息:
id,路由标识符,区别于其他 Route。
uri,路由指向的目的地 uri,即客户端请求最终被转发到的微服务。
order,用于多个 Route 之间的排序,数值越小排序越靠前,匹配优先级越高。
predicate,断言的作用是进行条件判断,只有断言都返回真,才会真正的执行路由。
fifilter,过滤器用于修改请求和响应信息。
执行流程
执行流程大体如下:
GatewayClient向GatewayServer发送请求
请求首先会被HttpWebHandlerAdapter进行提取组装成网关上下文
然后网关的上下文会传递到DispatcherHandler,它负责将请求分发给
RoutePredicateHandlerMapping
RoutePredicateHandlerMapping负责路由查找,并根据路由断言判断路由是否可用
如果过断言成功,由FilteringWebHandler创建过滤器链并调用
请求会一次经过PreFilter–微服务-PostFilter的方法,最终返回响应
断言
Predicate(断言, 谓词) 用于进行条件判断,只有断言都返回真,才会真正的执行路由。断言就是说:在 什么条件下 才能进行路由转发
内置路由断言工厂
SpringCloud Gateway包括许多内置的断言工厂,所有这些断言都与HTTP请求的不同属性匹配。具体如下:
基于Datetime类型的断言工厂
此类型的断言根据时间做判断,主要有三个:
AfterRoutePredicateFactory: 接收一个日期参数,判断请求日期是否晚于指定日期BeforeRoutePredicateFactory: 接收一个日期参数,判断请求日期是否早于指定日期BetweenRoutePredicateFactory: 接收两个日期参数,判断请求日期是否在指定时间段内
-After=2019-12-31T23:59:59.789+08:00[Asia/Shanghai]
基于远程地址的断言工厂 RemoteAddrRoutePredicateFactory:接收一个IP地址段,判断请求主机地址是否在地址段中
-RemoteAddr=192.168.1.1/24
基于Cookie的断言工厂
CookieRoutePredicateFactory:接收两个参数,cookie 名字和一个正则表达式。 判断请求cookie是否具有给定名称且值与正则表达式匹配。
-Cookie=chocolate, ch.
基于Header的断言工厂
HeaderRoutePredicateFactory:接收两个参数,标题名称和正则表达式。 判断请求Header是否具有给定名称且值与正则表达式匹配。
-Header=X-Request-Id, \d+
基于Host的断言工厂
HostRoutePredicateFactory:接收一个参数,主机名模式。判断请求的Host是否满足匹配规则。
-Host=**.testhost.org
基于Method请求方法的断言工厂
MethodRoutePredicateFactory:接收一个参数,判断请求类型是否跟指定的类型匹配。
-Method=GET
基于Path请求路径的断言工厂
PathRoutePredicateFactory:接收一个参数,判断请求的URI部分是否满足路径规则。
-Path=/foo/{segment}
基于Query请求参数的断言工厂
QueryRoutePredicateFactory:接收两个参数,请求param和正则表达式, 判断请求参数是否具有给定名称且值与正则表达式匹配。
-Query=baz, ba.
基于路由权重的断言工厂
WeightRoutePredicateFactory:接收一个[组名,权重], 然后对于同一个组内的路由按照权重转发
routes:
-id: weight_route1 uri: host1 predicates:
-Path=/product/**
-Weight=group3, 1
-id: weight_route2 uri: host2 predicates:
-Path=/product/**
-Weight= group3, 9
内置路由断言工厂的使用
接下来我们验证几个内置断言的使用:
server:
port: 7000
spring:
application:
name: api-gateway
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
gateway:
discovery:
locator:
enabled: true
routes:
- id: product_route
uri: lb://service-product
predicates:
- Path=/product-serv/**
- Before=2019-11-28T00:00:00.000+08:00 #限制请求时间在2019-11-28之前
- Method=POST #限制请求方式为POST filters:
- StripPrefix=1
自定义路由断言工厂
spring:
application:
name: api-gateway
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
gateway:
discovery:
locator:
enabled: true
routes:
- id: product-route
uri: lb://service-product
predicates:
- Path=/product-serv/**
- Age=18,60 # 限制年龄只有在18到60岁之间的人能访问
filters: - StripPrefix=1
我们来设定一个场景: 假设我们的应用仅仅让age在(min,max)之间的人来访问。第1步:在配置文件中,添加一个Age的断言配置
第2步:自定义一个断言工厂, 实现断言方法
package com.naixue.predicates; //泛型 用于接收一个配置类,配置类用于接收中配置文件中的配置
@Component
public class AgeRoutePredicateFactory extends AbstractRoutePredicateFactory<AgeRoutePredicateFactory.Config> { public AgeRoutePredicateFactory() { super(AgeRoutePredicateFactory.Config.class);
}
//用于从配置文件中获取参数值赋值到配置类中的属性上@Override
public List<String> shortcutFieldOrder() {
//这里的顺序要跟配置文件中的参数顺序一致
return Arrays.asList("minAge", "maxAge");
}
//断言@Override
public Predicate<ServerWebExchange> apply(AgeRoutePredicateFactory.Config
config) {
return new Predicate<ServerWebExchange>() {
@Override
public boolean test(ServerWebExchange serverWebExchange) {
//从serverWebExchange获取传入的参数String ageStr =
serverWebExchange.getRequest().getQueryParams().getFirst("age");
if (StringUtils.isNotEmpty(ageStr)) {
int age =Integer.parseInt(ageStr);
return age >config.getMinAge() && age< config.getMaxAge();
}
return true;
}
};
}
}
//自定义一个配置类, 用于接收配置文件中的参数
@Data
private intminAge; private intmaxAge;
}
第4步:启动测试
#测试发现当age在(20,60)可以访问,其它范围不能访问
http://localhost:7000/product-serv/product/1?age=30
http://localhost:7000/product-serv/product/1?age=10
过滤器
三个知识点:
作用: 过滤器就是在请求的传递过程中,对请求和响应做一些手脚
生命周期: PrePost
分类: 局部过滤器(作用在某一个路由上) 全局过滤器(作用全部路由上)
在Gateway中, Filter的生命周期只有两个:“pre” 和 “post”。
PRE: 这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。 POST:这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等。
Gateway的Filter从作用范围可分为两种: GatewayFilter与GlobalFilter。
GatewayFilter:应用到单个路由或者一个分组的路由上。GlobalFilter:应用到所有的路由上。
局部过滤器
局部过滤器是针对单个路由的过滤器。
内置局部过滤器
在SpringCloud Gateway中内置了很多不同类型的网关路由过滤器。具体如下:
过滤器工厂 | 作用 | 参数 |
---|---|---|
AddRequestHeader | 为原始请求添加Header | Header的名称及值 |
AddRequestParameter | 为原始请求添加请求参数 | 参数名称及值 |
AddResponseHeader | 为原始响应添加Header | Header的名称及值 |
DedupeResponseHeader | 剔除响应头中重复的值 | 需要去重的Header名称及去重策略 |
Hystrix | 为路由引入Hystrix的断路器保护 | HystrixCommand的名称 |
FallbackHeaders | 为fallbackUri的请求头中添加具体的异常信息 | Header的名称 |
PrefixPath | 为请求添加一个preserveHostHeader=true的属性,路由过滤器会检查该属性以决定是否要发送原始的Host | 无 |
RequestRateLimiter | 用于对请求限流,限流算法为令牌桶 | keyResolver、rateLimiter、statusCode、denyEmptyKey、emptyKeyStatus |
RedirectTo | 将原始请求重定向到指定的URL | http状态码及重定向的 url |
RemoveHopByHopHeadersFilter | 为原始请求删除IETF组织规定的一系列Header | 默认就会启用,可以通 过配置指定仅删除哪些 Header |
RemoveResponseHeader | 为原始请求删除某个Header | Header名称 |
RewritePath | 重写原始的请求路径 | 原始路径正则表达式以 及重写后路径的正则表达式 |
RewriteResponseHeader | 重写原始响应中的某个Header | Header名称,值的正则表达式,重写后的值 |
SaveSession | 在转发请求之前,强制执行WebSession::save 操作 | 无 |
secureHeaders | 为原始响应添加一系列起安全作用的响应头 | 无,支持修改这些安全 响应头的值 |
SetPath | 修改原始的请求路径 | 修改后的路径 |
SetResponseHeader | 修改原始响应中某个Header的值 | Header名称,修改后的值 |
过滤器工厂 | 作用 | 参数 |
---|---|---|
SetStatus | 修改原始响应的状态码 | HTTP 状态码,可以是数字,也可以是字符串 |
StripPrefix | 用于截断原始请求的路径 | 使用数字表示要截断的 路径的数量 |
Retry | 针对不同的响应进行重试 | retries、statuses、methods、series |
RequestSize | 设置允许接收最大请求包的大 小。如果请求包大小超过设置的值 , 则 返 回 413 Payload Too Large | 请求包大小,单位为字 节,默认值为5M |
ModifyRequestBody | 在转发请求之前修改原始请求体内容 | 修改后的请求体内容 |
ModifyResponseBody | 修改原始响应体的内容 | 修改后的响应体内容 |
内置局部过滤器的使用
server:
port: 7000
spring:
application:
name: api-gateway
cloud:
nacos:
discovery:
server-addr:
localhost:8848
gateway:
discovery:
locator:
enabled: true
routes:
- id: product_route
uri: lb://service-product
order: 1
predicates:
- Path=/product-serv/**
filters:
- StripPrefix=1
- SetStatus=2000 # 修改返回状态
自定义局部过滤
第1步:在配置文件中,添加一个Log的过滤器配置
server:
port: 7000
spring:
application:
name: api-gateway
cloud:
nacos:
discovery:
server-addr:
localhost:8848
gateway:
discovery:
locator:
enabled: true
routes:
- id: consumer
order: -1
uri: lb://consumer
predicates:
- Path=/consumer-serv/**
filters:
- StripPrefix=1
- Log=true,false # 控制日志是否开启
第2步:自定义一个过滤器工厂,实现方法
//自定义局部过滤器@Component
import org.springframework.web.server.ServerWebExchange;
import java.util.Arrays;
public class LogGatewayFilterFactory
extends AbstractGatewayFilterFactory<LogGatewayFilterFactory.Config> {
//构造函数
public LogGatewayFilterFactory() {
super(LogGatewayFilterFactory.Config.class);
}
//读取配置文件中的参数 赋值到 配置类中
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("consoleLog", "cacheLog");
}
//过滤器逻辑
@Override
public GatewayFilter apply(LogGatewayFilterFactory.Config config) {
return new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange,
GatewayFilterChain chain) {
if (config.isCacheLog()) {
System.out.println("cacheLog已经开启了");
}
if (config.isConsoleLog()) {
System.out.println("consoleLog已经开启了 ");
}
return chain.filter(exchange);
}
};
}
//配置类 接收配置参数
@Data
@NoArgsConstructor
public static class Config {
private boolean consoleLog;
private boolean cacheLog;
}
第3步:启动测试
全局过滤器
全局过滤器作用于所有路由, 无需配置。通过全局过滤器可以实现对权限的统一校验,安全性验证等功能。
内置全局过滤器
SpringCloud Gateway内部也是通过一系列的内置全局过滤器对整个路由转发进行处理如下:
自定义全局过滤器
内置的过滤器已经可以完成大部分的功能,但是对于企业开发的一些业务功能处理,还是需要我们自己 编写过滤器来实现的,那么我们一起通过代码的形式自定义一个过滤器,去完成统一的权限校验。
开发中的鉴权逻辑:
当客户端第一次请求服务时,服务端对用户进行信息认证(登录)
认证通过,将用户信息进行加密形成token,返回给客户端,作为登录凭证 以后每次请求,客户端都携带认证的token
服务端对token进行解密,判断是否有效。
如上图,对于验证用户是否已经登录鉴权的过程可以在网关统一检验。检验的标准就是请求中是否携带token凭证以及token的正确性。
下面的我们自定义一个GlobalFilter,去校验所有请求的请求参数中是否包含“token”,如何不包含请求 参数“token”则不转发路由,否则执行正常的逻辑。
package com.naixue.filters;
import io.micrometer.core.instrument.util.StringUtils;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
//自定义全局过滤器需要实现GlobalFilter和Ordered接口
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {
//完成判断逻辑
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String token = exchange.getRequest().getQueryParams().getFirst("token");
if (StringUtils.isBlank(token)) {
System.out.println("鉴权失败");
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
//调用chain.filter继续向下游执行
return chain.filter(exchange);
}
//顺序,数值越小,优先级越高
@Override
public int getOrder() {
return 0;
}
}
网关限流
官方指南:https://github.com/alibaba/Sentinel/wiki/%E7%BD%91%E5%85%B3%E9%99%90%E6%B5%81
网关是所有请求的公共入口,所以可以在网关进行限流,而且限流的方式也很多,我们本次采用前面学 过的Sentinel组件来实现网关的限流。Sentinel支持对SpringCloudGateway、Zuul等主流网关进行限流。
从1.6.0版本开始,Sentinel提供了SpringCloud Gateway的适配模块,可以提供两种资源维度的限流: route维度:即在Spring配置文件中配置的路由条目,资源名为对应的routeId
自定义API维度:用户可以利用Sentinel提供的API来自定义一些API分组
导入依赖
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-spring-cloud-gateway-adapter</artifactId> </dependency>
编写配置类
基于Sentinel 的Gateway限流是通过其提供的Filter来完成的,使用时只需注入对应的
SentinelGatewayFilter实例以及 SentinelGatewayBlockExceptionHandler 实例即可。
/**
* @author kojon
* @date 2020/4/17
*/
@Configuration
public class SCGConfiguration {
private final List<ViewResolver> viewResolvers;
private final ServerCodecConfigurer serverCodecConfigurer;
public SCGConfiguration(ObjectProvider<List<ViewResolver>>
viewResolversProvider,
ServerCodecConfigurer serverCodecConfigurer) {
this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
this.serverCodecConfigurer = serverCodecConfigurer;
}
/**
* 初始化一个限流的过滤器
*
* @return
*/
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public GlobalFilter sentinelGatewayFilter() {
return new SentinelGatewayFilter();
}
/**
* 配置限流的异常处理器
**/
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
return new SentinelGatewayBlockExceptionHandler(viewResolvers,
serverCodecConfigurer);
}
@PostConstruct
public void doInit() {
initCustomizedApis();
initGatewayRules();
initBlockHandlers();
}
private void initCustomizedApis() {
Set<ApiDefinition> definitions = new HashSet<>();
ApiDefinition api = new ApiDefinition("gateway-route")
.setPredicateItems(new HashSet<ApiPredicateItem>() {{
//分组定义
add(new ApiPathPredicateItem().setPattern("/group/**")
.setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
add(new ApiPathPredicateItem().setPattern("/discovery/**")
.setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
}});
definitions.add(api);
GatewayApiDefinitionManager.loadApiDefinitions(definitions);
}
/**
* 配置初始化的限流参数
*/
public void initGatewayRules() {
Set<GatewayFlowRule> rules = new HashSet<>();
//资源名称,对应路由id
rules.add(new GatewayFlowRule("gateway-route")
// 限流阈值
.setCount(10)
// 统计时间窗口,单位是秒,默认是 1 秒
.setIntervalSec(1)
);
GatewayRuleManager.loadRules(rules);
}
/**
* 自定义限流异常页面
*/
public void initBlockHandlers() {
BlockRequestHandler blockRequestHandler = (serverWebExchange, throwable)
-> ServerResponse.status(HttpStatus.TOO_MANY_REQUESTS).contentType(MediaType.APPLICATION_JSON).
// body(BodyInserters.fromValue(HttpStatus.TOO_MANY_REQUESTS));
body(BodyInserters.fromValue("GatewayApi current limiting"));
GatewayCallbackManager.setBlockHandler(blockRequestHandler);
}
}
测试
在一秒钟内多次访问http://localhost:7000/group/路径下的接口就可以看到限流启作用了。