1.背景
1.1、网关的背景
在分布式微服务架构中,某个服务可以会有多个实例来去注册到注册中心,那么如何去调用如此多个的服务也成为了一个比较大的问题。
此时客户端去调用服务就会出现以下问题:
① 客户端访问地址配置问题。
② 多个服务的认证授权问题,造成鉴权认证功能重复冗余情况。
③ 服务访问量大造成的重构问题。
如何解决上面的问题呢?
微服务引入了 网关 的概念,网关为微服务架构的系统提供简单、有效且统一的 API 路由管理,作为系统的统一入口,提供内部服务的路由中转,给客户端提供统一的服务,可以实现一些和业务没有耦合的公用逻辑,主要功能包含认证、鉴权、路由转发、安全策略、防刷、流量控制、监控日志等。
原本的对应服务客户端去==直接调服务实例接口转变为统一走 gateway 网关来进行服务转发==:
api = Controller 接口,说 api 更高级!
理解:
因为每一个微服务都要运行在不同的端口上面,那么前端就需要访问不同的端口才能访问不同的服务,非常麻烦,那么就引入一个网关作为统一的入口,这样前端就可以只访问网关就可以访问到后端不同的微服务了!!!
网关 = 路由 = 统一的入口来进行服务发现和负载均衡!
1.2、是否有网关的对比
没有网关:客户端直接访问我们的微服务,会需要在客户端配置很多的 ip:port,如果 user-service 并发比较大,则无法完成负载均衡。
- 试想:若是某个服务实例采用集群,那么我们在进行负载均衡配置时难道也要逐一配置真实的服务地址嘛,那这就会出现很大的人力问题。
有网关:客户端访问网关,网关来访问微服务,(网关可以和注册中心整合,通过服务名称找到目标的 ip:prot)这样只需要使用服务名称即可访问微服务,可以实现负载均衡,可以实现 token 拦截,权限验证,限流等操作。
- 好处:使用网关就不需要在 nginx 负载均衡中配置大量的服务实例地址,而是只需要配置 gateway 网关的集群地址,某个请求通过 nginx 走到网关,接着 ==在网关进行 服务发现+负载均衡== 去访问某个服务的实例。
1.3、网关的技术实现
本章节介绍其中的 Gateway:是 Spring Cloud 官方提供的用来取代 zuul(netflix 公司)的新一代网关组件
Zuul 1.0 : Netflix 开源的网关,使用 Java 开发,基于 Servlet 架构构建,本质就是 web 组件 web 三大组件(监听器 过滤器 servlet)便于二次开发。因为基于 Servlet 内部延迟严重,并发场景不友好,一个线程只能处理一次连接请求。
- 性能:使用的是 BIO(Blocking IO),tomcat7.0 以前都是 BIO, 性能一般。
Zuul 2.0 : 采用 Netty 实现异步非阻塞编程模型,一个 CPU 一个线程,能够处理所有的请求和响应,请求响应的生命周期通过事件和回调进行处理,减少线程数量,开销较小。
- 性能:性能好采用的是 NIO,AIO 异步非阻塞,基于 spring5.x,springboot2.x 和 ProjectReactor 等技术。
GateWay : 是 Spring Cloud 的一个全新的 API 网关项目,替换 Zuul 开发的网关服务,基于 Spring5.0 + SpringBoot2.0 + WebFlux(基于高性能的 Reactor 模式响应式通信框架 Netty,异步非阻塞模型)等技术开发,性能高于 Zuul
Nginx+lua : 性能要比上面的强很多,使用 Nginx 的反向代码和负载均衡实现对 API 服务器的负载均衡以及高可用,lua 作为一款脚本语言,可以编写一些简单的逻辑,但是无法嵌入到微服务架构中。
Kong : 基于 OpenResty(Nginx + Lua 模块)编写的高可用、易扩展的,性能高效且稳定,支持多个可用插件(限流、鉴权)等,开箱即可用,只支持 HTTP 协议,且二次开发扩展难,缺乏更易用的管理和配置方式。
2.认识 Springcloud Gateway
2.1、简介
Spring Cloud Gateway 是 Spring Cloud 的一个全新的 API 网关项目,目的是为了替换掉 Zuul1。
技术选型:基于 Spring5.0 + SpringBoot2.0 + WebFlux(基于高性能的 Reactor 模式响应式通信框架 Netty,异步非阻塞模型)等技术开发。
性能方面:性能高于 Zuul,官方测试,Spring Cloud GateWay 是 Zuul 的 1.6 倍 ,旨在为微服务架构提供⼀种简单有效的统⼀的 API 路由管理方式。
特点:Spring Cloud Gateway 里明确的区分了 Router 和 Filter,并且一个很大的特点是内置了非常多的开箱即用功能,并且都可以通过 SpringBoot 配置或者手工编码链式调用来使用。
- 比如内置了 10 种 Router,使得我们可以直接配置一下就可以随心所欲的根据 Header、或者 Path、或者 Host、或者 Query 来做路由。
- 比如区分了一般的 Filter 和全局 Filter,内置了 20 种 Filter 和 9 种全局 Filter,也都可以直接用。当然自定义 Filter 也非常方便。
2.2、gateway 的三大核心概念
三个核心:路由、断言、过滤器。
一、路由(Route):能够与注册中心来结合作动态路由。
是 GateWay 中最基本的组件之一,表示一个具体的路由信息载体,主要有一个 ID、一个目标 URI、一组断言和一组过滤
器来定义,具体如下:
- id:路由唯一标识,区别于其他的 route。
- url:路由指向的目的地 URL,客户端请求最终被转发到的微服务。
- order:用于多个 Route 之间的排序,数值越小越靠前,匹配优先级越高。
- predicate:断言的作用是进行条件判断,只有断言为 true,才执行路由。
- filter: 过滤器用于修改请求和响应信息。
二、断言(Predicate):返回一个 bool 类型,用于表示在不同状态情况下,该请求是否符合要求。
输入的类型是一个 ServerWebExchange,可以使用其来匹配 HTTP 请求的任何内容,例如 headers、cookie 等等。
三、过滤器(filter):在 gateway 中分为两种类型 filter:①Gateway filter。②Global filter。
- 效果:能够对请求和响应进行修改处理。
- 两种路由各自用途:
- 一个是针对某一个路由(路径)的 filter,例如对某一个接口做限流。
- 一个是针对全局的 filter token ip 黑名单。
2.3、gateway 的工作流程
执行流程如下:
1、Gateway Client 向 Spring Cloud Gateway 发送请求,请求首先会被 HttpWebHandlerAdapter 进行提取组装成网关上下文。
3、此时网关的上下文会传递到 DispatcherHandler ,它负责将请求分发给 RoutePredicateHandlerMapping。
4、RoutePredicateHandlerMapping 负责路由查找,并根据路由断言判断路由是否可用。
5、如果过断言成功,由 FilteringWebHandler 创建过滤器链并调用。
6、通过特定于请求的 Fliter 链运行请求,Filter 被虚线分隔的原因是 Filter 可以在发送代理请求之前(pre)或之后(post)运行逻辑。
7、执行所有 pre 过滤器逻辑。然后进行代理请求。发出代理请求后,将运行“post”过滤器逻辑。
8、处理完毕之后将 Response 返回到 Gateway 客户端。
针对于代理请求 pre 与 post 的分别应用场景:
- Filter 在 pre 类型的过滤器可以做参数效验、权限效验、流量监控、日志输出、协议转换等。
- Filter 在 post 类型的过滤器可以做响应内容、响应头的修改、日志输出、流量监控等
2.4、实际应用的服务架构
若是想要我们的网关达到高可用,那么就需要进行部署集群,并在 gateway 网关上层配置一个负载均衡层:
2.5、Nginx 与 Gateway 的区别
Nginx:在做路由,负载均衡,限流之前,都有修改 nginx.conf 的配置文件,把需要负载均衡,路由,限流的规则加在里面。
- 需要手动配置。
gateway:gateway 自动的负载均衡和路由,gateway 和 eureka 高度集成,实现自动的路由,和 Ribbon 结合,
实现了负载均衡(lb),gateway 也能轻易的实现限流和权限验证。
区别:
1、Nginx 需要去自行配置路由、负载均衡等规则;gateway 有动态路由,可与注册中心结合使用。
2、Nginx(c 语言)比 gateway(gateway)的性能高一点。
3、Nginx 是服务器级别的;Gateway 是项目级别的。
比较合适的搭配效果如下:
项目版本:SpringBoot 2.3.12.RELEASE、SpringCloud Hoxton.SR12。
3.实战 1:搭建 SpringCloud Gateway 服务
3.1、搭建基础 Gateway 服务,实现路由转发(暂无注册中心)
本小节只需要使用一个 gateway 与一个服务实例即可。
实现目标:网关实现路由转发的效果。(之后小结会进行动态路由的实现)
login-service:登录模块服务
当前该服务仅仅只要引入 web 模块即可,当前还没有涉及到注册中心。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
controller/LoginController.java:
package com.changlu.loginservice.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.UUID;
/**
* @Description:
* @Author: changlu
* @Date: 8:20 PM
*/
@RestController
public class LoginController {
@GetMapping("/doLogin")
public String doLogin() {
return UUID.randomUUID().toString();
}
}
gateway-service:网关服务
依赖配置:对应 springboot、springcloud 版本号在三章节下有说明
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
配置文件:application.yaml
server:
port: ${SERVER_PORT:81} # 默认是81端口,后面可以通过命令行读取参数 -DSERVER_PORT=82
spring:
application:
name: gateway-server
cloud:
gateway:
enabled: true # 默认开启,只要加了网关依赖
routes:
- id: login-service-route # 路由id,保持唯一
uri: localhost:8081 # uri
predicates:
- Path=/doLogin # 匹配路径规则 只要你浏览器中输入的urlPath匹配上了/doLogin,就会往这里的 uri 转发 并且将路径带上
实现:
① 开启网关 (其实是默认开启的)。
② 编写路由。(若是要进行转发我们只需要配置好 id、uri 以及断言匹配路径)
id:表示的该组路由的 id,需要是唯一的。
uri:表示匹配到的路由进行转发的地址。
predicates:当前是进行一个路径匹配的 uri 接口。
目前的话我们无需编写任何代码,就可以使用一个路由转发的效果,接下来我们来进行测试一下!
测试:访问路径 localhost:81/doLogin,即可在 gateway 中进行转发到我们的 login-service 服务实例上。
注意:当前仅仅是路由转发指定的地址,并没有去注册中心拉取服务实例进行访问!
3.2、Gateway 的两种路由配置方式(暂无注册中心)
方式一:代码配置方式
可参考官方案例:https://spring.io/projects/spring-cloud-gateway#overview
实现目标:路由转发到百度一下
可以看到百度的路由地址是:https://www.baidu.com/s?wd=123
思路:将/s 即之后的内容转发到指定的百度网址,实际上与 3.1 中配置的大体参数类似
package com.changlu.config;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @Description:
* @Author: changlu
* @Date: 1:15 PM
*/
@Configuration
public class RouteConfig {
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
//路由
.route("baidu_route", r -> r.path("/s").uri("https://www.baidu.com/"))
.build();
}
}
ok,此时我们来测试一下:localhost:81/s?wd=123
方式二:yaml 配置方式
实际上就是 3.1 中进行配置的内容:
spring:
application:
name: gateway-server
cloud:
gateway:
enabled: true # 默认开启,只要加了网关依赖
routes:
- id: login-service-route # 路由id,保持唯一
uri: localhost:8081 # uri
predicates:
- Path=/doLogin # 匹配路径规则 只要你Path匹配上了/doLogin 就往 uri 转发 并且将路径带上
- id: user-service-route # 路由id,保持唯一
uri: localhost:8082 # uri
predicates:
- Path=/info/** # 匹配路径规则 只要你Path匹配上了/doLogin 就往 uri 转发 并且将路径带上
对于下面的/info/,就是会匹配所有前缀为/info/*的内容
例如访问 localhost:81/info/doLogin,实际上就会进行转发 localhost:8082/info/doLogin
3.3、搭配注册中心的方式实现动态路由
产生的问题:
1.有可能一个服务里面有 100 个接口,如果全都配置在 yml 中,需要写的东西就太多了!如何解决?
方案:
predicates:
- Path=/info/** #只需要写一个,在这个服务的所有接口前面都加上 /info/前缀
2.有可能很多的微服务里面都有 doLogin 这个方法,那如何进行区分呢,如何实现负载均衡?
这时必须结合 Eureka!实现动态路由!
我们实现了能够去不同的服务里面调用 doLogin
并且在增加带有 doLogin 的服务时,也不需要重启网关模块了,直接写那个服务名字即可访问到对应服务
动态路由:
结合注册发现中心,拉取服务列表,在访问具体的服务上面的 api 时,带上服务名称即可
3.3.1 配置过程
之前 3.1 中案例并没有搭配注册中心,我们就以 3.1 中的案例来进行集成实现!
同样是对前两个进行改造:
两个服务都添加如下依赖及配置:
① 引入依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
② 配置 application.yaml 添加 eureka 连接信息:
# 配置eureka
eureka:
client:
service-url:
defaultZone: localhost:8761/eureka
instance:
hostname: localhost
instance-id: ${
eureka.instance.hostname}:${
spring.application.name}:${
server.port}
③ 开启服务发现:在启动器类上添加
@EnableEurekaClient //开启服务注册
最后在``gateway-server`服务的 yaml 配置里进行开启动态路由:
# 服务发现相关配置
discovery:
locator:
enabled: true # 开启动态路由 开启通过应用名称找到服务的功能
lower-case-service-id: true # 开启服务名称小写,因为在eureka中默认服务名是大写的
当前我们已经实现了动态路由了,完全就只需要进行配置即可!
3.3.2 测试动态路由
我们来启动网关、生产者服务以及 eureka 注册中心:
注册中心的代码案例使用之前博文中的 eureka 案例:
在对 login-service 与 gateway-server 都开启了服务注册之后,以及开启了 gateway-server 的动态路由,我们就可以来实现根据服务名调用指定注册中心中的服务实例了。
再此之前我们进行路由代码配置访问的是:localhost:81/doLogin
==针对于动态路由,我们在/doLogin 前添加一个服务名称==,例如在 eureka 中心中 login-service 注册的服务名为 login-service:localhost:81/login-service/doLogin
测试一下:ok 能够进行测试访问
3.4、非动态路由的配置的方式实现动态路由效果
之前 3.1、3.2 节是我们进行静态绑定的,那么我们如何来实现使用静态绑定的方式来达到动态路由的效果呢?
那么我们只需要对对应 uri 来进行操作即可:
将原本指明服务 ip 地址以及 port 端口的更改为==负载均衡协议 lb://服务名==
# uri: localhost:8081 # 普通的uri配置,写上对应的服务的端口号
uri: lb://login-service # lb为负载均衡协议,写对应服务的名字即可,并且实现负载均衡
测试一下:
没得问题!
但是这样缺少灵活性,只能在配置文件中指定访问哪个服务,而不能在 url 中指定访问!
4.实战 2:搭建 Gateway 集群
4.1、搭建详细过程
目标效果:访问 nginx,通过 nginx 来进行负载均衡转发请求到集群 gateway 中,此时gateway 里同样去搭配注册中心进行服务发现来进行负载均衡访问服务实例!下面就开始吧。——>两次负载均衡
准备:nginx 服务器+gateway 两个不同端口(实际上是不同 ip 地址)服务+生产者服务实例。
生产者服务实例:使用的是 login-service,也就是 3.1 节中的对外提供了一个接口。
gateway 服务实例准备
直接使用的是 3.1 章节中的 gateway,如何创建不同端口的多实例呢?
我们针对 yaml 配置文件来编写可接收命令传参:
server:
port: ${SERVER_PORT:81} # 默认是81端口,可以通过命令行读取参数 -DSERVER_PORT=82
-DSERVER_PORT=82
接着我们来进行启动服务实例:
这样就使用了一个模块,在不同的端口上运行了两个网关实例!
nginx 配置
下载地址:http://nginx.org/en/download.html
我们打开 nginx 文件夹中的 nginx.conf 来进行编辑:
- 第一个框+第二个框:主要目的是为了能够查看到访问请求进行负载均衡的一个日志情况。
- 第三个框+第四个框:进行负载均衡的配置。
配置如下:
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"'
'$connection $upstream_addr '
'upstream_response_time $upstream_response_time request_time $request_time ';
access_log logs/access.log main;
upstream www.gateway.com {
server localhost:81;
server localhost:82;
}
server {
location / {
# root html;
# index index.html index.htm;
# 代理服务地址
proxy_pass http://www.gateway.com;
}
}
nginx 相关的启动、关闭命令:
start nginx # 启动,下载好之后到指定目录下执行启动命令即可
nginx -s quit # 关闭nginx
nginx -s reload # 优雅重启
# 若是想要停止服务还可以使用这个命令或者使用任务管理器找到nginx.exe来关闭
taskkill /IM nginx.exe /F # 关闭所有正在启动的nginx服务
4.2、测试集群
接下来我们来访问 nginx 的 80 端口:localhost/doLogin
那么我们如何来看负载均衡访问服务器的地址呢?
查看 nginx 的日志:``access.log`即可
可以看到默认的 nginx 负载均衡是轮训:
注意:默认的日志打印内容是没有访问服务器的地址的,在前面 nginx 中我是有进行自主配置加上打印的服务器地址这里才会显示的。
核心:Predicate 就是为了实现一组匹配规则,让请求过来找到对应的 Route 进行处理。
5.Predicate 断言工厂
5.1、认识断言
在 gateway 启动时会去加载一些路由断言工厂**(判断一句话是否正确 一个 boolean 表达式**) ,例如我们 3.1 中搭建的案例在启动时就会出现如下的一些断言信息:
本质:满足条件的返回 true 放行,不满足的 false 进行拦截。
介绍:Spring Cloud Gateway 将路由作为 Spring WebFlux HandlerMapping 基础架构的一部分进行匹配。Spring Cloud Gateway 包括许多内置的路由断言工厂。
所有这些断言都与 HTTP 请求的不同属性匹配。您可以将多个路由断言可以组合使用。
源码分析:Spring Cloud Gateway 创建对象时,使用 RoutePredicateFactory 创建 Predicate 对象,Predicate 对象可以赋值给 Route。
5.2、断言详细配置
spring:
application:
name: gateway-server
cloud:
gateway:
enabled: true # 默认开启,只要加了网关依赖
routes:
- id: login-service-route # 路由id,保持唯一
# uri: localhost:8081 # uri
uri: lb://login-service # 实现负载均衡
predicates: #配置断言规则
- Path=/doLogin # 匹配路径规则 只要你Path匹配上了/doLogin 就往 uri 转发 并且将路径带上
- After=2020-01-20T17:42:47.789-07:00[Asia/Shanghai] #此断言匹配发生在指定 日期时间之后的请求,ZonedDateTime dateTime=ZonedDateTime.now()获得
- Before=2020-06-18T21:26:26.711+08:00[Asia/Shanghai] #此断言匹配发生在指定 日期时间之前的请求
- Between=2020-06-18T21:26:26.711+08:00[Asia/Shanghai],2020-06-18T21:32:26.711+08:00[Asia/Shanghai] #此断言匹配发生在指定日期时间之间的请求
- Cookie=name,xiaobai #Cookie 路由断言工厂接受两个参数,Cookie 名称和 regexp(一 个 Java 正则表达式)。此断言匹配具有给定名称且其值与正则表达式匹配的 cookie
- Header=token,123456 #头路由断言工厂接受两个参数,头名称和 regexp(一个 Java 正则表达式)。此断言与具有给定名称的头匹配,该头的值与正则表达式匹配。
- Host=**.bai*.com:* #主机路由断言工厂接受一个参数:主机名模式列表。该模式是一 个 ant 样式的模式。作为分隔符。此断言匹配与模式匹配的主机头
- Method=GET,POST #方法路由断言工厂接受一个方法参数,该参数是一个或多个参数: 要匹配的 HTTP 方法
- Query=username,cxs #查询路由断言工厂接受两个参数:一个必需的 param 和一个 可选的 regexp(一个 Java 正则表达式)。
- RemoteAddr=192.168.1.1/24 #RemoteAddr 路由断言工厂接受一个源列表(最小大小 1), 这些源是 cidr 符号(IPv4 或 IPv6)字符串,比如 192.168.1.1/24(其中 192.168.1.1 是 IP 地址,24 是子网掩码)。
其他额外的包含有权重属性的配置:
- 80%的请求,由 https://weighthigh.org 这个 url 去处理
- 20%的请求由 https://weightlow.org 去处理
spring:
application:
name: gateway-server
cloud:
gateway:
enabled: true
routes:
- id: weight_high
uri: https://weighthigh.org
predicates:
- Weight=group1, 2 # 权重
- id: weight_low
uri: https://weightlow.org
predicates:
- Weight=group1, 8 # 权重
5.3、实战 3:配置一个 After 断言
效果:指定某个接口只能在指定时间后才能够进行访问,否则无法访问,报出 404 异常。
配置内容如下:- After=2022-07-29T17:23:21.719+08:00[Asia/Shanghai]
可以看到当前时间是四点多,配置 After 是五点多,也就是说这个接口在五点多才能够访问,再此之前不能够访问!
测试效果:
5.4、自定义断言器
自定义步骤
1、编写一个断言工厂类:注意工厂类的名字尽量为 xxxRoutePredicateFactory,因为之后配置文件要进行使用
package com.changlu.config;
import lombok.Data;
import org.springframework.cloud.gateway.handler.predicate.AbstractRoutePredicateFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import java.util.function.Predicate;
/**
* @Description:
* @Author: changlu
* @Date: 4:38 PM
*/
@Component
public class CheckAuthRoutePredicateFactory extends AbstractRoutePredicateFactory<CheckAuthRoutePredicateFactory.Config> {
public CheckAuthRoutePredicateFactory() {
super(Config.class);
}
//此时Config也就是自定义配置的一些参数
@Override
public Predicate<ServerWebExchange> apply(Config config) {
return exchange -> {
System.out.println("当前进入到CheckAuthRoutePredicateFactory:" + config.getName());
return config.getName().equals("changlu");
};
}
@Data
static class Config {
private String name;
}
}
2、yaml 来进行配置
- -name 表示的是工厂名称。
- args.name:其中的 name 就是工厂参数。
- name: CheckAuth #自定义路由断言工厂的名称xxxRoutePredicateFactory,这个xxx就是在这里指明
args:
name: haojiahuo #传入到自定义路由断言工厂的参数,故意设置为不一样
测试
果然由于配置文件的 name 与在断言方法类中的值不一致,此时该接口就访问不到了
将 name 修改为 changlu,再次尝试一下:
- name: CheckAuth
args:
name: changlu
6.Filter 过滤器工厂
6.1、介绍
介绍:gateway 里面的过滤器和 Servlet 里面的过滤器,功能差不多,路由过滤器可以用于修改进入 Http 请求和返回 Http 响应。
分类:
1.按照生命周期:pre(在业务逻辑前)、post(在业务逻辑后)。
Filter 在 pre 类型的过滤器可以做参数效验、权限效验、流量监控、日志输出、协议转换、限流、token 认证等。
Filter 在 post 类型的过滤器可以做响应内容、响应头的修改、日志输出、流量监控等。
2.按照种类区别:路由过滤器(某个路由单独使用)、全局过滤器(所有路由)。
路由过滤器(GatewayFilter):需要配置某个路由,才能过滤。如果需要使用全局路由,需要配置 Default。
全局过滤器(GlobalFilter):不需要配置路由,系统初始化作用到所有路由上 。
官网
官网-gatewayfilter-factories:包含 31 种单一路由过滤器。
官网-global-filters:包含 9 种全局路由过滤器。
6.2、自定义全局过滤器
自定义过程
我们基于之前的 3.1 案例的 gateway-server 来进行自定义。
自定义全局 filter 实现了一个 GlobalFilter(过滤方法)、Ordered(执行顺序):
package com.changlu.filter;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.HashMap;
/**
* @Description: 自定义全局过滤器
* @Author: changlu
* @Date: 9:17 PM
*/
@Component
public class MyGlobalFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//获取请求体以及请求对象
ServerHttpRequest request = exchange.getRequest();
// HttpServletRequest 这个是web里面的
// ServerHttpRequest 这个是webFlux里面 响应式里面的
ServerHttpResponse response = exchange.getResponse();
//通过请求对象可以拿到请求的一系列内容
String path = request.getURI().getPath();//uri路径
System.out.println("path:" + path);
HttpHeaders headers = request.getHeaders();//请求头
System.out.println("headers:" + headers);
String name = request.getMethod().name();//请求方法名,也就是对应ip:port/xxx,这个/xxx
System.out.println("method name:"+ name);
String ip = request.getHeaders().getHost().getHostString();//获取到ip主机名
System.out.println("ip:" + ip);
//来进行测试响应数据
// 用了微服务 肯定是前后端分离的 前后端分离 一般前后通过 json
// {"code":200,"msg":"ok"}
//1、设置响应头
response.getHeaders().set("content-type", "application/json;charset=utf-8");
//2、响应结果集封装
HashMap<String, Object> result = new HashMap<>();
result.put("code", HttpStatus.UNAUTHORIZED.value());
result.put("msg", "暂未授权");
ObjectMapper objectMapper = new ObjectMapper(); //jackson工具类提供的
byte[] data = new byte[0];
try {
data = objectMapper.writeValueAsBytes(result);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
//通过使用buffer工厂类来将其转为一个数据包(底层是基于netty,该对象底层是nio的bytebuffer)
DataBuffer wrap = response.bufferFactory().wrap(data);
//return response.writeWith(Mono.just(wrap));
//放行过滤器
return chain.filter(exchange);
}
//越小优先级越高
@Override
public int getOrder() {
return 0;
}
}
其中放行是执行 chain 的方法:
return chain.filter(exchange);
若是直接拦截结束,则是对 response 进行写数据:
//这个wrap包装成netty中的DataBuffer
return response.writeWith(Mono.just(wrap));
测试
1.直接在 gateway 中进行拦截响应:
2.在 gateway 中放行的效果:
此时可以直接访问对应 login-service 接口:
该过滤器打印的一些 request 请求对象的信息:
6.3:实战 4:实现一个 ip 拦截的过滤器
思路:同样也是在 Gateway 网关中添加一个全局过滤器组件,如果该 ip 在我们禁止的名单中就拦截它。
package com.changlu.filter;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @Description: IP检查过滤器
* @Author: changlu
* @Date: 8:41 AM
*/
@Component
public class IPCheckFilter implements GlobalFilter, Ordered {
/**
* 网关的并发比较高 不要在网关里面直接操作mysql
* 后台系统可以查询数据库 用户量 并发量不大
* 如果并发量大 可以查redis 或者 在内存中写好
*/
private static final List<String> BLACK_LIST = Arrays.asList("127.0.0.1", "192.168.1.1");
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//获取到请求对象化
ServerHttpRequest request = exchange.getRequest();
String ip = request.getHeaders().getHost().getHostString();
//若是在集合中出现该ip,那么此时就拦截响应(一般黑名单可以存储在数据库中 也可以存储的redis里)
//如果不是在这个集合中的,就放行
if (!BLACK_LIST.contains(ip)) {
chain.filter(exchange); //放行
}
//若是存在就进行拦截,并响应
ServerHttpResponse response = exchange.getResponse();
response.getHeaders().set("content-type", "application/json;charset=utf-8");
Map<String, Object> result = new HashMap<>();
result.put("code", 438);
result.put("msg", "你已被拉黑,无法访问");
ObjectMapper objectMapper = new ObjectMapper();
byte[] data = new byte[0];
try {
data = objectMapper.writeValueAsBytes(result);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
DataBuffer wrap = response.bufferFactory().wrap(data);
return response.writeWith(Mono.just(wrap));
}
@Override
public int getOrder() {
return 1;
}
}
测试一下:
可以看到 localhost 是在拦截范围内的,所以 gateway 会进行拦截响应:
6.4、实战 5:在网关中实现 token 认证校验
在实战 5 中,我们完成的就是下图的第 7 步骤,也就是 token 进行认证校验是否合法来进行放行或直接响应!
说明:本章节会在 login-service 中完善 doLogin 接口,接着在 gateway 服务里添加一个认证 token 过滤器,并新建一个 user-service 并在其中添加一个接口对外使用。
注意:本章节的重点是在gateway 中实现 token 认证来达到放行 or 错误响应,并不是在登录接口存储用户信息这些细节上,对于 token 生成、校验以及用户认证都仅仅只是做了简单的实现。
1.login-service(增加登录接口)
domain/user.java:
package com.changlu.loginservice.domain;
import lombok.Data;
import java.io.Serializable;
/**
* @Description: 用户实体类
* @Author: changlu
* @Date: 9:20 AM
*/
@Data
public class User implements Serializable {
private String username;
private String password;
}
1、硬编码指定一个 token。
private static final String token = "700d7a8d-262a-447a-8254-9dd9ead6a0e2";
2、添加一个 doLogin 接口,来用于获取 token。
@PostMapping("/doLogin")
public String doLogin(@RequestBody User user) {
System.out.println("dologin进行登录:" + user);
//数据库进行认证,这里的话直接返回一个token
return token;
}
2.user-service 模块(新增,添加一个对外界接口)
说明:该模块主要是用于测试之后携带 token 的接口是否能够通过 gateway 认证并进行转发。
但是认证的工作是在网关中进行的!
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.12.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Hoxton.SR12</spring-cloud.version>
</properties>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
配置文件:application.yaml
server:
port: 8082
spring:
application:
name: user-service
# 注册目标
eureka:
client:
service-url:
defaultZone: localhost:8761/eureka
instance:
hostname: localhost
instance-id: ${
eureka.instance.hostname}:${
spring.application.name}:${
server.port}
提供一个用户接口:仅仅是进行简单的用户返回。
package com.changlu.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
/**
* @Description:
* @Author: changlu
* @Date: 9:16 AM
*/
@RestController
@RequestMapping("/user")
public class UserController {
@GetMapping
public Map<String, Object> getUser() {
Map<String, Object> result = new HashMap<>();
result.put("code", 200);
result.put("msg", "成功获取到用户信息");
return result;
}
}
3.gateway-server 模块(添加 token 认证过滤器)
1、添加一个 token 过滤器
package com.changlu.filter;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
/**
* @Description: token检查过滤器
* @Author: changlu
* @Date: 9:25 AM
*/
@Component
public class TokenCheckFilter implements GlobalFilter, Ordered {
private static final String token = "700d7a8d-262a-447a-8254-9dd9ead6a0e2";
private static final List<String> WHITE_PATH = Arrays.asList("/doLogin");
/**
* 流程:
1、路径检测(是否放行)。
2、请求头token获取。
3、校验:放行or直接响应
* @param exchange
* @param chain
* @return
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
//必要的一步:放行一些公开接口(如果是要登录的话,就没必要拦截了,因为登录肯定是本来没有token,没办法检验)
String path = request.getURI().getPath();
if (WHITE_PATH.contains(path)) {
return chain.filter(exchange);
}
//从请求头中获取到Authorization(即token)
List<String> authorization = request.getHeaders().get("Authorization");
if (!ObjectUtils.isEmpty(authorization)) {
String token = authorization.get(0);
//去掉前缀"bearer "
token = token.replaceFirst("Bearer ", "");
//token校验,成功放行(实际上会进行token解析取到uuid来从redis中获取,这里简单来表示一下)
if (TokenCheckFilter.token.equals(token)) {
return chain.filter(exchange);
}
}
//失败进行错误响应
ServerHttpResponse response = exchange.getResponse();
response.getHeaders().set("content-type", "application/json;charset=utf-8");
HashMap<String, Object> result = new HashMap<>();
result.put("code", HttpStatus.UNAUTHORIZED.value());
result.put("msg", "暂未授权");
ObjectMapper objectMapper = new ObjectMapper();//jackson工具类
byte[] data = new byte[0];
try {
data = objectMapper.writeValueAsBytes(result);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
DataBuffer wrap = response.bufferFactory().wrap(data);
return response.writeWith(Mono.just(wrap));
}
@Override
public int getOrder() {
return 2; //过滤的顺序在第三位
}
}
2、编写配置文件,新增一个路由
spring:
application:
name: gateway-server
cloud:
gateway:
enabled: true # 默认开启,只要加了网关依赖
routes:
# 用户服务路由
- id: user-service-route
uri: lb://user-service
predicates:
- Path=/user
#会去user-service中查找/user的方法!(ip和port去eureka中查找)
测试
我们启动这四个模块,分别是:注册中心、网关、登录服务、用户服务。
来启动服务,以及查看一下 eureka 的注册中心服务注册情况:
接下来就可以开始进行测试了:我准备好两个接口
① 测试 doLogin 接口是否能够放行并返回 token
② 测试用户服务接口
首先添加一下 token,接着来发送请求
那我们来故意写错 token 来发送一下:
7.实战系列
7.1、实战 6:集成 Redis 实现请求限流
7.1.1、认识限流
通俗的说,限流就是限制一段时间内,用户访问资源的次数,减轻服务器压力,限流大致分为两种:
- IP 限流(5s 内同一个 ip 访问超过 3 次,则限制不让访问,过一段时间才可继续访问)
- 请求量限流(只要在一段时间内(窗口期),请求次数达到阀值,就直接拒绝后面来的访问了,过一段时间才可以继续访问)(粒度可以细化到一个 api(url),一个服务)
7.1.2、限流模型
介绍限流模型
限流模型:漏斗算法,令牌桶算法,窗口滑动算法,计数器算法。
常用的模型分类有两种:
时间模型
固定窗口模型:timeline 按照固定间隔分窗口,每个窗口有一个独立计数器,每个计数器统计窗口内的 qps,如果达到阈值则拒绝服务。
这是最简单的限流模型,但是缺点比较明显,当在临界点出现大流量冲击,就无法满足流量控制。
滑动窗口模型:滑动时间模型会将每个窗口切分成 N 个子窗口,每个子窗口独立计数。
这样用 w1+w2 计数之和来做限流阈值校验,就可以解决此问题。
桶模型
令牌桶:系统会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。
- 解决了在实际上的互联网应用中,流量经常是突发性的问题。
漏桶:水(请求)先进入到漏桶里,漏桶以一定的速度出水,当水流入速度过大会直接溢出,可以看出漏桶算法能强行限制数据的传输速率。
本章实战说明
本章节的话就使用 Gateway 内置的一个限流过滤器 RequestRateLimiterGatewayFilterFactory:
也就是令牌桶限流模型:入不敷出,拒绝服务
1)所有的请求在处理之前都需要拿到一个可用的令牌才会被处理;
2)根据限流大小,设置按照一定的速率往桶里添加令牌;
3)桶设置最大的放置令牌限制,当桶满时、新添加的令牌就被丢弃或者拒绝;
4)请求达到后首先要获取令牌桶中的令牌,拿着令牌才可以进行其他的业务逻辑,处理完业务逻辑之后,将令牌直接
删除;
5)令牌桶有最低限额,当桶中的令牌达到最低限额的时候,请求处理完之后将不会删除令牌,以此保证足够的限流;
7.1.3、Gateway 结合 redis 实现请求量限流(Gateway 内置限流令牌桶实现)
集成过程
注意:Spring Cloud Gateway 已经内置了一个 RequestRateLimiterGatewayFilterFactory,该过滤器是针对于某个路由的,并不是全局过滤器。
1、添加 redis 依赖
<!-- redis依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
2、指定限流的内容:ip 或接口
config/RequestLimitConfig.java:
package com.changlu.config;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import reactor.core.publisher.Mono;
/**
* @Description: 请求限流配置类
* @Author: changlu
* @Date: 10:34 AM
*/
@Configuration
public class RequestLimitConfig {
//针对某一个ip地址来进行限流(例如:localhost)
@Bean(name = "ipKeyResolver") //bean的名字默认就是方法名
@Primary //主候选的,加此注解可以避免不能同时共存两个bean
public KeyResolver ipKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getHeaders().getHost().getHostString());
}
//针对某一个接口uri(api)来进行限流(例如:/doLogin)
//gateway = api网关(api就是接口)
@Bean
public KeyResolver apiKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getPath().value());
}
}
3、配置文件为指定的路由配置 filter:
配置文件:application.yaml
# redis参数配置
redis:
host: localhost
port: 6379
database: 0
password: 123456
# 配置路由
filters:
- name: RequestRateLimiter #必须写这个,因为我们用的就是这个限流器(过滤器)
args:
key-resolver: "#{@ipKeyResolver}" #通过spel表达式取到ico容器的bean里面的值!
#换成api限流:
#key-resolver: '#{@apiKeyResolver}'
redis-rate-limiter.replenishRate: 1 #令牌每秒填充速度(实际中要大一些,一般500)
redis-rate-limiter.burstCapacity: 3 #桶的总大小(实际中要大一些,一般3000)
redis-rate-limiter.requestedTokens: 1 #默认是1,每次请求消耗的令牌数
LoginService 的方法:
@RestController
@CrossOrigin // 加上这个注解之后 这个controller里面的方法就可以直接被访问了
public class LoginController {
@Autowired
public StringRedisTemplate redisTemplate;
@GetMapping("doLogin")
@CrossOrigin
public String doLogin(String name, String pwd) {
System.out.println(name);
System.out.println(pwd);
// 这里假设去做了登录验证
User user = new User(1, name, pwd, 18);
// token
String token = UUID.randomUUID().toString();
// 存起来
redisTemplate.opsForValue().set(token, user.toString(), Duration.ofSeconds(7200));
return token;
}
}
测试
使用 jmeter(或者 postman 或者在浏览器中按 f5)来进行测试:
若是请求失败,默认就会返回响应码为 429。
429 就是请求限制的意思!
看一下 redis 中存储的参数:
下面两个都是无效的
我们也可以换之前配置指定的另一个参数也就是接口名,此时 redis 中存储的如下:
7.2、实战 7:Gateway 集成跨域配置
对于 ajax 同源策略,例如前端的访问端口与后端访问的端口不一致时,也就会产生跨域问题。
下面都是全局跨域的配置:
方式一:参数配置
spring:
cloud:
gateway:
globalcors:
cors-configurations:
"[/**]":
allowedOrigins: "*"
allowedMethods:
- GET
- POST
- DELETE
- PUT
- OPTION
方式二:通过 java 配置过滤器
@Configuration
public class CorsConfig {
@Bean
public CorsWebFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedMethod("*");
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
source.registerCorsConfiguration("/**", config);
return new CorsWebFilter(source);
}
}
测试
准备一个 ajax 的跨域问题:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<input type="button" value="触发按钮" onclick="getData()" />
<script src="http://apps.bdimg.com/libs/jquery/1.9.1/jquery.min.js"></script>
<script>
function getData() {
//ajax请求
$.get("localhost:81/doLogin", function (data) {
alert(data);
});
}
</script>
</body>
</html>
配置完跨域后再来进行测试:
推荐文章:
[3]. Spring Cloud Gateway-自定义断言及过滤器
[4]. spring-cloud-gateway 11 限流 RequestRateLimiterGatewayFilterFactory