SpringCloud Gateway之filter

接上一篇:SpringCloud Gateway之路由规则

Predict决定了请求由哪一个路由处理,在路由处理之前,需要经过“pre”类型的过滤器处理,处理返回响应之后,可以由“post”类型的过滤器处理。在“pre”类型的过滤器可以做参数校验、权限校验、流量监控、日志输出、协议转换等,在“post”类型的过滤器中可以做响应内容、响应头的修改,日志的输出,流量监控等。
如下图所示,客户端的请求先经过“pre”类型的filter,然后将请求转发到具体的业务服务,比如上图中的user-service,收到业务服务的响应之后,再经过“post”类型的filter处理,最后返回响应到客户端。
在这里插入图片描述
Spring Cloud Gateway的Filter分为两种:GatewayFilter 与 GlobalFilter。GlobalFilter会应用到所有的路由上,而GatewayFilter将应用到单个路由或者一个分组的路由上。

Gateway filter

GatewayFilter工厂可以在配置文件application.yml中配置,遵循了约定大于配置的思想,只需要在配置文件配置GatewayFilter Factory的名称,而不需要写全部的类名,比如AddRequestHeaderGatewayFilterFactory只需要在配置文件中写AddRequestHeader,而不是全部类名。在配置文件中配置的GatewayFilter Factory最终都会相应的过滤器工厂类处理。
Spring Cloud Gateway包含许多内置的GatewayFilter工厂,如下图所示:
在这里插入图片描述
下面讲述几个常见的过滤器工厂:

1 AddRequestHeader GatewayFilter Factory
1.1 yml配置
server:
  port: 8080
spring:
  application:
    name: test-GATEWAY
  cloud:
    gateway:
      routes:
      - id: test-route
        uri: http://httpbin.org:80/get
        predicates:
          - After=2020-01-09T00:00:00+08:00[Asia/Shanghai]
        filters:
          - AddRequestHeader=Test,gateway
1.2 分析并查看结果

在上述配置中,配置了router的id为test-route,路由地址为http://httpbin.org:80/get,该router有AfterPredictFactory,有一个filter为AddRequestHeaderGatewayFilterFactory(约定写成AddRequestHeader),AddRequestHeader过滤器工厂会在请求头加上一对请求头,名称为Test,值为gateway。我们在浏览器中输入:http://localhost:8080/get,得到如下:

{
    "args": {},
    "headers": {
        "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
        "Accept-Encoding": "gzip, deflate, br",
        "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8,de;q=0.7",
        "Cache-Control": "max-age=0",
        "Cookie": "Hm_lvt_510ed80f0be980f190b7bfc3a27fabd9=1556184289; Hm_lvt_3242ee7ea9b8b5d0e409bb74bc4e0091=1556184289; Phpstorm-d6b3462e=aa37f591-2c2c-49d9-9293-479590c634c0; Idea-8bd2b531=a445ac7e-83b1-496c-baaa-96bc8d5cee14",
        "Forwarded": "proto=http;host=\"localhost:8080\";for=\"0:0:0:0:0:0:0:1:56947\"",
        "Host": "httpbin.org",
        "Sec-Fetch-Mode": "navigate",
        "Sec-Fetch-Site": "none",
        "Sec-Fetch-User": "?1",
        "Test": "gateway",
        "Upgrade-Insecure-Requests": "1",
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36",
        "X-Forwarded-Host": "localhost:8080"
    },
    "origin": "0:0:0:0:0:0:0:1, 139.199.106.7, ::1",
    "url": "https://localhost:8080/get"
}

在上述响应结果中,确实在请求头中加入了Test这样的一个请求头,在配置文件中配置的AddRequestHeader过滤器工厂生效。

2 RewritePath GatewayFilter Factory
2.1 yml配置
server:
  port: 8080
spring:
  application:
    name: test-GATEWAY
  cloud:
    gateway:
      routes:
      - id: test-route
        uri: https://blog.csdn.net/
        predicates:
          - Path=/sso/**
        filters:
          - RewritePath=/sso/(?<segment>.*),/$\{segment}
2.2 分析并查看结果

上面的配置中,所有的/sso/**开始的路径都会命中配置的router,并执行过滤器的逻辑,在本案例中配置了RewritePath过滤器工厂,此工厂将/sso/(?.*)重写为{segment},然后转发到https://blog.csdn.net。比如在网页上请求localhost:8080/sso/mameng1988,此时会将请求转发到https://blog.csdn.net/mameng1988的页面。

自定义过滤器

Spring Cloud Gateway内置了19种过滤器工厂,可以满足大部分场景,但我们也可以自定义过滤器。在Spring Cloud Gateway中,我们实现GatewayFilter和Ordered2个接口就可以自定义过滤器了,下面我们写一个

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

/**
 * @author by mazhen
 * @Classname CustomerFilter
 * @Description TODO
 * @Date 2020/1/11 22:22
 */
@Component
public class CustomerFilter implements GatewayFilter, Ordered {

    private static final Logger logger = LoggerFactory.getLogger(CustomerFilter.class);
    private static final String REQUEST_START_TIME = "Start_time";

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        exchange.getAttributes().put(REQUEST_START_TIME,System.currentTimeMillis());
        return chain.filter(exchange).then(
                Mono.fromRunnable(
                        () -> {
                            Long startTime = exchange.getAttribute(REQUEST_START_TIME);
                            if (null != startTime) {
                                logger.info(exchange.getRequest().getURI().getRawPath() + ": " 
                                + (System.currentTimeMillis() - startTime) + "ms");
                            }
                        }
                )
        );
    }

    @Override
    public int getOrder() {
        return 0;
    }
}

在上面的代码中,Ordered中的getOrder()方法是来给过滤器设定优先级的,值越大则优先级越低。还有一个filter(exchange,chain)方法,在该方法中,先记录了请求的开始时间,并保存在ServerWebExchange中,此处是一个“pre”类型的过滤器,然后再chain.filter的内部类中的run()方法中相当于"post"过滤器,在此处打印了请求所消耗的时间。然后将该过滤器注册到router中,代码如下:

@Bean
    public RouteLocator customerRouteLocator(RouteLocatorBuilder builder) {
        // @formatter:off
        return builder.routes()
                .route(r -> r.path("/sso/**")
                        .filters(f -> f.filter(new CustomerFilter()))
                        .uri("lb://test-SSO")
                        .order(0)
                        .id("customer_filter_router")
                )
                .build();
        // @formatter:on
    }

重启服务,通过浏览器输入:http://localhost:8080/sso/test
在服务的控制台输出一下的请求信息的日志:

[20200111 22:53:52][INFO ][appKey=IAM-GATEWAY][traceId=][SpanId=] c.c.i.g.c.CustomerFilter [reactor-http-nio-4] 
[CustomerFilter  ][32 ] - /sso/test: 2514ms

由于自定义过滤器无法在配置文件中配置,而自定义过滤器工厂是可以的,下面我们实现一个过滤器工厂。

自定义过滤器工厂

在实现自定义过滤器工厂之前,我们先看一下GatewayFilterFactory的源码,其层级如下:
在这里插入图片描述
过滤器工厂的顶级接口是GatewayFilterFactory,有两个较接近具体实现的抽象类,分别为AbstractGatewayFilterFactory和AbstractNameValueGatewayFilterFactory,这两个类前者接收一个参数,比如它的实现类RedirectToGatewayFilterFactory;后者接收两个参数,比如它的实现类AddRequestHeaderGatewayFilterFactory类。现在需要将请求的日志打印出来,需要使用一个参数,这时可以参照RedirectToGatewayFilterFactory的写法。

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;

import java.util.Arrays;
import java.util.List;

/**
 * @author by mazhen
 * @Classname CustomerGatewayFilterFactory
 * @Description 自定义过滤器工厂
 * @Date 2020/1/11 23:17
 */
@Component
public class CustomerGatewayFilterFactory extends AbstractGatewayFilterFactory<CustomerGatewayFilterFactory.Config> {

    private static final Logger logger = LoggerFactory.getLogger(CustomerGatewayFilterFactory.class);
    private static final String REQUEST_START_TIME = "startTime";

