为什么需要服务网关:
1、什么是服务网关:
微服务架构中将一个系统拆分成多个微服务,如果没有网关,客户端只能在本地记录每个微服务的调用地址,当需要调用的微服务数量很多时,它需要了解每个服务的接口,这个工作量很大。那有了网关之后,能够起到怎样的改善呢?
网关作为系统的唯一流量入口,封装内部系统的架构,所有请求都先经过网关,由网关将请求路由到合适的微服务,所以,使用网关的好处在于:
- (1)简化客户端的工作。网关将微服务封装起来后,客户端只需同网关交互,而不必调用各个不同服务;
- (2)降低函数间的耦合度。 一旦服务接口修改,只需修改网关的路由策略,不必修改每个调用该函数的客户端,从而减少了程序间的耦合性
- (3)解放开发人员把精力专注于业务逻辑的实现。由网关统一实现服务路由(灰度与ABTest)、负载均衡、访问控制、流控熔断降级等非业务相关功能,而不需要每个服务 API 实现时都去考虑
但是 API 网关也存在不足之处,在微服务这种去中心化的架构中,网关又成了一个中心点或瓶颈点,它增加了一个我们必须开发、部署和维护的高可用组件。正是由于这个原因,在网关设计时必须考虑即使 API 网关宕机也不要影响到服务的调用和运行,所以需要对网关的响应结果有数据缓存能力,通过返回缓存数据或默认数据屏蔽后端服务的失败。
在服务的调用方式上面,网关也有一定的要求,API 网关最好是支持 I/O 异步、同步非阻塞的,如果服务是同步阻塞调用,可以理解为微服务模块之间是没有彻底解耦的,即如果A依赖B提供的API,如果B提供的服务不可用将直接影响到A不可用,除非同步服务调用在API网关层或客户端做了相应的缓存。因此为了彻底解耦,在微服务调用上更建议选择异步方式进行。而对于 API 网关需要通过底层多个细粒度的 API 组合的场景,推荐采用响应式编程模型进行而不是传统的异步回调方法组合代码,其原因除了采用回调方式导致的代码混乱外,还有就是对于 API 组合本身可能存在并行或先后调用,对于采用回调方式往往很难控制。
2、服务网关的基本功能:
网关的核心功能特性:
- 请求路由
- 权限控制
- 限流
架构图:
- 权限控制:网关作为微服务入口,需要校验用户是是否有请求资格,如果没有则进行拦截。
- 路由和负载均衡:一切请求都必须先经过gateway,但网关不处理业务,而是根据某种规则,把请求转发到某个微服务,这个过程叫做路由。当然路由的目标服务有多个时,还需要做负载均衡。(这里的负载均衡与Ribbon的负载均衡不太一样。Gateway的负载均衡是客户端与微服务之间的,而Ribbon的负载均衡是微服务与微服务之间的)
- 限流:当请求流量过高时,在网关中按照下流的微服务能够接受的速度来放行请求,避免服务压力过大。
3、流量网关与服务网关的区别:
流量网关和服务网关在系统整体架构中所处的位置如上图所示:
- 流量网关(如Nignx)是指提供全局性的、与后端业务应用无关的策略,例如 HTTPS证书卸载、Web防火墙、全局流量监控等。
- 微服务网关(如Spring Cloud Gateway)是指与业务紧耦合的、提供单个业务域级别的策略,如服务治理、身份认证等。也就是说,流量网关负责南北向流量调度及安全防护,微服务网关负责东西向流量调度及服务治理。
网关为微服务架构的系统提供简单、有效且统一的API路由管理,作为系统的统一入口,提供内部服务的路由中转,给客户端提供统一的服务,可以实现一些和业务没有耦合的公用逻辑,主要功能包含认证、鉴权、路由转发、安全策略、防刷、流量控制、监控日志等。
二、服务网关的部署:
1、主流网关的对比与选型:
gateway 网关:功能强大丰富,性能好,官方基准测试 RPS (每秒请求数)是Zuul的1.6倍,能与 SpringCloud 生态很好兼容,单从流式编程+支持异步上也足以让开发者选择它了。
是Spring Cloud的一个全新的API网关项目,替换Zuul开发的网关服务,基于Spring5.0 + SpringBoot2.0 + WebFlux(基于性能的Reactor模式响应式通信框架Netty,异步阻塞模型)等技术开发,性能高于Zuul
Zuul 2.x:性能与 gateway 差不多,基于非阻塞的,支持长连接,但 SpringCloud 没有集成 zuul2 的计划,官网中也明确说了不再维护Zuul了, 并且 Netflix 相关组件都宣布进入维护期,前景未知。
采用Netty实现异步非阻塞编程模型,一个CPU一个线程,能够处理所有的请求和响应,请求响应的生命周期通过事件和回调进行处理,减少线程数量,开销较小
Spring Cloud Gateway比 Zuul(1.x) 它有哪些优势呢?
- Zuul(1.x) 基于 Servlet,是一个基于阻塞 I/ O 的 API Gateway,它不支持任何长连接,如WebSockets;每次 I/ O 操作都是从工作线程中选择一个执行,请求线程被阻塞到工作线程完成,性能较差;
Springcloud中所集成的Zuul版本,采用的是Tomcat容器,使用的是传统的Servlet IO处理模型。 - Spring Cloud Gateway 使用非阻塞 API,支持长连接 WebSockets,支持限流等新特性。
- 在性能方面,根据官方提供的基准测试, Spring Cloud Gateway 的 RPS(每秒请求数)是Zuul 的 1. 6 倍
Zuul 2.x理念更先进,想基于Netty非阻塞和支持长连接,但SpringCloud目前还没有整合。 Zuul 2.x的性能较 Zuul 1.x 有较大提升。在性能方面,根据官方提供的基准测试, Spring Cloud Gateway 的 RPS(每秒请求数)是Zuul 的 1. 6 倍。
2. API网关
API网关是一个服务器,是系统的唯一入口。从面向对象设计的角度看,它与外观模式类似。API网关封装了系统内部架构,为每个客户端提供一个定制的API。它可能还具有其它职责,如身份验证、监控、负载均衡、缓存、请求分片与管理、静态响应处理。API网关方式的核心要点是,所有的客户端和消费端都通过统一的网关接入微服务,在网关层处理所有的非业务功能。通常,网关也是提供REST/HTTP的访问API。
网关应当具备以下功能:
- 性能:API高可用,负载均衡,容错机制。
- 安全:权限身份认证、脱敏,流量清洗,后端签名(保证全链路可信调用),黑名单(非法调用的限制)。
- 日志:日志记录(spainid,traceid)一旦涉及分布式,全链路跟踪必不可少。
- 缓存:数据缓存。
- 监控:记录请求响应数据,api耗时分析,性能监控。
- 限流:流量控制,错峰流控,可以定义多种限流规则。
- 灰度:线上灰度部署,可以减小风险。
- 路由:动态路由规则。
那要Spring Cloud Gateway还有什么用呢?
- 像Nginx这类网关,性能肯定是没得说,它适合做那种门户网关,是作为整个全局的网关,是对外的,处于最外层的;而Gateway这种,更像是业务网关,主要用来对应不同的客户端提供服务的,用于聚合业务的。各个微服务独立部署,职责单一,对外提供服务的时候需要有一个东西把业务聚合起来。
- 像Nginx这类网关,都是用不同的语言编写的,不易于扩展;而Gateway就不同,它是用Java写的,易于扩展和维护
- Gateway这类网关可以实现熔断、重试等功能,这是Nginx不具备的
所以,你看到的网关可能是这样的:
Spring Cloud Gateway
在高并发和潜在的高延迟场景下,网关要实现高性能高吞吐量的一个基本要求是全链路异步,不要阻塞线程。Zuul网关采用同步阻塞模式不符合要求。
Spring Cloud Gateway基于Webflux,比较完美地支持异步非阻塞编程,很多功能实现起来比较方便。Spring5必须使用java 8,函数式编程就是java8重要的特点之一,而WebFlux支持函数式编程来定义路由端点处理请求。
Spring Cloud Gateway 是 Spring Cloud官方推出来的新组件,用来代替服务网关:Zuul
该项目是基于 Spring 5.0,Spring Boot 2.0 和 Project Reactor 等技术开发的网关,它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。
- 为了提升网关的性能,SpringCloud Gateway是基于WebFlux框架实现的,而WebFlux框架底层则使用了高性能的Reactor模式通信框架Netty。
- Spring Cloud Gateway不仅提供统一的路由方式,并且基于 Filter 链的方式提供了网关基本的功能,例如:安全,监控/指标,和限流。 (反向代理鉴权流量控制熔断日志监控)
特性
- 基于 Spring Framework 5,Project Reactor 和 Spring Boot 2.0
- 动态路由
- Predicates 和 Filters 作用于特定路由
- 集成 Hystrix 断路器
- 集成 Spring Cloud DiscoveryClient(服务发现功能)
- 易于编写的 Predicates 和 Filters
- 限流
- 路径重写
三大核心概念:
- Route(路由): 这是网关的基本构建块。它由一个 ID,一个目标 URI,一组断言和一组过滤器定义。如果断言为真,则路由匹配。
- (1)id:路由标识,要求唯一,名称任意(默认值 uuid,一般不用,需要自定义)
- (2)uri:请求最终被转发到的目标地址
- (3)order: 路由优先级,数字越小,优先级越高
- (4)predicates:断言数组,即判断条件,如果返回值是boolean,则转发请求到 uri 属性指定的服务中
- (5)filters:过滤器数组,在请求传递过程中,对请求做一些修改
- Predicate(断言): 这是一个 Java 8 的 Predicate。输入类型是一个 ServerWebExchange。我们可以使用它来匹配来自 HTTP 请求的任何内容,例如 headers 或参数。如果请求与断言相匹配则进行路由
- Filter(过滤器): 这是org.springframework.cloud.gateway.filter.GatewayFilter的实例,我们可以使用它修改请求和响应。使用过滤器,可以在请求被路由前或者之后对请求进行修改。
web请求,通过一些匹配条件,定位到真正的服务节点。并在这个转发过程的前后,进行一些精细化控制。
predicate就是我们的匹配条件;
而filter,就可以理解为一个无所不能的拦截器。有了这两个元素,再加上目标uri,就可以实现一个具体的路由了
工作流程:
核心逻辑:路由转发+执行过滤器链
解析流程
核心概念:
- Gateway Client 向 Spring Cloud Gateway 发送请求
- 请求首先会被 HttpWebHandlerAdapter 进行提取组装成网关上下文
- 然后网关的上下文会传递到 DispatcherHandler ,它负责将请求分发给 RoutePredicateHandlerMapping
- RoutePredicateHandlerMapping 负责路由查找,并根据路由断言判断路由是否可用
- 如果过断言成功,由FilteringWebHandler 创建过滤器链并调用
- 通过特定于请求的 Fliter 链运行请求,Filter 被虚线分隔的原因是Filter可以在发送代理请求之前(pre)和之后(post)运行逻辑
- 执行所有pre过滤器逻辑。然后进行代理请求。发出代理请求后,将运行“post”过滤器逻辑。
- 处理完毕之后将 Response 返回到 Gateway 客户端
Filter过滤器:
- Filter在pre类型的过滤器可以做参数效验、权限效验、流量监控、日志输出、协议转换等。
- Filter在post类型的过滤器可以做响应内容、响应头的修改、日志输出、流量监控等
核心思想
当用户发出请求达到 GateWay 之后,会通过一些匹配条件,定位到真正的服务节点,并且在这个转发过程前后,进行一些细粒度的控制,其中 Predicate(断言) 是我们的匹配条件,Filter 是一个拦截器,有了这两点,再加上URL,就可以实现一个具体的路由,核心思想:路由转发+执行过滤器链
这个过程就好比考试,我们考试首先要找到对应的考场,我们需要知道考场的地址和名称(id和url),然后我们进入考场之前会有考官查看我们的准考证是否匹配(断言),如果匹配才会进入考场,我们进入考场之后,(路由之前)会进行身份的登记和考试的科目,填写考试信息,当我们考试完成之后(路由之后)会进行签字交卷,走出考场,这个就类似我们的过滤器
Route(路由) :构建网关的基础模块,由ID、目标URL、过滤器等组成
Predicate(断言) :开发人员可以匹配HTTP请求中的内容(请求头和请求参数),如果请求断言匹配贼进行路由
Filter(过滤) :GateWayFilter的实例,使用过滤器,可以在请求被路由之前或者之后对请求进行修改
- 客户端向 Spring Cloud Gateway 发出请求。如果 Gateway Handler Mapping 中找到与请求相匹配的路由,将其发送到 Gateway Web Handler。
- Handler 再通过指定的过滤器链来将请求发送到我们实际的服务执行业务逻辑,然后返回。
- 过滤器可以在发送代理请求之前或之后执行逻辑。执行所有的“pre”过滤逻辑,然后发出代理请求,最后执行“post”过滤逻辑。
- Filter在“pre”类型的过滤器可以做参数校验、权限校验、流量监控、日志输出、协议转换等,在“post”类型的过滤器中可以做响应内容、响应头的修改,日志的输出,流量监控等有着非常重要的作用。
快速上手
Spring Cloud Gateway 网关路由有两种配置方式:**
- 在配置文件 yml 中配置(推荐),,一般采用yml配置。
- 通过@Bean自定义 RouteLocator,在启动主类 Application 中配置
1.添加依赖包
Spring Cloud Gateway 是使用 netty+webflux 实现因此不需要再引入 web 模块。
org.springframework.cloud
spring-cloud-starter-gateway
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-config
com.alibaba.cloud
spring-cloud-alibaba-sentinel-gateway
com.alibaba.csp
sentinel-datasource-nacos
org.springframework.boot
spring-boot-starter-actuator
2.YML方式配置路由
server:
port: 9527
spring:
application:
name: cloud-gateway
cloud:
gateway:
discovery:
locator:
lowerCaseServiceId: true
enabled: true
routes:
- id: payment_routh #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名
uri: http://localhost:8001 #匹配后提供服务的路由地址
predicates:
- Path=/payment/get/** # 断言,路径相匹配的进行路由
- id: payment_routh2 #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名
uri: http://localhost:8001 #匹配后提供服务的路由地址
predicates:
- Path=/payment/lb/** # 断言,路径相匹配的进行路由,,路由到uri地址
eureka:
instance:
hostname: cloud-gateway-service
client: #网关服务注册进注册中心
service-url:
register-with-eureka: true
fetch-registry: true
defaultZone: http://eureka7001.com:7001/eureka
解析:各字段含义如下:
- spring.cloud.gateway.discovery.locator.enabled为true,表明gateway开启服务注册和发现的功能,
并且spring cloud gateway自动根据服务发现为每一个服务创建了一个router,这个router将以服务名开头的
请求路径转发到对应的服务
- spring.cloud.gateway.discovery.locator.lowerCaseServiceId是将请求路径上的服务名配置为小写
(因为服务注册的时候,向注册中心注册时将服务名转成大写的了),比如以/service-hi/*的请求路径被路由转发
到服务名为service-hi的服务上。
routes:
● id:我们自定义的路由 ID,保持唯一
● uri:目标服务地址
● predicates:路由条件,Predicate 接受一个输入参数,返回一个布尔值结果。该接口包含多种默认方法来将 Predicate 组合成其他复杂的逻辑(比如:与,或,非)。
● filters:过滤规则,本示例暂时没用。
上面这段配置的意思是,配置了一个 id 为 payment_routh的路由规则,
当访问地址 http://localhost:9527/payment/get/31时
会自动转发到地址:http://localhost:8001/payment/get/31。
在gateway中配置uri配置有三种方式,包括第一种:ws(websocket)方式: uri: ws://localhost:9000
第二种:http方式: uri: http://localhost:8130/第三种:lb(注册中心中服务名字)方式: uri: lb://brilliance-consumer
其中ws和http方式不容易出错,因为http格式比较固定,但是lb方式比较灵活自由。不考虑网关,只考虑服务时,服务名命名时比较自由,都能启动被访问,被注册到注册中心,但是如果提供给gateway使用时,就会对服务命名方式有特殊要求了。
java命名规范中可以使用的英文下划线(”_”)不能被识别,而我命名为:brilliance_consumer,刚好带下划线,改为brilliance-consumer后则可以正常通过网关访问自己项目。
3.通过微服务名实现动态路由
-
spring.main.allow-circular-references
为true来解决循环依赖问题 - spring.main.allow-bean-definition-overriding设置为true,表示后发现的bean会覆盖之前相同名称的bean。只有当集成了第三方的库,不同库直接由于是多个团队开发的,甚至这些团队属于不同的国家,有可能会出现bean名称相同的情况,所以默认不覆盖比较好。
# Tomcat
server:
port: 38080
# Spring
spring:
application:
# 应用名称
name: deml-gateway
profiles:
# 环境配置
active: dev
main:
allow-circular-references: true
allow-bean-definition-overriding: true
cloud:
nacos:
discovery:
# 服务注册地址
server-addr: 127.0.0.1:8848
# namespace: ${NACOS_NAMEPACE:Public}
group: ${NACOS_GROUP:DEFAULT_GROUP}
username: nacos
password: nacos
#服务注册ip和端口
#ip: ${NETWORK_IP:127.0.0.1}
#port: ${server.port}
config:
# 配置中心地址
server-addr: ${spring.cloud.nacos.discovery.server-addr}
# namespace: ${spring.cloud.nacos.discovery.namespace}
group: ${spring.cloud.nacos.discovery.group}
username: ${spring.cloud.nacos.discovery.username}
password: ${spring.cloud.nacos.discovery.password}
# 配置文件格式
file-extension: yml
# 共享配置
shared-configs:
- data-id: application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
group: ${spring.cloud.nacos.discovery.group}
sentinel:
# 取消控制台懒加载
eager: true
transport:
# 控制台地址
dashboard: ${SENTINEL_HOST:127.0.0.1}:${SENTINEL_PORT:8718}
# nacos配置持久化
datasource:
ds1:
nacos:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
dataId: sentinel-demo-gateway
groupId: DEFAULT_GROUP
# namespace: ${spring.cloud.nacos.discovery.namespace}
username: ${spring.cloud.nacos.discovery.username}
password: ${spring.cloud.nacos.discovery.password}
data-type: json
rule-type: gw-flow
默认情况下Gateway会根据注册中心注册的服务列表,以注册中心上微服务名为路径创建动态路由进行转发,从而实现动态路由的功能
- 需要注意的是uri的协议为lb,表示启用Gateway的负载均衡功能。
- lb://serviceName是spring cloud gateway在微服务中自动为我们创建的负载均衡uri
server:
port: 9527
spring:
application:
name: cloud-gateway
cloud:
gateway:
discovery:
locator:
enabled: true #开启从注册中心动态创建路由的功能,利用微服务名进行路由
routes:
- id: payment_routh #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名
# uri: http://localhost:8001 #匹配后提供服务的路由地址
uri: lb://cloud-payment-service #匹配后提供服务的路由地址
predicates:
- Path=/payment/** # 断言,路径相匹配的进行路由,路由到uri地址
- id: payment_routh2 #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名
# uri: http://localhost:8001 #匹配后提供服务的路由地址
uri: lb://cloud-payment-service #匹配后提供服务的路由地址
predicates:
- Path=/payment/** # 断言,路径相匹配的进行路由
eureka:
instance:
hostname: cloud-gateway-service
client: #服务提供者provider注册进eureka服务列表内
service-url:
register-with-eureka: true
fetch-registry: true
defaultZone: http://eureka7001.com:7001/eureka
4.自定义 RouteLocator方式配置路由
转发功能同样可以通过代码来实现,我们可以在启动类 GateWayApplication 中添加方法 customRouteLocator() 来定制转发规则。
@Configuration
public class GateWayConfig
{
/**
* 配置了一个id为route-name的路由规则,
* 当访问地址 http://localhost:9527/guonei时会自动转发到地址:http://news.baidu.com/guonei
* @param builder
* @return
*/
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder)
{
RouteLocatorBuilder.Builder routes = builder.routes();
routes
.route("path_route_atguigu", r -> r.path("/guonei").uri("http://news.baidu.com/guonei"))
.route("path_route_atguigu2", r -> r.path("/guoji").uri("http://news.baidu.com/guoji"))
.build();
return routes.build();
}
}
路由规则:Predicate的使用
Spring Cloud Gateway将路由匹配作为Spring WebFlux HandlerMapping基础架构的一部分。
- Spring Cloud Gateway包括许多内置的Route Predicate工厂。所有这些Predicate都与HTTP请求的不同属性匹配。多个 Route Predicate Factories 可以通过逻辑与(and)结合起来一起使用
- Spring Cloud Gateway 创建 Route 对象时, 使用 RoutePredicateFactory 创建 Predicate 对象,Predicate 对象可以赋值给 Route。 Spring Cloud Gateway 包含许多内置的Route Predicate Factories。
Predicate就是为了实现一组路由匹配规则,让请求过来找到对应的Route进行处理。
Spring Cloud Gateway 的功能很强大,我们仅仅通过 Predicates 的设计就可以看出来,前面我们只是使用了 predicates 进行了简单的条件匹配,其实 Spring Cloud Gataway 帮我们内置了很多 Predicates 功能。
Spring Cloud Gateway 是通过 Spring WebFlux 的 HandlerMapping 做为底层支持来匹配到转发路由,Spring Cloud Gateway 内置了很多 Predicates 工厂,这些 Predicates 工厂通过不同的 HTTP 请求参数来匹配,多个 Predicates 工厂可以组合使用。
Predicate 介绍
Predicate 来源于 Java 8,是 Java 8 中引入的一个函数,Predicate 接受一个输入参数,返回一个布尔值结果。该接口包含多种默认方法来将 Predicate 组合成其他复杂的逻辑(比如:与,或,非)。可以用于接口请求参数校验、判断新老数据是否有变化需要进行更新操作。
在 Spring Cloud Gateway 中 Spring 利用 Predicate 的特性实现了各种路由匹配规则,有通过 Header、请求参数等不同的条件来进行作为条件匹配到对应的路由。网上有一张图总结了 Spring Cloud 内置的几种 Predicate 的实现。
通过时间匹配
Predicate 支持设置一个时间,在请求进行转发的时候,可以通过判断在这个时间之前或者之后进行转发。通过时间匹配路由的功能可以用在限时抢购的一些场景中。
spring:
cloud:
gateway:
routes:
- id: time_route
uri: http://ityouknow.com
predicates:
- After=2018-01-20T06:06:06+08:00[Asia/Shanghai]
- Before=2018-01-20T06:06:06+08:00[Asia/Shanghai]
- Between=2018-01-20T06:06:06+08:00[Asia/Shanghai], 2019-01-20T06:06:06+08:00[Asia/Shanghai]
说明:
1. After Route Predicate 是指在这个时间之后的请求都转发到目标地址。
2. Before Route Predicate 刚好相反,在某个时间之前的请求的请求都进行转发。
3. Gateway 还支持限制路由请求在某一个时间段范围内,可以使用 Between Route Predicate 来实现。
● 上面的示例是指,请求时间在 2018年1月20日6点6分6秒之后的所有请求都转发到地址http://ityouknow.com。
● +08:00是指时间和UTC时间相差八个小时,时间地区为Asia/Shanghai。
● 添加完路由规则之后,访问地址http://localhost:8080会自动转发到http://ityouknow.com。
Spring 是通过 ZonedDateTime 来对时间进行的对比,ZonedDateTime 是 Java 8 中日期时间功能里,用于表示带时区的日期与时间信息的类,ZonedDateTime 支持通过时区来设置时间,中国的时区是:Asia/Shanghai。
通过 Cookie 匹配
Cookie Route Predicate 可以接收两个参数,一个是 Cookie name ,一个是正则表达式,路由规则会通过获取对应的 Cookie name 值和正则表达式去匹配,如果匹配上就会执行路由,如果没有匹配上则不执行。
spring:
cloud:
gateway:
routes:
- id: cookie_route
uri: http://ityouknow.com
predicates:
- Cookie=ityouknow, kee.e
使用 curl 测试,命令行输入: curl http://localhost:8080 –cookie “ityouknow=kee.e”则会返回页面代码,如果去掉–cookie “ityouknow=kee.e”,后台汇报 404 错误。
通过 Header 属性匹配
Header Route Predicate 和 Cookie Route Predicate 一样,也是接收 2 个参数,一个 header 中属性名称和一个正则表达式,这个属性值和正则表达式匹配则执行。
spring:
cloud:
gateway:
routes:
- id: header_route
uri: http://ityouknow.com
predicates:
- Header=X-Request-Id, d+
使用 curl 测试,命令行输入:curl http://localhost:8080 -H “X-Request-Id:666666″则返回页面代码证明匹配成功。将参数-H “X-Request-Id:666666″改为-H “X-Request-Id:neo”再次执行时返回404证明没有匹配。
通过 Host 匹配
Host Route Predic服务器托管网ate 接收一组参数,一组匹配的域名列表,这个模板是一个 ant 分隔的模板,用 . 号作为分隔符。它通过参数中的主机地址作为匹配规则。
spring:
cloud:
gateway:
routes:
- id: host_route
uri: http://ityouknow.com
predicates:
- Host=**.ityouknow.com
使用 curl 测试,命令行输入:curl http://localhost:8080 -H “Host: www.ityouknow.com” curl http://localhost:8080 -H “Host: md.ityouknow.com”经测试以上两种 host 均可匹配到 host_route 路由,去掉 host 参数则会报 404 错误。
通过请求方式匹配
可以通过是 POST、GET、PUT、DELETE 等不同的请求方式来进行路由。
spring:
cloud:
gateway:
routes:
- id: method_route
uri: http://ityouknow.com
predicates:
- Method=GET
使用 curl 测试,命令行输入:curl 默认是以 GET 的方式去请求 curl http://localhost:8080测试返回页面代码,证明匹配到路由,我们再以 POST 的方式请求测试。curl 默认是以 GET 的方式去请求 curl -X POST http://localhost:8080,返回 404 没有找到,证明没有匹配上路由
通过请求路径匹配
Path Route Predicate 接收一个匹配路径的参数来判断是否走路由。
spring:
cloud:
gateway:
routes:
- id: host_route
uri: http://ityouknow.com
predicates:
- Path=/foo/{segment}
如果请求路径符合要求,则此路由将匹配,例如:/foo/1 或者 /foo/bar。
通过请求参数匹配
Query Route Predicate 支持传入两个参数,一个是属性名一个为属性值,属性值可以是正则表达式。
spring:
cloud:
gateway:
routes:
- id: query_route
uri: http://ityouknow.com
predicates:
- Query=smile
这样配置,只要请求中包含 smile 属性的参数即可匹配路由。
使用 curl 测试,命令行输入:curl localhost:8080?smile=x&id=2,经过测试发现只要请求汇总带有 smile 参数即会匹配路由,不带 smile 参数则不会匹配。
还可以将 Query 的值以键值对的方式进行配置,这样在请求过来时会对属性值和正则进行匹配,匹配上才会走路由。
spring:
cloud:
gateway:
routes:
- id: query_route
uri: http://ityouknow.com
predicates:
- Query=keep, pu.
这样只要当请求中包含 keep 属性并且参数值是以 pu 开头的长度为三位的字符串才会进行匹配和路由。
使用 curl 测试,命令行输入:curl localhost:8080?keep=pub,测试可以返回页面代码,将 keep 的属性值改为 pubx 再次访问就会报 404,证明路由需要匹配正则表达式才会进行路由。
通过请求 ip 地址进行匹配
Predicate 也支持通过设置某个 ip 区间号段的请求才会路由,RemoteAddr Route Predicate 接受 cidr 符号(IPv4 或 IPv6 )字符串的列表(最小大小为1),例如 192.168.0.1/16 (其中 192.168.0.1 是 IP 地址,16 是子网掩码)。
spring:
cloud:
gateway:
routes:
- id: remoteaddr_route
uri: http://ityouknow.com
predicates:
- RemoteAddr=192.168.1.1/24
可以将此地址设置为本机的 ip 地址进行测试。curl localhost:8080,如果请求的远程地址是 192.168.1.10,则此路由将匹配。
组合使用
Spring Cloud Gateway 使用非常的灵活,可以根据不同的情况来进行路由分发,在实际项目中可以将各种 Predicate自由组合使用。
spring:
cloud:
gateway:
routes:
- id: host_foo_path_headers_to_httpbin
uri: http://ityouknow.com
predicates:
- Host=**.foo.org
- Path=/headers
- Method=GET
- Header=X-Request-Id, d+
- Query=foo, ba.
- Query=baz
- Cookie=chocolate, ch.p
- After=2018-01-20T06:06:06+08:00[Asia/Shanghai]
各种 Predicates 同时存在于同一个路由时,请求必须同时满足所有的条件才被这个路由匹配。
一个请求满足多个路由的谓词条件时,请求只会被首个成功匹配的路由转发
server:
port: 9527
spring:
application:
name: cloud-gateway
cloud:
gateway:
discovery:
locator:
enabled: true #开启从注册中心动态创建路由的功能
routes:
- id: payment_routh #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名
# uri: http://localhost:8001 #匹配后提供服务的路由地址
uri: lb://cloud-payment-service #匹配后提供服务的路由地址
predicates:
- Path=/payment/get/** # 断言,路径相匹配的进行路由
- id: payment_routh2 #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名
# uri: http://localhost:8001 #匹配后提供服务的路由地址
uri: lb://cloud-payment-service #匹配后提供服务的路由地址
predicates:
- Path=/payment/lb/** # 断言,路径相匹配的进行路由
- After=2020-02-05T15:10:03.685+08:00[Asia/Shanghai] # 断言,路径相匹配的进行路由
#- Before=2020-02-05T15:10:03.685+08:00[Asia/Shanghai] # 断言,路径相匹配的进行路由
#- Between=2020-02-02T17:45:06.206+08:00[Asia/Shanghai],2020-03-25T18:59:06.206+08:00[Asia/Shanghai]
#- Cookie=username,zzyy
#- Header=X-Request-Id, d+ # 请求头要有X-Request-Id属性并且值为整数的正则表达式
#- Host=**.atguigu.com
- Method=GET
- Query=username, d+ # 要有参数名username并且值还要是整数才能路由
eureka:
instance:
hostname: cloud-gateway-service
client: #服务提供者provider注册进eureka服务列表内
service-url:
register-with-eureka: true
fetch-registry: true
defaultZone: http://eureka7001.com:7001/eureka
● id:我们自定义的路由 ID,保持唯一
● uri:目标服务地址
● predicates:路由条件,Predicate接受一个输入参数返回一个布尔值。
● 该属性包含多种默认方法来将Predicate组合成其他复杂的逻辑(比如:与,或,非)
基于 Filter(过滤器) 实现的高级功能
路由过滤器可用于修改请求的 Http 的请求或者响应,或者根据请求或者响应做一些特殊的限制 , 更多时候我们会利用 GatewayFilter 做一些具体的路由配置。路由过滤器只能指定路由进行使用。 Spring Cloud Gateway 内置了多种路由过滤器,他们都由GatewayFilter的工厂类来产生
Spring Cloud Gateway 的 Filter 的生命周期只有两个:“pre” 和 “post”
。
- PRE: 这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。
- POST:这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的 HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等。
Spring Cloud Gateway 的 Filter 分为两种:GatewayFilter 与 GlobalFilter。
- GlobalFilter 会应用到所有的路由上;
- GatewayFilter 将应用到单个路由或者一个分组的路由上;
Spring Cloud Gateway 内置了9种 GlobalFilter,比如 Netty Routing Filter、LoadBalancerClient Filter、Websocket Routing Filter 等,根据名字即可猜测出这些 Filter 的作者,具体大家可以参考官网内容:Global Filters、
GatewayFilter
快速上手 Filter 使用
以 AddRequestParameter GatewayFilter 来演示一下,如何在项目中使用 GatewayFilter,AddRequestParameter GatewayFilter 可以在请求中添加指定参数。
这样就会给匹配的每个请求添加上foo=bar的参数和值。
server:
port: 8888
spring:
application:
name: cloud-gateway-eureka
cloud:
gateway:
discovery:
locator:
enabled: true
routes:
- id: add_request_parameter_route
uri: http://localhost:9000
filters:
- AddRequestParameter=foo, bar #过滤器工厂会在匹配的请求头加上一对请求头,名称为foo值为bar
predicates:
- Method=GET
eureka:
client:
service-url:
defaultZone: http://localhost:8000/eureka/
logging:
level:
org.springframework.cloud.gateway: debug
- 这里的 routes 手动指定了服务的转发地址,设置所有的 GET 方法都会自动添加foo=bar,http://localhost:9000 是 spring-cloud-producer 项目,我们在此项目中添加一个 foo() 方法,用来接收转发中添加的参数 foo。
- 修改完成后重启 cloud-gateway-eureka、spring-cloud-producer 项目。访问地址http://localhost:9000/foo页面返回:hello null!,说明并没有接受到参数 foo;
- 通过网关来调用此服务,浏览器访问地址http://localhost:8888/foo页面返回:hello bar!,说明成功接收到参数 foo 参数的值 bar ,证明网关在转发的过程中已经通过 filter 添加了设置的参数和值。
修改请求路径的过滤器:
StripPrefix Filter
StripPrefix Filter 是一个请求路径截取的功能,我们可以利用这个功能来做特殊业务的转发。
spring:
cloud:
gateway:
routes:
- id: nameRoot
uri: http://nameservice
predicates:
- Path=/name/**
filters:
- StripPrefix=2
上面这个配置的例子表示,当请求路径匹配到/name/**会将包含name和后边的字符串接去掉转发, StripPrefix=2就代表截取路径的个数,这样配置后当请求/name/bar/foo后端匹配到的请求路径就会变成http://nameservice/foo。
PrefixPath Filter
PrefixPath Filter 的作用和 StripPrefix 正相反,是在 URL 路径前面添加一部分的前缀
spring:
cloud:
gateway:
routes:
- id: prefixpath_route
uri: http://example.org
filters:
- PrefixPath=/mypath
自定义全局过滤器 GlobalFilter
- 全局日志记录/统一网关鉴权
@Component //必须加
public class MyLogGateWayFilter implements GlobalFilter,Ordered
{
@Override
public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain)
{
System.out.println("time:"+new Date()+"t 执行了自定义的全局过滤器: "+"MyLogGateWayFilter"+"hello");
String uname = exchange.getRequest().getQueryParams().getFirst("uname");
if (uname == null) {
System.out.println("****用户名为null,无法登录");
exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}
@Override
public int getOrder()
{
return 0;
}
}
权限验证
网关本地yml
# Tomcat
server:
port: 38080
# Spring
spring:
application:
# 应用名称
name: demo-gateway
profiles:
# 环境配置
active: dev
main:
allow-circular-references: true
allow-bean-definition-overriding: true
cloud:
nacos:
discovery:
# 服务注册地址
server-addr: 127.0.0.1:8848
# namespace: ${NACOS_NAMEPACE:Public}
group: ${NACOS_GROUP:DEFAULT_GROUP}
username: nacos
password: nacos
#服务注册ip和端口
#ip: ${NETWORK_IP:127.0.0.1}
#port: ${server.port}
config:
# 配置中心地址
server-addr: ${spring.cloud.nacos.discovery.server-addr}
# namespace: ${spring.cloud.nacos.discovery.namespace}
group: ${spring.cloud.nacos.discovery.group}
username: ${spring.cloud.nacos.discovery.username}
password: ${spring.cloud.nacos.discovery.password}
# 配置文件格式
file-extension: yml
# 共享配置
shared-configs:
- data-id: application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
group: ${spring.cloud.nacos.discovery.group}
sentinel:
# 取消控制台懒加载
eager: true
transport:
# 控制台地址
dashboard: ${SENTINEL_HOST:127.0.0.1}:${SENTINEL_PORT:8718}
# nacos配置持久化
datasource:
ds1:
nacos:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
dataId: sentinel-demo-gateway
groupId: DEFAULT_GROUP
# namespace: ${spring.cloud.nacos.discovery.namespace}
username: ${spring.cloud.nacos.discovery.username}
password: ${spring.cloud.nacos.discovery.password}
data-type: json
rule-type: gw-flow
nacos的yml
spring:
redis:
host: 127.0.0.1
port: 6379
password: 123456
timeout: 1000 # 连接超时时间(毫秒)
jedis:
pool:
max-active: 10 # 连接池最大连接数(使用负值表示没有限制)
max-wait: -1ms # 连接池最大阻塞等待时间(使用负值表示没有限制)
max-idle: 8 # 连接池中的最大 空闲连接
min-idle: 0 # 连接池中的最小空闲连接
cloud:
gateway:
discovery:
locator:
lowerCaseServiceId: true
enabled: true
routes:
# 认证中心
- id: demo-auth
uri: lb://demo-auth
predicates:
- Path=/auth/**
filters:
# 验证码处理
- CacheRequestFilter
- ValidateCodeFilter
- StripPrefix=1
# 系统模块
- id: demo-system
uri: lb://demo-system
predicates:
- Path=/system/**
filters:
- StripPrefix=1
# 文件服务
- id: demo-file
uri: lb://demo-file
predicates:
- Path=/file/**
filters:
- StripPrefix=1
# 监控模块
- id: demo-monitor
uri: lb://demo-monitor
predicates:
- Path=/monitor/**
filters:
- StripPrefix=1
# 定时模块
- id: demo-job
uri: lb://demo-job
predicates:
- Path=/job/**
filters:
- StripPrefix=1
# 安全配置
security:
# 验证码
captcha:
enabled: true
type: math
# 防止XSS攻击
xss:
enabled: true
excludeUrls:
- /system/notice
# 不校验白名单
ignore:
whites:
- /auth/logout
- /auth/login
- /auth/register
- /*/v2/api-docs
- /csrf
- /job/api/callback
- /job/api/registry
- /job/api/registryRemove
读取白名单配置
/**
* 放行白名单配置
*/
@Configuration
@RefreshScope
@ConfigurationProperties(prefix = "security.ignore")
@Data
public class IgnoreWhiteProperties
{
/**
* 放行白名单配置,网关不校验此处的白名单
*/
private List whites = new ArrayList();
}
权限校验全局配置器
/**
* 网关鉴权
*/
@Component
public class AuthFilter implements GlobalFilter, Ordered
{
private static final Logger log = LoggerFactory.getLogger(AuthFilter.class);
// 排除过滤的 uri 地址,nacos自行添加
@Autowired
private IgnoreWhiteProperties ignoreWhite;
@Autowired
private RedisService redisService;
@Override
public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain)
{
ServerHttpRequest request = exchange.getRequest();
ServerHttpRequest.Builder mutate = request.mutate();
String url = request.getURI().getPath();
// 跳过不需要验证的路径
if (StringUtils.matches(url, ignoreWhite.getWhites()))
{
return chain.filter(exchange);
}
String token = getToken(request);
if (StringUtils.isEmpty(token))
{
return unauthorizedResponse(exchange, "令牌不能为空");
}
Claims claims = JwtUtils.parseToken(token);
if (claims == null)
{
return unauthorizedResponse(exchange, "令牌已过期或验证不正确!");
}
String userkey = JwtUtils.getUserKey(claims);
boolean islogin = redisService.hasKey(getTokenKey(userkey));
if (!islogin)
{
return unauthorizedResponse(exchange, "登录状态已过期");
}
String userid = JwtUtils.getUserId(claims);
String username = JwtUtils.getUserName(claims);
String deptid = JwtUtils.getDeptId(claims);
if (StringUtils.isEmpty(userid) || StringUtils.isEmpty(username))
{
return unauthorizedResponse(exchange, "令牌验证失败");
}
// 设置用户信息到请求
addHeader(mutate, SecurityConstants.USER_KEY, userkey);
addHeader(mutate, SecurityConstants.DETAILS_USER_ID, userid);
addHeader(mutate, SecurityConstants.DETAILS_USERNAME, username);
addHeader(mutate, SecurityConstants.DETAILS_DEPT_ID, deptid);
// 内部请求来源参数清除
removeHeader(mutate, SecurityConstants.FROM_SOURCE);
return chain.filter(exchange.mutate().request(mutate.build()).build());
}
private void addHeader(ServerHttpRequest.Builder mutate, String name, Object value)
{
if (value == null)
{
return;
}
String valueStr = value.toString();
String valueEncode = ServletUtils.urlEncode(valueStr);
mutate.header(name, valueEncode);
}
private void removeHeader(ServerHttpRequest.Builder mutate, String name)
{
mutate.headers(httpHeaders -> httpHeaders.remove(name)).build();
}
private Mono unauthorizedResponse(ServerWebExchange exchange, String msg)
{
log.error("[鉴权异常处理]请求路径:{}", exchange.getRequest().getPath());
return ServletUtils.webFluxResponseWriter(exchange.getResponse(), msg, HttpStatus.UNAUTHORIZED);
}
/**
* 获取缓存key
*/
private String getTokenKey(String token)
{
return CacheConstants.LOGIN_TOKEN_KEY + token;
}
/**
* 获取请求token
*/
private String getToken(ServerHttpRequest request)
{
String token = request.getHeaders().getFirst(TokenConstants.AUTHENTICATION);
// 如果前端设置了令牌前缀,则裁剪掉前缀
if (StringUtils.isNotEmpty(token) && token.startsWith(TokenConstants.PREFIX))
{
token = token.replaceFirst(TokenConstants.PREFIX, StringUtils.EMPTY);
}
return token;
}
@Override
public int getOrder()
{
return -200;
}
}
自定义局部过滤器:
虽说内置的过滤器能够解决很多场景,但是难免还是有些特殊需求需要定制一个过滤器,下面就来介绍一下如何自定义局部过滤器。
/**
* 名称必须是xxxGatewayFilterFactory形式
* todo:模拟授权的验证,具体逻辑根据业务完善
*/
@Component
@Slf4j
public class AuthorizeGatewayFilterFactory extends AbstractGatewayFilterFactory {
private static final String AUTHORIZE_TOKEN = "token";
//构造函数,加载Config
public AuthorizeGatewayFilterFactory() {
//固定写法
super(AuthorizeGatewayFilterFactory.Config.class);
log.info("Loaded GatewayFilterFactory [Authorize]");
}
//读取配置文件中的参数 赋值到 配置类中
@Override
public List shortcutFieldOrder() {
//Config.enabled
return Arrays.asList("enabled");
}
@Override
public GatewayFilter apply(AuthorizeGatewayFilterFactory.Config config) {
return (exchange, chain) -> {
//判断是否开启授权验证
if (!config.isEnabled()) {
return chain.filter(exchange);
}
ServerHttpRequest request = exchange.getRequest();
HttpHeaders headers = request.getHeaders();
//从请求头中获取token
String token = headers.getFirst(AUTHORIZE_TOKEN);
if (token == null) {
//从请求头参数中获取token
token = request.getQueryParams().getFirst(AUTHORIZE_TOKEN);
}
ServerHttpResponse response = exchange.getResponse();
//如果token为空,直接返回401,未授权
if (StringUtils.isEmpty(token)) {
response.setStatusCode(HttpStatus.UNAUTHORIZED);
//处理完成,直接拦截,不再进行下去
return response.setComplete();
}
/**
* todo chain.filter(exchange) 之前的都是过滤器的前置处理
*
* chain.filter().then(
* 过滤器的后置处理...........
* )
*/
//授权正常,继续下一个过滤器链的调用
return chain.filter(exchange);
};
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class Config {
// 控制是否开启认证
private boolean enabled;
}
}
局部过滤器需要在路由中配置才能生效,配置如下:
spring:
cloud:
gateway:
routes:
- id: gateway-provider_1
uri: http://localhost:9024
predicates:
- Path=/gateway/provider/**
## 配置过滤器(局部)
filters:
- AddResponseHeader=X-Response-Foo, Bar
- AuthorizeGatewayFilterFactory #自定义过滤器配置,值为true需要验证授权,false不需要
- Authorize=true
上述的 AuthorizeGatewayFilterFactory 只是涉及到了过滤器的前置处理,后置处理是在 chain.filter().then() 中的 then() 方法中完成的,具体可以看下项目源码中的 TimeGatewayFilterFactory,代码就不再贴出来了,如下图:
/**
* 黑名单过滤器
*/
@Component
public class BlackListUrlFilter extends AbstractGatewayFilterFactory
{
@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 blacklistUrl;
private List blacklistUrlPattern = new ArrayList();
public boolean matchBlacklist(String url)
{
return !blacklistUrlPattern.isEmpty() && blacklistUrlPattern.stream().anyMatch(p -> p.matcher(url).find());
}
public List getBlacklistUrl()
{
return blacklistUrl;
}
public void setBlacklistUrl(List blacklistUrl)
{
this.blacklistUrl = blacklistUrl;
this.blacklistUrlPattern.clear();
this.blacklistUrl.forEach(url -> {
this.blacklistUrlPattern.add(Pattern.compile(url.replaceAll("**", "(.*?)"), Pattern.CASE_INSENSITIVE));
});
}
}
}
/**
* 获取body请求数据(解决流不能重复读取问题)
*/
@Component
public class CacheRequestFilter extends AbstractGatewayFilterFactory
{
public CacheRequestFilter()
{
super(Config.class);
}
@Override
public String name()
{
return "CacheRequestFilter";
}
@Override
public GatewayFilter apply(Config config)
{
CacheRequestGatewayFilter cacheRequestGatewayFilter = new CacheRequestGatewayFilter();
Integer order = config.getOrder();
if (order == null)
{
return cacheRequestGatewayFilter;
}
return new OrderedGatewayFilter(cacheRequestGatewayFilter, order);
}
public static class CacheRequestGatewayFilter implements GatewayFilter
{
@Override
public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain)
{
// GET DELETE 不过滤
HttpMethod method = exchange.getRequest().getMethod();
if (method == null || method == HttpMethod.GET || method == HttpMethod.DELETE)
{
return chain.filter(exchange);
}
return ServerWebExchangeUtils.cacheRequestBodyAndRequest(exchange, (serverHttpRequest) -> {
if (serverHttpRequest == exchange.getRequest())
{
return chain.filter(exchange);
}
return chain.filter(exchange.mutate().request(serverHttpRequest).build());
});
}
}
@Override
public List shortcutFieldOrder()
{
return Collections.singletonList("order");
}
static class Config
{
private Integer order;
public Integer getOrder()
{
return order;
}
public void setOrder(Integer order)
{
this.order = order;
}
}
}
**
* 验证码过滤器
*/
@Component
public class ValidateCodeFilter extends AbstractGatewayFilterFactory
/**
* 跨站脚本过滤器
*/
@Component
@ConditionalOnProperty(value = "security.xss.enabled", havingValue = "true")
public class XssFilter implements GlobalFilter, Ordered
{
// 跨站脚本的 xss 配置,nacos自行添加
@Autowired
private XssProperties xss;
@Override
public Mono 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 getBody()
{
Flux 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;
}
}
路由器:
限速路由器
限速在高并发场景中比较常用的手段之一,可以有效的保障服务的整体稳定性,Spring Cloud Gateway 提供了基于 Redis 的限流方案。首先需要添加对应的依赖包spring-boot-starter-data-redis-reactive
org.springframework.cloud
spring-boot-starter-data-redis-reactive
配置文件中需要添加 Redis 地址和限流的相关配置
spring:
application:
name: cloud-gateway-eureka
redis:
host: localhost
password:
port: 6379
cloud:
gateway:
discovery:
locator:
enabled: true
routes:
- id: requestratelimiter_route
uri: http://example.org
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 10
redis-rate-limiter.burstCapacity: 20
key-resolver: "#{@userKeyResolver}"
predicates:
- Method=GET
- filter 名称必须是 RequestRateLimiter
- redis-rate-limiter.replenishRate:允许用户每秒处理多少个请求
- redis-rate-limiter.burstCapacity:令牌桶的容量,允许在一秒钟内完成的最大请求数
- key-resolver:使用 SpEL 按名称引用 bean
项目中设置限流的策略,创建 Config 类。
public class Config {
@Bean
KeyResolver userKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("user"));
}
}
根据请求参数中的 user 字段来限流,也可以设置根据请求 IP 地址来限流,设置如下:
@Bean
public KeyResolver ipKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
}
这样网关就可以根据不同策略来对请求进行限流了。
熔断路由器
熔断器 Hystrix
Spring Cloud Gateway 也可以利用 Hystrix 的熔断特性,在流量过大时进行服务降级,同样我们还是首先给项目添加上依赖。
org.springframework.cloud
spring-cloud-starter-netflix-hystrix
spring:
cloud:
gateway:
routes:
- id: hystrix_route
uri: http://example.org
filters:
- Hystrix=myCommandName
配置后,gateway 将使用 myCommandName 作为名称生成 HystrixCommand 对象来进行熔断管理。如果想添加熔断后的回调内容,需要在添加一些配置。
spring:
cloud:
gateway:
routes:
- id: hystrix_route
uri: lb://spring-cloud-producer
predicates:
- Path=/consumingserviceendpoint
filters:
- name: Hystrix
args:
name: fallbackcmd
fallbackUri: forward:/incaseoffailureusethis
fallbackUri: forward:/incaseoffailureusethis配置了 fallback 时要会调的路径,当调用 Hystrix 的 fallback 被调用时,请求将转发到/incaseoffailureusethis这个 URI。
重试路由器
RetryGatewayFilter 是 Spring Cloud Gateway 对请求重试提供的一个 GatewayFilter Factory配置示例:
spring:
cloud:
gateway:
routes:
- id: retry_test
uri: lb://spring-cloud-producer
predicates:
- Path=/retry
filters:
- name: Retry
args:
retries: 3
statuses: BAD_GATEWAY
Retry GatewayFilter 通过这四个参数来控制重试机制: retries, statuses, methods, 和 series。
- retries:重试次数,默认值是 3 次
- statuses:HTTP 的状态返回码,取值请参考:org.springframework.http.HttpStatus
- methods:指定哪些方法的请求需要进行重试逻辑,默认值是 GET 方法,取值参考:org.springframework.http.HttpMethod
- series:一些列的状态码配置,取值参考:org.springframework.http.HttpStatus.Series。符合的某段状态码才会进行重试逻辑,默认值是 SERVER_ERROR,值是 5,也就是 5XX(5 开头的状态码),共有5 个值。
以上便是项目中常用的一些网关操作,更多关于 Spring Cloud GateWay 的使用请参考官网。
Gateway 集成 nacos 注册中心实现服务发现:
集成的注册中心,使得网关能够从注册中心自动获取uri,并实现负载均衡
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
(2)启动类添加 @EnableDiscoveryClient 注解开启注册中心功能
(3)配置 nacos 注册中心的地址:
nacos:
namespace: 856a40d7-6548-4494-bdb9-c44491865f63
url: 120.76.129.106:80
spring:
cloud:
nacos:
discovery:
server-addr: ${nacos.url}
namespace: ${nacos.namespace}
register-enabled: true
(4)服务路由配置:
spring:
cloud:
gateway:
routes:
- id: gateway-provider_1
## 使用了lb形式,从注册中心负载均衡的获取uri
uri: lb://gateway-provider
## 配置断言
predicates:
- Path=/gateway/provider/**
filters:
- AddResponseHeader=X-Response-Foo, Bar
路由配置中唯一不同的就是路由的 uri,格式:lb://service-name,这是固定写法:
- lb:固定格式,指的是从nacos中按照名称获取微服务,并遵循负载均衡策略
- service-name:nacos注册中心的服务名称,这里并不是IP地址形式的
为什么指定了 lb 就可以开启负载均衡,前面说过全局过滤器 LoadBalancerClientFilter 就是负责路由寻址和负载均衡的,可以看到如下源码:
(5)开启 gateway 自动路由配置:
随着我们的系统架构不断地发展,系统中微服务的数量肯定会越来越多,我们不可能每添加一个服务,就在网关配置一个新的路由规则,这样的维护成本很大;特别在很多种情况,我们在请求路径中会携带一个路由标识方便进行转发,而这个路由标识一般都是服务在注册中心中的服务名,因此这是我们就可以开启 spring cloud gateway 的自动路由功能,网关自动根据注册中心的服务名为每个服务创建一个router,将以服务名开头的请求路径转发到对应的服务,配置如下:
# enabled:默认为false,设置为true表明spring cloud gateway开启服务发现和路由的功能,网关自动根据注册中心的服务名为每个服务创建一个router,将以服务名开头的请求路径转发到对应的服务
spring.cloud.gateway.discovery.locator.enabled = true
# lowerCaseServiceId:启动 locator.enabled=true 自动路由时,路由的路径默认会使用大写ID,若想要使用小写ID,可将lowerCaseServiceId设置为true
spring.cloud.gateway.discovery.locator.lower-case-service-id = true
这里需要注意的是,由于我们的网关项目配置了 server.servlet.context-path 属性,这会导致自动路由失败的问题,因此我们需要做如下两个修改:
# 重写过滤链,解决项目设置了 server.servlet.context-path 导致 locator.enabled=true 默认路由策略404的问题
spring.cloud.gateway.discovery.locator.filters[0] = PreserveHostHeader
@Configuration
public class GatewayConfig
{
@Value ("${server.servlet.context-path}")
private String prefix;
/**
* 过滤 server.servlet.context-path 属性配置的项目路径,防止对后续路由策略产生影响,因为 gateway 网关不支持 servlet
*/
@Bean
@Order (-1)
public WebFilter apiPrefixFilter()
{
return (exchange, chain) ->
{
ServerHttpRequest request = exchange.getRequest();
String path = request.getURI().getRawPath();
path = path.startsWith(prefix) ? path.replaceFirst(prefix, "") : path;
ServerHttpRequest newRequest = request.mutate().path(path).build();
return chain.filter(exchange.mutate().request(newRequest).build());
};
}
}
至此,我们就开启了 spring cloud gateway 的自动路由功能,网关自动根据注册中心的服务名为每个服务创建一个router,将以服务名开头的请求路径转发到对应的服务。假设我们的服务提供者在 nacos 注册中心的服务名为 “gateway-provider”,这时我们只需要访问 “http://localhost:9023/gateway/gateway-provider/test”,就可以将请求成功转发过去了
自定义全局异常处理器:
前后端分离架构中必须定制返回的异常信息。传统的Spring Boot 服务中都是使用 @ControllerAdvice 来包装全局异常处理的,但是由于服务下线,请求并没有到达。因此必须在网关中也要定制一层全局异常处理,这样才能更加友好的和客户端交互。
Spring Cloud Gateway提供了多种全局处理的方式,今天只介绍其中一种方式,实现还算比较优雅:
直接创建一个类 GlobalErrorExceptionHandler,实现 ErrorWebExceptionHandler,重写其中的 handle 方法,代码如下:
/**
* 用于网关的全局异常处理
* @Order(-1):优先级一定要比ResponseStatusExceptionHandler低
*/
@Slf4j
@Order(-1)
@Component
@RequiredArgsConstructor
public class GlobalErrorExceptionHandler implements ErrorWebExceptionHandler {
private final ObjectMapper objectMapper;
@SuppressWarnings({"rawtypes", "unchecked", "NullableProblems"})
@Override
public Mono handle(ServerWebExchange exchange, Throwable ex) {
ServerHttpResponse response = exchange.getResponse();
if (response.isCommitted()) {
return Mono.error(ex);
}
// JOSN格式返回
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
if (ex instanceof ResponseStatusException) {
response.setStatusCode(((ResponseStatusException) ex).getStatus());
}
return response.writeWith(Mono.fromSupplier(() -> {
DataBufferFactory bufferFactory = response.bufferFactory();
try {
//todo 返回响应结果,根据业务需求,自己定制
CommonResponse resultMsg = new CommonResponse("500",ex.getMessage(),null);
return bufferFactory.wrap(objectMapper.writeValueAsBytes(resultMsg));
}
catch (JsonProcessingException e) {
log.error("Error writing response", ex);
return bufferFactory.wrap(new byte[0]);
}
}));
}
}
好了,全局异常处理已经定制完成了,在测试一下,此时正常返回JSON数据了(JSON的样式根据架构需要自己定制),如下图:
实战:
MateCloud项目源码
MateCloud后端源码 github.com/matevip/mat… gitee.com/matevip/mat…
Artemis前端源码 github.com/matevip/art… gitee.com/matevip/art…
org.springframework.boot
spring-boot-starter-webflux
org.springframework.cloud
spring-cloud-starter-gateway
org.springframework.boot
spring-boot-starter-data-redis-reactive
注意:
Spring Cloud Gateway 不使用 Web 作为服务器,而是 使用 WebFlux 作为服务器 ,Gateway 项目已经依赖了 starter-webflux,所以这里千万不要依赖 starter-web
由于过滤器等功能依然需要 Servlet 支持,故这里还需要依赖javax.servlet:javax.servlet-api
地址:gitee.com/matevip/mat…
package vip.mate.gateway.filter;
import io.jsonwebtoken.Claims;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import vip.mate.core.cloud.props.MateApiProperties;
import vip.mate.core.common.constant.MateConstant;
import vip.mate.core.common.constant.Oauth2Constant;
import vip.mate.core.common.util.ResponseUtil;
import vip.mate.core.common.util.SecurityUtil;
import vip.mate.core.common.util.StringPool;
import vip.mate.core.common.util.TokenUtil;
import vip.mate.core.redis.core.RedisService;
/**
* 网关统一的token验证
*
* @author pangu
* @since 1.5.8
*/
@Slf4j
@Component
@AllArgsConstructor
public class PreUaaFilter implements GlobalFilter, Ordered {
private final MateApiProperties mateApiProperties;
private final RedisService redisService;
/**
* 路径前缀以/mate开头,如mate-system
*/
public static final String PATH_PREFIX = "/mate";
/**
* 索引自1开头检索,跳过第一个字符就是检索的字符的问题
*/
public static final int FROM_INDEX = 1;
@Override
public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 如果未启用网关验证,则跳过
if (!mateApiProperties.getEnable()) {
return chain.filter(excha服务器托管网nge);
}
// 如果在忽略的url里,则跳过
String path = replacePrefix(exchange.getRequest().getURI().getPath());
String requestUrl = exchange.getRequest().getURI().getRawPath();
if (ignore(path) || ignore(requestUrl)) {
return chain.filter(exchange);
}
// 验证token是否有效
ServerHttpResponse resp = exchange.getResponse();
String headerToken = exchange.getRequest().getHeaders().getFirst(Oauth2Constant.HEADER_TOKEN);
if (headerToken == null) {
return unauthorized(resp, "没有携带Token信息!");
}
String token = TokenUtil.getToken(headerToken);
Claims claims = SecurityUtil.getClaims(token);
if (claims == null) {
return unauthorized(resp, "token已过期或验证不正确!");
}
// 判断token是否存在于redis,对于只允许一台设备场景适用。
// 如只允许一台设备登录,需要在登录成功后,查询key是否存在,如存在,则删除此key,提供思路。
boolean hasKey = redisService.hasKey("auth:" + token);
log.debug("查询token是否存在: " + hasKey);
if (!hasKey) {
return unauthorized(resp, "登录超时,请重新登录");
}
return chain.filter(exchange);
}
/**
* 检查是否忽略url
* @param path 路径
* @return boolean
*/
private boolean ignore(String path) {
return mateApiProperties.getIgnoreUrl().stream()
.map(url -> url.replace("/**", ""))
.anyMatch(path::startsWith);
}
/**
* 移除模块前缀
* @param path 路径
* @return String
*/
private String replacePrefix(String path) {
if (path.startsWith(PATH_PREFIX)) {
return path.substring(path.indexOf(StringPool.SLASH, FROM_INDEX));
}
return path;
}
private Mono unauthorized(ServerHttpResponse resp, String msg) {
return ResponseUtil.webFluxResponseWriter(resp, MateConstant.JSON_UTF8, HttpStatus.UNAUTHORIZED, msg); }
@Override
public int getOrder() {
return MateConstant.MATE_UAA_FILTER_ORDER;
}
}
package vip.mate.gateway.service.impl;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.event.RefreshRoutesEvent;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionWriter;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
/**
* 动态更新路由网关service
* 1)实现一个Spring提供的事件推送接口ApplicationEventPublisherAware
* 2)提供动态路由的基础方法,可通过获取bean操作该类的方法。该类提供新增路由、更新路由、删除路由,然后实现发布的功能。
* @author pangu
*/
@Slf4j
@Service
@AllArgsConstructor
public class DynamicRouteServiceImpl implements ApplicationEventPublisherAware {
private final RouteDefinitionWriter routeDefinitionWriter;
/**
* 发布事件
*/
private ApplicationEventPublisher publisher;
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.publisher = applicationEventPublisher;
}
/**
* 删除路由
* @param id
* @return
*/
public String delete(String id) {
try {
log.info("gateway delete route id {}",id);
this.routeDefinitionWriter.delete(Mono.just(id));
return "delete success";
} catch (Exception e) {
return "delete fail";
}
}
/**
* 更新路由
* @param definition
* @return
*/
public String update(RouteDefinition definition) {
try {
log.info("gateway update route {}",definition);
this.routeDefinitionWriter.delete(Mono.just(definition.getId()));
} catch (Exception e) {
return "update fail,not find route routeId: "+definition.getId();
}
try {
routeDefinitionWriter.save(Mono.just(definition)).subscribe();
this.publisher.publishEvent(new RefreshRoutesEvent(this));
return "success";
} catch (Exception e) {
return "update route fail";
}
}
/**
* 增加路由
* @param definition
* @return
*/
public String add(RouteDefinition definition) {
log.info("gateway add route {}",definition);
routeDefinitionWriter.save(Mono.just(definition)).subscribe();
this.publisher.publishEvent(new RefreshRoutesEvent(this));
return "success";
}
}
参考:
blog.csdn.net/a745233700/…
www.bmabk.com/index.php/p…
blog.csdn.net/bufegar0/ar…
blog.csdn.net/joy_tom/art…
spring.io/projects/sp…
www.ityouknow.com/springcloud…
小知识:
Zuul1.x模型:
Servlet的生命周期?
Springcloud中所集成的Zuul版本,采用的是Tomcat容器,使用的是传统的Servlet IO处理模型。
servlet由servlet container进行生命周期管理。
- container启动时构造servlet对象并调用servlet init()进行初始化;
- container运行时接受请求,并为每个请求分配一个线程(一般从线程池中获取空闲线程)然后调用service()。
- container关闭时调用servlet destory()销毁servlet;
上述模式的缺点:
servlet是一个简单的网络IO模型,当请求进入servlet container时,servlet container就会为其绑定一个线程,在并发不高的场景下这种模型是适用的。
但是一旦高并发(比如抽风用jemeter压),线程数量就会上涨,而线程资源代价是昂贵的(上线文切换,内存消耗大)严重影响请求的处理时间。在一些简单业务场景下,不希望为每个request分配一个线程,只需要1个或几个线程就能应对极大并发的请求,这种业务场景下servlet模型没有优势
所以Zuul 1.X是基于servlet之上的一个阻塞式处理模型,即spring实现了处理所有request请求的一个servlet(DispatcherServlet)并由该servlet阻塞式处理处理。所以Springcloud Zuul无法摆脱servlet模型的弊端
GateWay模型:
docs.spring.io/spring/docs…
传统的Web框架,比如说:struts2,springmvc等都是基于Servlet API与Servlet容器基础之上运行的。
但是在Servlet3.1之后有了异步非阻塞的支持。 而WebFlux是一个典型非阻塞异步的框架,它的核心是基于Reactor的相关API实现的。相对于传统的web框架来说,它可以运行在诸如Netty,Undertow及支持Servlet3.1的容器上。非阻塞式+函数式编程(Spring5必须让你使用java8)
Spring WebFlux 是 Spring 5.0 引入的新的响应式框架,区别于 Spring MVC,它不需要依赖Servlet API,它是完全异步非阻塞的,并且基于 Reactor 来实现响应式流规范。
服务器托管,北京服务器托管,服务器租用 http://www.fwqtg.net
机房租用,北京机房租用,IDC机房托管, http://www.fwqtg.net