Spring Cloud Gateway 常见过滤器的基本使用

发布时间:2023年12月27日

目录

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. 过滤器的作用

过滤器通常用于拦截、处理或修改数据流和事件流,在数据流中执行特定的操作或转换。

过滤器主要在以下几个方面发挥作用:

  1. 功能扩展和定制:过滤器允许您自定义和扩展网关的功能,以满足特定需求,如请求和响应的修改、路由规则的动态配置等。

  2. 数据校验和过滤:通过过滤器,您可以检查、验证和过滤传入或传出的数据,确保请求和响应的合法性和一致性。

  3. 安全保护:过滤器可以用于实施安全策略,如认证、授权、防止攻击等,以增强网关的安全性。

  4. 性能优化:通过过滤器,您可以对请求和响应进行性能优化,如缓存、压缩、请求路由的智能选择,以提高网关的性能。

  5. 统一处理:过滤器允许您在网关层面执行共享的处理逻辑,如日志记录、监控、审计等,以确保整个微服务体系的一致性和可维护性。

  6. 逻辑复用:通过过滤器,您可以将一些常见的操作抽象出来,以实现逻辑的复用,减少重复代码和维护工作。

2. Spring Cloud Gateway 过滤器的类型

Spring Cloud Gateway 过滤器可以分为两大类:

1. 内置过滤器

  • 局部的内置过滤器
  • 全局的内置过滤器

2. 自定义过滤器

2.1 内置过滤器

内置过滤器常见的有以下几种:

  1. AddResponseHeader
  2. AddRequestHeader
  3. AddRequestParameter(和 AddRequestHeader 相似)
  4. PrefixPath
  5. RequestRateLimiter
  6. 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 的一些特性可以很好地满足限流中对于性能、一致性和分布式处理的要求。

  1. 分布式环境:在微服务架构中,服务实例往往是分布式部署的,Redis 由于其天然的分布式特性,能够确保在不同的服务实例之间共享限流的状态,实现全局限流。

  2. 性能:Redis 是一个高性能的内存数据库,它的读写速度非常快,可以达到每秒数十万次的读写请求。这种性能上的优势使得 Redis 成为实现限流中维护和检查速率限制状态的理想选择。

  3. 原子操作:Redis 支持多种原子操作,这对于计数器来说非常重要。例如,使用 INCRDECR 命令递增或递减计数器,可以保证即使在高并发的情况下,计数器的值也是准确的。

  4. 过期策略:Redis 允许为数据设置生存时间(TTL),这对于限流算法中的时间窗口非常有用。例如,在固定时间窗口算法中,可以设置令牌或计数器在特定时间后自动过期。

它的实现步骤总共分为三步:

  1. 添加 Redis 框架依赖
  2. 创建限流规则
  3. 配置限流过滤器

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,就可以正常访问到相应服务了。

当有多个过滤器时,我们还可以给过滤器指定它的执行顺序:

假如说,此时有两个过滤器,一个用来验证登录,一个用来验证是否有权限。那么这个时候,需要先验证登录,才会再去验证是否有权限。如何实现?

  1. 实现 Ordered 接口
  2. 重写 getOrder 方法
@Override
public int getOrder() {
    // 此值越小越先执行
    return 1;
}

文章来源:https://blog.csdn.net/xaiobit_hl/article/details/134996200
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。