    @Override
    public List<String> shortcutFieldOrder() {
        return Arrays.asList("enabled");
    }

    public CustomerGatewayFilterFactory () {
        super(Config.class);
        logger.info("Loaded CustomerGatewayFilterFactory");
    }

    @Override
    public GatewayFilter apply(Config config) {
        return (exchange, chain) -> {
            if (!config.isEnabled()) {
                return chain.filter(exchange);
            }
            exchange.getAttributes().put(REQUEST_START_TIME,System.currentTimeMillis());
            return chain.filter(exchange).then(Mono.fromRunnable(() -> {
                Long startTime = exchange.getAttribute(REQUEST_START_TIME);
                if (startTime != null) {
                    StringBuilder sb = new StringBuilder(exchange.getRequest().getURI().getRawPath())
                            .append(": ")
                            .append(System.currentTimeMillis() - startTime)
                            .append("ms");
                    sb.append(" params:").append(exchange.getRequest().getQueryParams());
                    sb.append(" heads:").append(exchange.getRequest().getHeaders());
                    logger.info(sb.toString());
                }
            }));
        };
    }

    public static class Config {
        /**
         * 控制是否开启统计
         */
        private boolean enabled;
        public Config() {}
        public boolean isEnabled() {
            return enabled;
        }
        public void setEnabled(boolean enabled) {
            this.enabled = enabled;
        }
    }
}

配置文件

server:
  port: 8080
spring:
  application:
    name: test-GATEWAY
  cloud:
    gateway:
      routes:
      - id: sso-route
        uri: lb://test-sso
        predicates:
          - Path=/sso/**
        filters:
          - Customer=true

上面的配置文件中为什么是Customer属性,那是有个规范,就是工厂类要以GatewayFilterFactory为结尾,前面的名称为配置文件的属性,所以配置文件中就用Customer作为属性。值true赋值给的对象就CustomerGatewayFilterFactory中的config内部类,设置true就是对enable进行赋值。
重启服务后,和上面的自定义过滤器的访问效果是一样的。

GlobalFilter

Spring Cloud Gateway根据作用范围划分为GatewayFilter和GlobalFilter,二者区别如下:

  • GatewayFilter : 需要通过spring.cloud.routes.filters 配置在具体路由下,只作用在当前路由上或通过spring.cloud.default-filters配置在全局,作用在所有路由上
  • GlobalFilter : 全局过滤器,不需要在配置文件中配置,作用在所有的路由上,最终通过GatewayFilterAdapter包装成GatewayFilterChain可识别的过滤器,它为请求业务以及路由的URI转换为真实业务服务的请求地址的核心过滤器,不需要配置,系统初始化时加载,并作用在每个路由上。
    Spring Cloud Gateway框架内置的GlobalFilter如下:
    在这里插入图片描述
    上图中每一个GlobalFilter都作用在每一个router上,能够满足大多数的需求。但是如果遇到业务上的定制,可能需要编写满足自己需求的GlobalFilter。在下面的案例中将讲述如何编写自己GlobalFilter,该GlobalFilter会校验请求中是否包含了请求参数“token”,如何不包含请求参数“token”则不转发路由,否则执行正常的逻辑。代码如下:

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

/**
 * @author by mazhen
 * @Classname AuthorizeFilter
 * @Description TODO
 * @Date 2020/1/12 00:17
 */
@Component
public class AuthorizeFilter implements GlobalFilter , Ordered {
    private static final Logger logger = LoggerFactory.getLogger(AuthorizeFilter.class);
    private static final String AUTHORIZE_TOKEN = "token";

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String token = exchange.getRequest().getQueryParams().getFirst(AUTHORIZE_TOKEN);
        if (StringUtils.isBlank(token)) {
            logger.info("token is empty");
            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            return exchange.getResponse().setComplete();
        }
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        return 0;
    }
}

启动请求
在这里插入图片描述
以上请求没有参数token,无法转发路由。看看下面我们带上参数token
在这里插入图片描述
参考文章:微服务网关Gateway在实战中如何结合注册中心,定义过滤器?
spring cloud gateway之filter篇

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值