目录
1. 过滤器的作用
2. Spring Cloud Gateway 过滤器的类型
2.1 内置过滤器
2.1.1 AddResponseHeader
2.1.2 AddRequestHeader
2.1.3 PrefixPath
2.1.4 RequestRateLimiter
2.1.5 Retry
2.2 自定义过滤器
1. 过滤器的作用
过滤器通常用于拦截、处理或修改数据流和事件流,在数据流中执行特定的操作或转换。
过滤器主要在以下几个方面发挥作用:
-
功能扩展和定制:过滤器允许您自定义和扩展网关的功能,以满足特定需求,如请求和响应的修改、路由规则的动态配置等。
-
数据校验和过滤:通过过滤器,您可以检查、验证和过滤传入或传出的数据,确保请求和响应的合法性和一致性。
-
安全保护:过滤器可以用于实施安全策略,如认证、授权、防止攻击等,以增强网关的安全性。
-
性能优化:通过过滤器,您可以对请求和响应进行性能优化,如缓存、压缩、请求路由的智能选择,以提高网关的性能。
-
统一处理:过滤器允许您在网关层面执行共享的处理逻辑,如日志记录、监控、审计等,以确保整个微服务体系的一致性和可维护性。
-
逻辑复用:通过过滤器,您可以将一些常见的操作抽象出来,以实现逻辑的复用,减少重复代码和维护工作。
2. Spring Cloud Gateway 过滤器的类型
Spring Cloud Gateway 过滤器可以分为两大类:
1. 内置过滤器
- 局部的内置过滤器
- 全局的内置过滤器
2. 自定义过滤器
2.1 内置过滤器
内置过滤器常见的有以下几种:
- AddResponseHeader
- AddRequestHeader
- AddRequestParameter(和 AddRequestHeader 相似)
- PrefixPath
- RequestRateLimiter
- Retry
Spring Cloud Gateway 过滤器常见有这么几种,实际上它有30多种,可以借助官方文档加以了解:Spring Cloud Gateway
过滤器又分为前置过滤器和后置过滤器:
在目标方法返回之前执行的过滤器就叫做前置过滤器(AddRequestXXX),在目标方法返回之后执行的过滤器就叫做后置过滤器。(AddResponseXXX)
前置工作:准备 user-service 和 order-service 两个模块,并且配置好 naocs 连接信息。
① user-service:创建一个 controller
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired // 获取动态端口
private ServletWebServerApplicationContext context;
@RequestMapping("/getname")
public String getName() {
return context.getWebServer().getPort() +
"--UserService:name=java-"+
new Random().nextInt(100);
}
}
server.port=0
spring.application.name=user-service-gateway
spring.cloud.nacos.discovery.server-addr=localhost:8848
spring.cloud.nacos.discovery.username=nacos
spring.cloud.nacos.discovery.password=nacos
② order-service:创建一个 controller
@RestController
@RequestMapping("/order")
public class OrderController {
@RequestMapping("/getcount")
public int getCount() {
return new Random().nextInt(1000);
}
}
server.port=0
spring.application.name=order-service-gateway
spring.cloud.nacos.discovery.server-addr=localhost:8848
spring.cloud.nacos.discovery.username=nacos
spring.cloud.nacos.discovery.password=nacos
2.1.1 AddResponseHeader
spring:
cloud:
nacos: # 配置注册中心
discovery:
server-addr: localhost:8848
username: nacos
password: nacos
gateway: # 配置网关
routes:
- id: userservice
uri: lb://user-service-gateway # loadbalancer
predicates:
- Path=/user/**
filters:
- AddResponseHeader=My-Resp-Header, www.baidu
server:
port: 10086
在配置过滤器之前,我在网关服务中引入 Nacos 和 LoadBalancer 之后,使用10086 端口去访问userservice服务时,查看对应的响应头:
配置了 AddResponseHeader 过滤器之后,再去访问userservice服务时,查看对应的响应头:
上述的设置,设置的是局部过滤器,如果此时在网关服务中再新增一组 id (如下),此时再去访问order-service服务时,响应头中就不会包含 My-Resp-Header 这个标头了。
spring:
cloud:
gateway: # 配置网关
routes:
- id: userservice # 用户服务
uri: lb://user-service-gateway # loadbalancer
predicates:
- Path=/user/**
filters:
- AddResponseHeader=My-Resp-Header, www.baidu
- id: orderservice # 订单服务
uri: lb://order-service-gateway
predicates:
- Path=/order/**
此时还想访问 orderservice 服务时,也在响应头中看到对应的标头,要么就把 filters 照搬到下面,但是这样做,代码就不具备维护性了,发生修改的时候,这将会是一个体力活;要么就使用全局过滤器。
全局过滤器的配置
spring:
cloud:
gateway: # 配置网关
routes:
- id: userservice # 用户服务
uri: lb://user-service-gateway # loadbalancer
predicates:
- Path=/user/**
filters:
- AddResponseHeader=My-Resp-Header, www.baidu
- id: orderservice # 订单服务
uri: lb://order-service-gateway
predicates:
- Path=/order/**
default-filters: # 全局过滤器
- AddResponseHeader=MyApplication-Resp-Header, gateway.org
2.1.2 AddRequestHeader
spring:
cloud:
nacos: # 配置注册中心
discovery:
server-addr: localhost:8848
username: nacos
password: nacos
gateway: # 配置网关
routes:
- id: userservice # 用户服务
uri: lb://user-service-gateway # loadbalancer
predicates:
- Path=/user/**
filters:
- AddResponseHeader=My-Resp-Header, www.baidu
default-filters:
- AddRequestHeader=My-Req-Marking, www.baidu
如何拿到前置过滤器,可以在 userservice 服务里边写一个打印请求头的 controller:
@RequestMapping("/print-header")
public void printHeader(HttpServletRequest request) {
Enumeration<String> headers = request.getHeaderNames();
while(headers.hasMoreElements()) {
String key = headers.nextElement();
String value = request.getHeader(key);
System.out.println(key +": " + value);
}
}
此时运行userservice 和 gateway,去反问 print-header 接口, 查看 userservice 的控制台:
2.1.3 PrefixPath
spring:
cloud:
nacos: # 配置注册中心
discovery:
server-addr: localhost:8848
username: nacos
password: nacos
gateway: # 配置网关
routes:
- id: userservice
uri: lb://user-service-gateway
predicates:
- Path=/user/**
filters:
- PrefixPath=/v2
假设此时 userservice 中的 UserController 升级为了 v2 版本,但是我的接口 /user/** 已经 对外公布了,此时还想让 localhost:10086/user/getname 访问到 v2 版本的 controller,肯定是不允许再修改后端的接口了,也不能让前端跟着改,使用 PrefixPath 就可以很好的调整。
第一版代码:
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired // 获取动态端口
private ServletWebServerApplicationContext context;
@RequestMapping("/getname")
public String getName() {
return context.getWebServer().getPort() +
"--UserService:name=java-"+
new Random().nextInt(100);
}
}
第二版代码:
@RequestMapping("/v2/user")
@RestController
public class UserControllerV2 {
@Autowired // 获取动态端口
private ServletWebServerApplicationContext context;
@RequestMapping("/getname")
public String getName() {
return context.getWebServer().getPort() +
"--V2:UserService:name=java-"+
new Random().nextInt(10);
}
}
当我们配置了 PrefixPath 之后,再使用原来的 localhost:10086/user/getname 去访问服务的时候,就可以正常的访问到 v2 版本的 controller 了,无需再去修改前后端接口。
2.1.4 RequestRateLimiter
这是 Spring Cloud Gateway 内置的网关限流过滤器,它使用了令牌桶的限流算法。
令牌桶限流算法:令牌桶限流算法通过固定速率生成令牌放入桶中,桶满则丢弃新令牌。请求到来时消耗令牌进行处理,桶内无令牌则等待或丢弃请求,从而平滑流量,防止网络拥堵。
Spring Cloud Gateway 当前版本支持和 Redis 一起实现限流功能,Spring Cloud Gateway 选择 Redis 作为限流方案的一个重要支撑是因为 Redis 的一些特性可以很好地满足限流中对于性能、一致性和分布式处理的要求。
分布式环境:在微服务架构中,服务实例往往是分布式部署的,Redis 由于其天然的分布式特性,能够确保在不同的服务实例之间共享限流的状态,实现全局限流。
性能:Redis 是一个高性能的内存数据库,它的读写速度非常快,可以达到每秒数十万次的读写请求。这种性能上的优势使得 Redis 成为实现限流中维护和检查速率限制状态的理想选择。
原子操作:Redis 支持多种原子操作,这对于计数器来说非常重要。例如,使用
INCR
和DECR
命令递增或递减计数器,可以保证即使在高并发的情况下,计数器的值也是准确的。过期策略:Redis 允许为数据设置生存时间(TTL),这对于限流算法中的时间窗口非常有用。例如,在固定时间窗口算法中,可以设置令牌或计数器在特定时间后自动过期。
它的实现步骤总共分为三步:
- 添加 Redis 框架依赖
- 创建限流规则
- 配置限流过滤器
a.添加 Redis 框架依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
注意事项:Redis版本太低,此功能设置会无效,建议使用 Redis 版本 5.x+。
b.创建限流规则
限流规则,既可以针对某一个 IP 做限流,也可以针对 URL 进行限流(所有的 IP 访问 URL 都会限流)
创建一个类,根据IP进行限流:
@Component
public class IpAddressKeyResolver implements KeyResolver {
@Override
public Mono<String> resolve(ServerWebExchange exchange) {
return Mono.just(exchange.getRequest().
getRemoteAddress().getHostName());
}
}
创建一个类,根据URL进行限流:
@Component
public class UrlKeyResolver implements KeyResolver {
@Override
public Mono<String> resolve(ServerWebExchange exchange) {
// 获取请求的URI路径作为限流的key
String path = exchange.getRequest().getURI().getPath();
return Mono.just(path);
}
}
c.设置限流过滤器
spring:
cloud:
nacos: # 配置注册中心
discovery:
server-addr: localhost:8848
username: nacos
password: nacos
gateway: # 配置网关
routes:
- id: userservice
uri: lb://user-service-gateway
predicates:
- Path=/user/**
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 1 # 每秒请求数
redis-rate-limiter.burstCapacity: 1 # 最大请求数
key-resolver: "#{@ipAddressKeyResolver}" # spEL表达式
data:
redis:
host: 127.0.0.1
port: 16379
database: 0
注意,内置的限流过滤器 name 必须等于"RequestRateLimiter" ,其他参数的含义如下:
1. redis-rate-limiter.replenishRate:令牌填充速度(每秒允许请求数)
2. redis-rate-limiter.burstCapacity:令牌桶容量(最大令牌数)
3. key-resolver:根据哪个 key 进行限流,它的值是 spEL 表达式。
这三步完成之后,就表示每个 IP 每秒钟只能访问一次,如果刷新页面刷新的太快,就会出现如下页面:
2.1.5 Retry
在 OpenFeign 里面呢,也有个 Retry 超时重试,而且它还可以自定义重试规则,为什么 Gateway 还要有一个 Retry 呢 ?
OpenFeign里面的重试机制是服务调用层面的,它是用来帮助服务消费者处理调用远程服务时的问题的。而Spring Cloud Gateway的重试机制是在网关层面上,主要用于对所有通过网关的服务调用提供统一的重试策略,以处理上游服务可能出现的不稳定性(网络抖动)。两者虽都提供重试功能,但服务的层次不同,因此它们在微服务架构中各有其作用。
请求重试过滤器配置案例:
spring:
cloud:
nacos: # 配置注册中心
discovery:
server-addr: localhost:8848
username: nacos
password: nacos
gateway: # 配置网关
routes:
- id: userservice
uri: lb://user-service-gateway
predicates:
- Path=/user/**
filters:
- name: Retry # 重试过滤器
args:
retries: 3
statuses: GATEWAY_TIMEOUT
methods: GET
series: SERVER_ERROR
backoff:
firstBackoff: 10ms # 第一次重试间隔
maxBackoff: 50ms # 最大重试间隔
factor: 2 # firstBack * (factor ^ n) # 重试系数
basedOnPreviousValue: false # 基于上次重试时间加上重试系数来计算
注意,重试过滤器的 name 必须等于 "Retry" ,因为 "Retry" 就是内置重试过滤器的名字,改为其他框架就无法识别了;其他参数的含义如下:
1. retries:尝试的重试次数。
2. statuses:重试的HTTP状态码。取值参考:HttpStatus (Spring Framework 6.1.2 API)
3. methods:重试的HTTP方法。取值:GET(默认值),HEAD,POST,PUT,PATCH,DELETE,OPTIONS,TRACE。
4. series:要重试的一系列状态码。默认值是 SERVER_ERROR,值是 5,表示 5xx,( 5开头的状态码)。共有 5 个取值:
- 1xx:INFORMATIONAL
- 2xx:SUCCESSFUL
- 3xx:REDIRECTION
- 4xx:CLIENT_ERROR
- 5xx:SERVER_ERROR
5. backoff:配置的重试策略。
- firstBackoff:第一次的重试间隔
- maxBackoff:最大重试间隔
- factor:重试系数,firstBackoff *(factor ^ n),n= 1,2,3,4...
- basedOnPreviousValue:默认关闭,如果设为 true,就表示基于上次的重试时间加上重试系数来计算,例如 firstBackoff 为 10,假设上一次重试是 10 * 2 ^ 1,那么下一次就不是 10 * 2 ^ 2,而是 20 * 2 ^ 2。
【案例演示】
在 userservice 服务中,写一个接口去触发 GATEWAY_TIMEOUT:
@RequestMapping("/504")
public void return504(HttpServletResponse response) {
System.out.println("------- Do return504 method. ------");
response.setStatus(504);
}
启动 userservice 模块和 gateway 模块,此时 userservice 服务自动注入到 nacos 中,使用原生服务接口访问:
原生服务接口:192.168.10.83:63129/user/504,触发 504
此时查看控制台 :打印了一次
再使用 Gateway 网关去访问 userservice 服务, localhost:10086/user/504,触发 504
此时查看控制台:打印了 4 次(清除上面的打印后)
可见重试过滤器确实生效了。
【注意事项】
1. 此处的超时重试和 OpenFeign 里面的超时重试不太一样,这里设置重试 3 次,就是真的重试 3 次,加上触发重试的 1 次,总共就是打印 4 次,而 OpenFeign 里面的超时重试,重试次数的下标是从 1 开始的,所以在 OpenFeign 里面,这样设置,只会重试 2 次,打印 3 次。
2. 上面重试过滤器的配置,像 method、series 这种参数,它都是有默认值的,比如说 series 的默认值是 5 开头的状态码,那么即使我们不设置这个参数,当我们触发 5 开头的异常时,并且没有与其他设置的参数相悖的时候,也会触发超时重试功能。
2.2 自定义过滤器
代码案例:使用 Spring Cloud Gateway 提供的全局过滤器实现统一认证授权。
@Component
public class AuthFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, // 执行的事件
GatewayFilterChain chain) // 过滤器链
{
// 未登录判断逻辑,当参数中 username=admin && password=admin 继续执行,
// 否则退出执行
// 得到 Request 对象 (reactive web)
ServerHttpRequest request = exchange.getRequest();
// 得到 Response 对象 (reactive web)
ServerHttpResponse response = exchange.getResponse();
String username = request.getQueryParams().getFirst("username");
String password = request.getQueryParams().getFirst("password");
if (username != null && username.equals("admin")
&& password != null && password.equals("admin")) {
// 已经登录,执行下一步
return chain.filter(exchange);
} else {
// 设置无权限 401
response.setStatusCode(HttpStatus.UNAUTHORIZED);
// 执行完成,不用继续执行后续流程了
return response.setComplete();
}
}
}
在访问相对应服务的时候,可以先不加用户名密码试一次,它会触发 401,再加上 username=admin&password=admin,就可以正常访问到相应服务了。
当有多个过滤器时,我们还可以给过滤器指定它的执行顺序:
假如说,此时有两个过滤器,一个用来验证登录,一个用来验证是否有权限。那么这个时候,需要先验证登录,才会再去验证是否有权限。如何实现?
- 实现 Ordered 接口
- 重写 getOrder 方法
@Override
public int getOrder() {
// 此值越小越先执行
return 1;
}