【Springcloud】(08)路由网关Zuul和GateWay
【一】简单说明
微服务技术必须要有网关,让网关挡在服务的前面,进行一些日志、限流、权限、安全架构等功能,好比是医院的挂号台,给病人分配医生。
Zuul是旧的网关,GateWay是新的网关。
【二】GateWay是什么
【1】启示
不要只把重心放在编写代码上,现在更多的是进行配置,要学习的是知识体系和技术选型。
【2】GateWay厉害之处
SpringCloud2.0以上版本中,没有对新版本的Zuul2.0以上最新高性能版本进行集成,仍然还是使用Zuul1.x非reactor模式的老版本。而为了提升网关的性能,SpringCloud Gateway是基于webflux框架实现的,而webFlux框架底层则使用了高性能的reactor模式通信框架Netty。
GateWay的目标是提供统一的路由方式,且基于filter链的方式提供了网关基本的工程,旨在提供一种简单而有效的方式来对API进行路由,以及提供了一些强大的过滤器功能,例如:安全,监控/指标、熔断、限流、重试等。在高并发下,GateWay是基于异步非阻塞模型上进行开发的,就很有优势。
【3】GateWay能干什么
路由转发(反向代理),执行过滤器链,鉴权,流量控制,熔断,日志监控
网关,旨在为微服务架构提供一种简单有效的统一的API路由管理方式。同时,基于Filter链的方式提供了网关的基本功能,比如:鉴权、流量控制、熔断、路径重写、黑白名单、日志监控等。
基本功能如下:
(1)统一入口:暴露出网关地址,作为请求唯一入口,隔离内部微服务,保障了后台服务的安全性
(2)鉴权校验:识别每个请求的权限,拒绝不符合要求的请求
(3)动态路由:能够匹配任何请求属性,动态的将请求路由到不同的后端集群中
【4】GateWay的特性
(1)特性
1-基于spring framework5,project reactor和spring boot2.0进行构建,使用非阻塞API
2-动态路由:能够匹配任何请求属性
3-可以对路由指定Predicate(断言)和Filter(过滤器)
4-集成Hystrix的断路器功能
5-集成SpringCloud服务发现功能
6-易于编写的Predicate(断言)和Filter(过滤器)
7-请求限流功能
8-支持路径重写
(2)三个重要的概念
(1)路由Route
路由是构建网关的基本模块,由ID,目标URI,一系列的断言和过滤器组成,如果断言为true则匹配该路由
(2)断言Predicate
参考的是Java8的java.util.function.Predicate开发人员可以匹配HTTP请求中的所有内容(例如请求头或请求参数),如果请求与断言相匹配则进行路由
(3)过滤Filter
指的是Spring框架中GateWayFilter的实例,使用过滤器,可以在请求被路由前或者之后对请求进行修改
【5】微服务架构中网关在哪里?
【6】GateWay和Zuul有什么区别?为什么选择GateWay?
(1)Zuul1模型的特性介绍
(2)GateWay模型的特性
(3)总结区别
1-GateWay是SpringCloud团队研发出来的,
很多功能是Zuul没有的,GateWay用来非常的简单便捷
2-GateWay是基于异步非阻塞模型上进行开发的,性能方面不需要担心
多方面综合考虑GateWay是很理想的网关选择
【三】三大核心概念配置
【1】路由
(1)简单案例模板
spring:
cloud:
gateway:
routes:
- id: manager # 路由唯一标识
uri: lb://manager_server # 路由指向目的地URL或服务名,客户端请求最终被转发到的微服务
predicates:
- Path=/manager/** # 断言:以manager开头的请求都负载到manager_server服务
filters:
- RewritePath=/manager/(?<segment>.*), /$\{segment} # 过滤器:过滤掉url里的manager,例如http://ip:port/manager/test -> http://ip:port/test
order: 5 # 用于多个Route之间的排序,数值越小越靠前,匹配优先级越高
(2)路由配置(URI)
在spring cloud gateway中配置uri有三种方式,包括
现在断言的主要方式就是匹配请求路径
(1)websocket配置方式
spring:
cloud:
gateway:
routes:
- id: ruoyi-api
uri: ws://localhost:9090/
predicates:
- Path=/api/**
(2)http地址配置方式
spring:
application:
name: ruoyi-gateway
cloud:
gateway:
routes:
- id: ruoyi-system
uri: http://localhost:9201/
predicates:
- Path=/system/**
(3)注册中心配置方式
spring:
cloud:
gateway:
routes:
- id: ruoyi-api
uri: lb://ruoyi-api
predicates:
- Path=/api/**
【2】断言
(1)简单案例模板
spring:
cloud:
gateway:
routes:
- id: manager # 路由唯一标识
uri: https://manager_server
predicates:
- After=2017-01-20T17:42:47.789-07:00[America/Denver] # 时间点后匹配
- Before=2017-01-20T17:42:47.789-07:00[America/Denver] # 时间点前匹配
- Between=2017-01-20T17:42:47.789-07:00[America/Denver],2017-01-21T17:42:47.789-07:00[America/Denver] # 时间区间匹配
- Cookie=chocolate, ch.p # 指定cookie正则匹配
- Header=X-Request-Id, \d+ # 指定Header正则匹配
- Host=**.somehost.org,**.anotherhost.org # 请求Host匹配
- Method=GET,POST # 请求Method匹配指定请求方式
- Path=/red/{segment},/blue/{segment} # 请求路径正则匹配
- Query=green # 请求包含某参数
- Query=red, gree. # 请求包含某参数并且参数值匹配正则表达式(匹配red;green,greet,gree...)
- RemoteAddr=192.168.1.1/24 # 远程地址匹配
# 设置分组和权重,按照路由权重选择同一个分组中的路由
- id: preManager1 # 路由唯一标识
uri: https://preManager1
predicates:
- Weight=group1, 2
- id: preManager2 # 路由唯一标识
uri: https://preManager2
predicates:
- Weight=group1, 8
(2)路由规则(断言predicates)
(1)Datetime
匹配日期时间之后发生的请求
spring:
application:
name: ruoyi-gateway
cloud:
gateway:
routes:
- id: ruoyi-system
uri: http://localhost:9201/
predicates:
- After=2021-02-23T14:20:00.000+08:00[Asia/Shanghai]
(2)Cookie
匹配指定名称且其值与正则表达式匹配的cookie
spring:
application:
name: ruoyi-gateway
cloud:
gateway:
routes:
- id: ruoyi-system
uri: http://localhost:9201/
predicates:
- Cookie=loginname, ruoyi
测试 curl http://localhost:8080/system/config/1 --cookie “loginname=ruoyi”
就会跳转到:http://localhost:9201/
(3)Header
匹配具有指定名称的请求头,\d+值匹配正则表达式
spring:
application:
name: ruoyi-gateway
cloud:
gateway:
routes:
- id: ruoyi-system
uri: http://localhost:9201/
predicates:
- Header=X-Request-Id, \d+
(4)Host
匹配主机名的列表
spring:
application:
name: ruoyi-gateway
cloud:
gateway:
routes:
- id: ruoyi-system
uri: http://localhost:9201/
predicates:
- Host=**.somehost.org,**.anotherhost.org
(5)Method
匹配请求methods的参数,它是一个或多个参数
spring:
application:
name: ruoyi-gateway
cloud:
gateway:
routes:
- id: ruoyi-system
uri: http://localhost:9201/
predicates:
- Method=GET,POST
(6)Path
匹配请求路径
spring:
application:
name: ruoyi-gateway
cloud:
gateway:
routes:
- id: ruoyi-system
uri: http://localhost:9201/
predicates:
- Path=/system/**
(7)Query
匹配查询参数
spring:
application:
name: ruoyi-gateway
cloud:
gateway:
routes:
- id: ruoyi-system
uri: http://localhost:9201/
predicates:
- Query=username, abc.
(8)RemoteAddr
匹配IP地址和子网掩码
spring:
application:
name: ruoyi-gateway
cloud:
gateway:
routes:
- id: ruoyi-system
uri: http://localhost:9201/
predicates:
- RemoteAddr=192.168.10.1/0
(9)Weight
匹配权重
spring:
application:
name: ruoyi-gateway
cloud:
gateway:
routes:
- id: ruoyi-system-a
uri: http://localhost:9201/
predicates:
- Weight=group1, 8
- id: ruoyi-system-b
uri: http://localhost:9201/
predicates:
- Weight=group1, 2
【3】过滤器
(1)过滤器介绍
(1)按生命周期分类
- 前置(pre)过滤器: 在请求被路由之前调用:在chain.filter(exchange)前编写过滤器逻辑
- 后置(post)过滤器: 在路由到微服务之后调用:通过chain.filter(exchange).then(Mono.fromRunnable(() -> {过滤器逻辑})实现
(2)按类型分类
- 局部(GatewayFilter)过滤器:作用在某一个路由上,使用时需要关联指定的路由
- 全局(GlobalFilter)过滤器:作用在所有路由上,不需要在配置文件中配置
(2)内置局部过滤器与使用
1-各种内置局部过滤器
spring:
cloud:
gateway:
routes:
- id: gateway_filter
uri: https://example.org
predicates:
- Path=/red/{segment}
filters:
# 1、为原始请求添加Header。headerName:X-Request-red,headerValue:blue。
- AddRequestHeader=X-Request-red, blue
- AddRequestHeadersIfNotPresent=X-Request-Color-1:blue,X-Request-Color-2:green
# 2、为原始请求添加参数。参数名,参数值
- AddRequestParameter=red, blue
# 3、为原始响应添加Header
- AddResponseHeader=X-Response-Red, Blue
# 4、剔除响应头中重复的值
- DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin
# 5、为原始请求路径添加前缀
- PrefixPath=/mypath
# 6、配置该过滤器后,会原始请求的host头信息,并原封不动的转发出去,而不是被gateway的http客户端重置。
- PreserveHostHeader
# 7、将原始请求重定向到指定的URL,参数为http状态码及重定向的url
- RedirectTo=302, https://acme.org
# 8、移除响应Body中的指定key
- RemoveJsonAttributesResponseBody=id,color
# 9、移除原始请求中的指定Header
- RemoveRequestHeader=X-Request-Foo
# 10、移除原始请求中的指定参数
- RemoveRequestParameter=red
# 11、移除响应中的指定Header
- RemoveResponseHeader=X-Response-Foo
2-限流规则,根据URI限流
(1)添加依赖
<!-- spring data redis reactive 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
(2)修改yml
spring:
cloud:
gateway:
routes:
- id: gateway_filter
uri: https://example.org
predicates:
- Path=/red/{segment}
filters:
# 12、请求限流,限流算法为令牌桶,以下示例为根据用户id做限流
# @Configuration
# public class RateLimiterConfig {
# @Bean
# public KeyResolver userKeyResolver() {
# return exchange -> Mono.just(Objects.requireNonNull(exchange.getRequest().getQueryParams().getFirst("userId")));
# }
# }
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 10 # 允许用户每秒处理的请求数
redis-rate-limiter.burstCapacity: 20 # 令牌桶的容量,即允许在 1 秒内完成的最大请求数。设置为 0 则表示拒绝所有请求。
key-resolver: "#{@userKeyResolver}" # 一个引用名为 userKeyResolver 的 bean 的 SpEL 表达式
# 13、重写原始的请求路径
- RewritePath=/red/?(?<segment>.*), /$\{segment}
# 14、重写响应中的某个Header
- RewriteResponseHeader=X-Response-Red, , password=[^&]+, password=***
# 15、在转发请求之前,强制执行websession::save操作,保存会话状态
- SaveSession
# 16、修改原始的请求路径
- SetPath=/{segment}
# 17、修改原始请求中的指定Header值
- SetRequestHeader=X-Request-Red, Blue
# 18、修改原始响应中的指定Header值
- SetResponseHeader=X-Response-Red, Blue
# 19、修改原始响应的响应码
- SetStatus=401
# 20、剥离原始请求路径
- StripPrefix=2
# 21、请求重试
- name: Retry
args:
retries: 3 # 重试次数
statuses: BAD_GATEWAY # 应被重试的 HTTP Status Codes
methods: GET,POST # 应被重试的 HTTP Methods
backoff: # 为重试配置指数级的 backoff。重试时间间隔的计算公式为 firstBackoff * (factor ^ n),n 是重试的次数;如果设置了 maxBackoff,最大的 backoff 限制为 maxBackoff. 如果 basedOnPreviousValue 设置为 true, backoff 计算公式为 prevBackoff * factor.
firstBackoff: 10ms
maxBackoff: 50ms
factor: 2
basedOnPreviousValue: false
# 22、设置允许接收最大请求包的大小。如果请求包大小超过设置的值,则返413Payload Too Large
- name: RequestSize
args:
maxSize: 5000000
(3)编写URI限流规则配置类
/**
* 限流规则配置类
*/
@Configuration
public class KeyResolverConfiguration
{
@Bean
public KeyResolver pathKeyResolver()
{
return exchange -> Mono.just(exchange.getRequest().getURI().getPath());
}
}
(4)其他限流规则
1-参数限流:key-resolver: “#{@parameterKeyResolver}”
@Bean
public KeyResolver parameterKeyResolver()
{
return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("userId"));
}
2-IP限流:key-resolver: “#{@ipKeyResolver}”
@Bean
public KeyResolver ipKeyResolver()
{
return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
}
(3)内置全局过滤器
(1)GatewayMetricsFilter(0):统计一些网关的性能指标
(2)RouteToRequestUrlFilter(10000):把浏览器的URL请求的Path路径添加到路由的URI之中。
(3)NettyRoutingFilter(2147483647):通过HttpClient客户端转发真实的URL,并存储返回的结果。
(4)NettyWriteResponseFilter(-1):在所有的其它的过滤器执行完成之后运行,将响应的数据发送给网关的客户端。
(5)ForwardRoutingFilter(2147483647):转发路由过滤器,若URI是forward模式,过滤器会将请求转发到DispatcherHandler来处理请求。
(6)ForwardPathFilter(0):解析路径,并将路径转发。
(7)LoadBalancerClientFilter(10100):负载均衡,解析服务名,获取真实服务地址。
(8)RemoveCachedBodyFilter(-2147483648):清除网关上下文中的缓存的请求Body。
(9)WebsocketRoutingFilter(2147483646):如果请求中的ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR 属性对应的URL前缀为 ws 或 wss,会使用Spring Web Socket 模块转发WebSocket请求。WebSockets可以使用路由进行负载均衡。
(10)AdaptCachedBodyGlobalFilter(-2147482648):从请求中获取body缓存到网关上下文。
(4)自定义局部过滤器
局部过滤器需要在指定路由配置才能生效,默认是不生效的。注册局部过滤器与全局不同的是需要继承AbstractGatewayFilterFactory接口。
1-黑名单校验过滤器
顾名思义,就是不能访问的地址。实现自定义过滤器BlackListUrlFilter,需要配置黑名单地址列表blacklistUrl,当然有其他需求也可以实现自定义规则的过滤器。
spring:
cloud:
gateway:
routes:
# 系统模块
- id: ruoyi-system
uri: lb://ruoyi-system
predicates:
- Path=/system/**
filters:
- StripPrefix=1
- name: BlackListUrlFilter # 指定过滤器为BlackListUrlFilter
args:
blacklistUrl: # 黑名单列表作为参数
- /user/list
/**
* 黑名单过滤器
*
* @author ruoyi
*/
@Component
public class BlackListUrlFilter extends AbstractGatewayFilterFactory<BlackListUrlFilter.Config>
{
@Override
public GatewayFilter apply(Config config)
{
return (exchange, chain) -> {
String url = exchange.getRequest().getURI().getPath();
if (config.matchBlacklist(url))
{
return ServletUtils.webFluxResponseWriter(exchange.getResponse(), "请求地址不允许访问");
}
return chain.filter(exchange);
};
}
public BlackListUrlFilter()
{
super(Config.class);
}
public static class Config
{
private List<String> blacklistUrl;
private List<Pattern> blacklistUrlPattern = new ArrayList<>();
public boolean matchBlacklist(String url)
{
return !blacklistUrlPattern.isEmpty() && blacklistUrlPattern.stream().anyMatch(p -> p.matcher(url).find());
}
public List<String> getBlacklistUrl()
{
return blacklistUrl;
}
public void setBlacklistUrl(List<String> blacklistUrl)
{
this.blacklistUrl = blacklistUrl;
this.blacklistUrlPattern.clear();
this.blacklistUrl.forEach(url -> {
this.blacklistUrlPattern.add(Pattern.compile(url.replaceAll("\\*\\*", "(.*?)"), Pattern.CASE_INSENSITIVE));
});
}
}
}
(5)自定义全局过滤器
全局过滤器作用于所有的路由,不需要单独配置,我们可以用它来实现很多统一化处理的业务需求,比如权限认证,IP访问限制等等。
创建自定义全局过滤器类 ,实现GlobalFilter和Ordered两个接口。
(1)GlobalFilter:全局过滤拦截器
(2)Ordered:拦截器的顺序,数字越低,优先级越高
1-黑名单校验过滤器
/**
* 定义全局过滤器,会对所有路由生效
*/
@Slf4j
@Component // 让容器扫描到,等同于注册了
public class BlackListFilter implements GlobalFilter, Ordered {
// 模拟黑名单(实际可以去数据库或者redis中查询)
private static List<String> blackList = new ArrayList<>();
static {
blackList.add("0:0:0:0:0:0:0:1"); // 模拟本机地址
}
/**
* 过滤器核心方法
* @param exchange 封装了request和response对象的上下文
* @param chain 网关过滤器链(包含全局过滤器和单路由过滤器)
* @return
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 思路:获取客户端ip,判断是否在黑名单中,在的话就拒绝访问,不在的话就放行
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
// 从request对象中获取客户端ip
String clientIp = request.getRemoteAddress().getHostString();
// 拿着clientIp去黑名单中查询,存在的话就决绝访问
if(blackList.contains(clientIp)) {
// 拒绝访问,返回
response.setStatusCode(HttpStatus.UNAUTHORIZED); // 状态码
log.debug("=====>IP:" + clientIp + " 在⿊名单中,将被拒绝访问!");
String data = "Request be denied!";
DataBuffer wrap = response.bufferFactory().wrap(data.getBytes());
return response.writeWith(Mono.just(wrap));
}
// 合法请求,放行,执行后续的过滤器
return chain.filter(exchange);
}
/**
* @return 过滤器的顺序(优先级),数值越小,优先级越高
*/
@Override
public int getOrder() {
return 0;
}
}
2-跨域脚本过滤器
@Component
@ConditionalOnProperty(value = "security.xss.enabled", havingValue = "true")
public class XssFilter implements GlobalFilter, Ordered
{
// 跨站脚本的 xss 配置,nacos自行添加
@Autowired
private XssProperties xss;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain)
{
ServerHttpRequest request = exchange.getRequest();
// xss开关未开启 或 通过nacos关闭,不过滤
if (!xss.getEnabled())
{
return chain.filter(exchange);
}
// GET DELETE 不过滤
HttpMethod method = request.getMethod();
if (method == null || method == HttpMethod.GET || method == HttpMethod.DELETE)
{
return chain.filter(exchange);
}
// 非json类型,不过滤
if (!isJsonRequest(exchange))
{
return chain.filter(exchange);
}
// excludeUrls 不过滤
String url = request.getURI().getPath();
if (StringUtils.matches(url, xss.getExcludeUrls()))
{
return chain.filter(exchange);
}
ServerHttpRequestDecorator httpRequestDecorator = requestDecorator(exchange);
return chain.filter(exchange.mutate().request(httpRequestDecorator).build());
}
private ServerHttpRequestDecorator requestDecorator(ServerWebExchange exchange)
{
ServerHttpRequestDecorator serverHttpRequestDecorator = new ServerHttpRequestDecorator(exchange.getRequest())
{
@Override
public Flux<DataBuffer> getBody()
{
Flux<DataBuffer> body = super.getBody();
return body.buffer().map(dataBuffers -> {
DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory();
DataBuffer join = dataBufferFactory.join(dataBuffers);
byte[] content = new byte[join.readableByteCount()];
join.read(content);
DataBufferUtils.release(join);
String bodyStr = new String(content, StandardCharsets.UTF_8);
// 防xss攻击过滤
bodyStr = EscapeUtil.clean(bodyStr);
// 转成字节
byte[] bytes = bodyStr.getBytes();
NettyDataBufferFactory nettyDataBufferFactory = new NettyDataBufferFactory(ByteBufAllocator.DEFAULT);
DataBuffer buffer = nettyDataBufferFactory.allocateBuffer(bytes.length);
buffer.write(bytes);
return buffer;
});
}
@Override
public HttpHeaders getHeaders()
{
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.putAll(super.getHeaders());
// 由于修改了请求体的body,导致content-length长度不确定,因此需要删除原先的content-length
httpHeaders.remove(HttpHeaders.CONTENT_LENGTH);
httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked");
return httpHeaders;
}
};
return serverHttpRequestDecorator;
}
/**
* 是否是Json请求
*
* @param exchange HTTP请求
*/
public boolean isJsonRequest(ServerWebExchange exchange)
{
String header = exchange.getRequest().getHeaders().getFirst(HttpHeaders.CONTENT_TYPE);
return StringUtils.startsWithIgnoreCase(header, MediaType.APPLICATION_JSON_VALUE);
}
@Override
public int getOrder()
{
return -100;
}
}
【四】Gateway整合Hystrix实现熔断降级
(1)添加pom依赖,使用的是hystrix
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
(2)修改yml配置,配置需要熔断降级服务
指定过滤器为Hystrix
spring:
redis:
host: localhost
port: 6379
password:
cloud:
gateway:
routes:
# 系统模块
- id: ruoyi-system
uri: lb://ruoyi-system
predicates:
- Path=/system/**
filters:
- StripPrefix=1
# 降级配置
- name: Hystrix
args:
name: default
# 降级接口的地址
fallbackUri: 'forward:/fallback'
上面配置包含了一个Hystrix过滤器,该过滤器会应用Hystrix熔断与降级,会将请求包装成名为fallback的路由指令RouteHystrixCommand,RouteHystrixCommand继承于HystrixObservableCommand,其内包含了Hystrix的断路、资源隔离、降级等诸多断路器核心功能,当网关转发的请求出现问题时,网关能对其进行快速失败,执行特定的失败逻辑,保护网关安全。
配置中有一个可选参数fallbackUri,当前只支持forward模式的URI。如果服务被降级,请求会被转发到该URI对应的控制器。控制器可以是自定义的fallback接口;也可以使自定义的Handler,需要实现接口org.springframework.web.reactive.function.server.HandlerFunction。
(3)实现添加熔断降级处理返回信息
/**
* 熔断降级处理
*
* @author ruoyi
*/
@Component
public class HystrixFallbackHandler implements HandlerFunction<ServerResponse>
{
private static final Logger log = LoggerFactory.getLogger(HystrixFallbackHandler.class);
@Override
public Mono<ServerResponse> handle(ServerRequest serverRequest)
{
Optional<Object> originalUris = serverRequest.attribute(GATEWAY_ORIGINAL_REQUEST_URL_ATTR);
originalUris.ifPresent(originalUri -> log.error("网关执行请求:{}失败,hystrix服务降级处理", originalUri));
return ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR.value()).contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(JSON.toJSONString(R.fail("服务已被降级熔断"))));
}
}
(4)路由配置信息加一个控制器方法用于处理重定向的/fallback请求
/**
* 路由配置信息
*
* @author ruoyi
*/
@Configuration
public class RouterFunctionConfiguration
{
@Autowired
private HystrixFallbackHandler hystrixFallbackHandler;
@Autowired
private ValidateCodeHandler validateCodeHandler;
@SuppressWarnings("rawtypes")
@Bean
public RouterFunction routerFunction()
{
return RouterFunctions
.route(RequestPredicates.path("/fallback").and(RequestPredicates.accept(MediaType.TEXT_PLAIN)),
hystrixFallbackHandler)
.andRoute(RequestPredicates.GET("/code").and(RequestPredicates.accept(MediaType.TEXT_PLAIN)),
validateCodeHandler);
}
}
(5)测试服务熔断降级
启动网关服务RuoYiGatewayApplication.java,访问/system/**在进行测试,会发现返回服务已被降级熔断,表示降级成功。
【五】Gateway整合Sentinel实现限流
Sentinel 支持对 Spring Cloud Gateway、Netflix Zuul 等主流的 API Gateway 进行限流。
(1)添加依赖
<!-- SpringCloud Alibaba Sentinel -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!-- SpringCloud Alibaba Sentinel Gateway -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
</dependency>
(2)限流规则配置类
/**
* 网关限流配置
*
* @author ruoyi
*/
@Configuration
public class GatewayConfig
{
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SentinelFallbackHandler sentinelGatewayExceptionHandler()
{
return new SentinelFallbackHandler();
}
@Bean
@Order(-1)
public GlobalFilter sentinelGatewayFilter()
{
return new SentinelGatewayFilter();
}
@PostConstruct
public void doInit()
{
// 加载网关限流规则
initGatewayRules();
}
/**
* 网关限流规则
*/
private void initGatewayRules()
{
Set<GatewayFlowRule> rules = new HashSet<>();
rules.add(new GatewayFlowRule("ruoyi-system")
.setCount(3) // 限流阈值
.setIntervalSec(60)); // 统计时间窗口,单位是秒,默认是 1 秒
// 加载网关限流规则
GatewayRuleManager.loadRules(rules);
}
}
测试验证,一分钟内访问三次系统服务出现异常提示表示限流成功。
【四】GateWay非阻塞异步模型
【五】GateWay工作流程
【1】组成部分
1-Route路由:是构建网关的基本模块,由ID、目标URI,一系列的断言和过滤器组成,如果断言为true则匹配该路由
2-Predicate断言:参考的是Java8的Predicate,开发人员可以匹配Http请求中的所有内容,例如请求头或者请求参数,如果请求与断言相匹配则进行路由
3-Filter过滤:指的是spring框架中GateWayFilter的实例,使用过滤器,可以在请求被路由前或者之后队请求进行修改
【2】GateWay工作流程
客户端向GateWay发出web请求,通过一些匹配条件,定位到真正的服务节点,在GateWay Handler Mapping中找到与请求相匹配的路由,把请求发送到Handler ,并且在这个转发过程的前后,进行一些精细化控制。predicate就是我们的匹配条件;而Filter,就可以理解为一个无所不能的拦截器,有了这两个元素,再加上目标uri,就可以实现一个具体的路由了。
Handler再通过指定的过滤器链来吧请求发送到我们实际的服务执行业务逻辑,然后返回。过滤器之间用虚线分开是因为过滤器可能会在发送代理请求之前(pre)或之后(post)执行业务逻辑
Filter在pre类型的过滤器可以做参数校验、权限校验、流量监控、日志输出、协议转换等。在post类型的过滤器中可以做响应内容、响应头的修改,日志的输出、流量监控等,有着非常重要的作用。
【六】Gateway整合Nacos
【五】GateWay9527搭建过程(整合Eureka)
【1】第一步:创建子模块cloud-gateway-gateway9527,并且修改pom
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--加入熔断器的依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<!--表示把这个模块注册到注册中心里去-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
<version>2.0.0.RELEASE</version>
</dependency>
<!--引入自己定义的API通用包,可以使用Payment支付的Entity-->
<dependency>
<groupId>com.atguigu.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<!--devtools-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
</dependencies>
【2】第二步:修改网关服务的yml文件(将Gateway注册进Eureka)
server:
port: 9527
spring:
application:
name: cloud-gateway #把这个微服务注册进7001
eureka:
instance:
hostname: cloud-gateway-service #微服务在注册中心的名称为cloud-gateway-service
client:
service-url:
register-with-eureka: true
fetch-registry: true
defaultZone: http://eureka7001.com:7001/eureka
【3】第三步:没有业务类,添加启动类
@SpringBootApplication
@EnableEurekaClient
public class GateWayMain9527 {
public static void main(String[] args) {
SpringApplication.run(GateWayMain9527.class,args);
}
}
【4】第四步:YML新增网关配置内容
server:
port: 9527
spring:
application:
name: cloud-gateway #把这个微服务注册进7001
# start
cloud:
gateway:
routes:
- id: payment_routh #路由的id,没有固定规则但要求唯一,建议配合服务器
uri: http://localhost:8001 #匹配后提供服务的路由地址
predicates:
- Path=/payment/get/** #断言,路径相匹配的进行路由
- id: payment_routh2 #路由的id,没有固定规则但要求唯一,建议配合服务器
uri: http://localhost:8001 #匹配后提供服务的路由地址
predicates:
- Path=/payment/lb/** #断言,路径相匹配的进行路由
eureka:
instance:
hostname: cloud-gateway-service
client:
service-url:
register-with-eureka: true
fetch-registry: true
defaultZone: http://eureka7001.com:7001/eureka
【5】第五步:测试
依次启动7001/8001/9527
先从8001地址测试:http://localhost:8001/payment/get/2
再从9527地址测试:http://localhost:9527/payment/get/2
【6】分析一下
我们不希望请求直接到达服务,所以让网关挡在服务前面,或者说网关把服务包裹起来,web请求先到达网关,由网关分配服务,这样即使有安全问题也会被网关拦截下来。有了网关就可以不用暴露8001端口,
【六】GateWay配置路由的两种方式
【1】两种方式
get和lb
【2】发现问题
每一个服务都要在yml配置,这样最后导致庞大和臃肿,所以考虑另外一种方法就是硬代码配置,在代码中注入RouteLocator的Bean
【3】第一步:添加配置类
@Configuration
public class GateWayConfig {
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder routeLocatorBuilder){
RouteLocatorBuilder.Builder routes=routeLocatorBuilder.routes();
routes.route("path_route_atguigu",
r -> r.path("/guonei")
.uri("http://news.baidu.com/guonei"));
return routes.build();
}
}
【4】第二步:测试
测试地址:http://localhost:9527/guonei
会跳转到:http://news.baidu.com/guonei
【七】GateWay配置路由实现负载均衡
【1】基本介绍
现在使用网关9527,挡在服务前面,对服务进行分配,但是可能会有很多个服务,就需要进行负载均衡,以前是使用Ribbon,现在可以使用网关进行动态路由的配置。默认情况下,Gateway会根据注册中心注册的服务列表,以注册中心上微服务名为路径创建动态路由进行转发,从而实现动态路由的功能
【2】启动eureka7001,服务提供者8001和8002
【3】修改pom
【4】修改yml
server:
port: 9527
spring:
application:
name: cloud-gateway #把这个微服务注册进7001
cloud:
gateway:
discovery:
locator:
enabled: true #开启从注册中心丰台创建路由的功能,利用微服务进行路由
routes:
- id: payment_routh #路由的id,没有固定规则但要求唯一,建议配合服务器
#uri: http://localhost:8001 #匹配后提供服务的路由地址
uri: lb://CLOUD-PAYMENT-SERVICE #匹配后提供服务的路由地址,把写死的地址换成微服务的名称
predicates:
- Path=/payment/get/** #断言,路径相匹配的进行路由
- id: payment_routh2 #路由的id,没有固定规则但要求唯一,建议配合服务器
#uri: http://localhost:8001 #匹配后提供服务的路由地址
uri: lb://CLOUD-PAYMENT-SERVICE #匹配后提供服务的路由地址
predicates:
- Path=/payment/lb/** #断言,路径相匹配的进行路由
eureka:
instance:
hostname: cloud-gateway-service
client:
service-url:
register-with-eureka: true
fetch-registry: true
defaultZone: http://eureka7001.com:7001/eureka
【5】测试
启动9527,测试地址:http://localhost:9527/payment/lb
以前是从80端口访问8001服务的,现在直接从网关9527开始访问
会在8001服务和8002服务之间切换,达到了动态路由的效果
【八】GateWay常用的Predicate
【1】Path 路由断言工厂
Path 路由断言工厂接收一个参数,根据 Path 定义好的规则来判断访问的 URI 是否匹配。
spring:
cloud:
gateway:
routes:
- id: host_route
uri: http://c.biancheng.net
predicates:
- Path=/blog/detail/{segment}
如果请求路径为 /blog/detail/xxx,则此路由将匹配。也可以使用正则,例如 /blog/detail/** 来匹配 /blog/detail/ 开头的多级 URI。
我们访问本地的网关:http://localhost:2001/blog/detail/36185 ,可以看到显示的是 http://c.biancheng.net/blog/detail/36185 对应的内容。
【2】Query 路由断言工厂
Query 路由断言工厂接收两个参数,一个必需的参数和一个可选的正则表达式。
spring:
cloud:
gateway:
routes:
- id: query_route
uri: http://c.biancheng.net
predicates:
- Query=foo, ba.
如果请求包含一个值与 ba 匹配的 foo 查询参数,则此路由将匹配。bar 和 baz 也会匹配,因为第二个参数是正则表达式。
测试链接:http://localhost:2001/?foo=baz。
【3】Method 路由断言工厂
Method 路由断言工厂接收一个参数,即要匹配的 HTTP 方法。
spring:
cloud:
gateway:
routes:
- id: method_route
uri: http://baidu.com
predicates:
- Method=GET
【4】Header 路由断言工厂
Header 路由断言工厂接收两个参数,分别是请求头名称和正则表达式。
spring:
cloud:
gateway:
routes:
- id: header_route
uri: http://example.org
predicates:
- Header=X-Request-Id, \d+
如果请求中带有请求头名为 x-request-id,其值与 \d+ 正则表达式匹配(值为一个或多个数字),则此路由匹配。
如果你想学习更多路由断言工厂的用法,可以参考官方文档进行学习。
【九】GateWay的Filter
【1】简单介绍
使用过滤器,可以在请求被路由前或者之后对请求进行修改
生命周期:pre,post
种类:GatewayFilter,GlobalFilter
常用的GatewayFilter:AddRequestParameter,在yml配置
【2】自定义全局过滤器
自定义过滤器用的更多一点,自定义全局GlobalFilter
(1)两个主要接口:GlobalFilter和Ordered
(2)能干嘛:全局日志记录,统一网关鉴权…
(3)案例代码
@Component
@Slf4j
public class MyLogGateWayFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("***********come in MyLogGateWayFilter: "+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;
}
}
(4)测试
正确地址访问:http://localhost:9527/payment/lb?uname=z3
错误地址访问:http://localhost:9527/payment/lb
前台控制器的日志输出: