WebFilter过滤器

SpringWebFlux中,WebFilter过滤器是类似SpringMVC拦截器或JavaEE Servlet API中Filter过滤器的组件,但它是支持响应式的。WebFilter允许在请求到达控制器前或响应返回客户端前执行特定业务逻辑,适用于日志记录、鉴权等场景。这篇笔记我们将对SpringWebFlux中的WebFilter使用进行介绍。

使用WebFilter

SpringWebFlux中WebFilter是一个接口,它的定义如下。

public interface WebFilter {
    Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain);
}

ServerWebExchange这个对象我们在上一章节中已经介绍过了,它封装了请求和响应上下文,我们可以从其中读取请求信息或写入响应信息。WebFilterChain是一个典型的过滤器设计模式中的“链”对象,它用于串接整个调用流程,将请求传递给下一个过滤器或控制器。

我们想要自定义WebFilter过滤器时就需要实现WebFilter接口。

package com.gacfox.demo.filter;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;

@Slf4j
@Component
public class DemoWebFilter implements WebFilter {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        log.info("before filter");
        return chain.filter(exchange).doFinally(signalType -> log.info("after filter"));
    }
}

上面代码中,我们在请求被控制器执行前打印before filter,在执行后打印after filter

下面是个更有用一些的例子,代码中我们检查了请求头中名为X-MY-TOKEN的自定义信息并以此作为认证的凭证,如果它的值是abc123则通过验证,否则WebFilter将终端HTTP处理流程,向客户端输出认证不通过的信息。

package com.gacfox.demo.filter;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.gacfox.demo.model.ApiResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;

import javax.annotation.Resource;

@Slf4j
@Component
public class DemoWebFilter implements WebFilter {
    @Resource
    private ObjectMapper objectMapper;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        HttpHeaders headers = exchange.getRequest().getHeaders();
        String token = headers.getFirst("X-MY-TOKEN");
        return checkToken(token).flatMap(isValid -> {
            if (isValid) {
                return chain.filter(exchange);
            } else {
                ServerHttpResponse response = exchange.getResponse();
                response.setStatusCode(HttpStatus.FORBIDDEN);
                response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
                try {
                    byte[] jsonBytes = objectMapper.writeValueAsBytes(ApiResult.failure("You are not allowed to access!"));
                    DataBuffer buffer = response.bufferFactory().wrap(jsonBytes);
                    return response.writeWith(Mono.just(buffer));
                } catch (JsonProcessingException e) {
                    return Mono.error(e);
                }
            }
        });
    }

    private Mono<Boolean> checkToken(String token) {
        // ... 虽然简单的equals不需要使用Mono,但这里假设查询了认证服务或调用了某种复杂算法检查token
        return Mono.just("abc123".equals(token));
    }
}

代码中,我们对WebFilter使用了@Component注解声明,它会自动注册到框架并在全局范围内生效,所有请求都会经过这种WebFilter。过滤器中,我们首先从ServerWebExchangeServerHttpRequest中取出了请求头,然后调用checkToken()方法对其进行检查,这个方法在实际开发中通常会调用认证服务或是调用某些复杂的密码学算法,为了避免阻塞式代码我们通常需要将其封装到Mono对象,根据检查结果,我们分别执行继续处理请求或是中断请求流程,返回错误提示的代码逻辑。向HTTP响应写入消息时,我们需要从ServerWebExchange获取ServerHttpResponse对象,我们在其中设置了403响应状态码,还使用writeWith()方法写出了JSON字符串信息。这样我们便基于WebFilter实现了一个简单的鉴权功能。

包含或排除路径

SpringMVC中的拦截器可以在注册时指定包含或排除路径,但不巧的是SpringWebFlux中没有类似的功能,我们只能通过编程方式处理路径包含或排除。Spring中处理路径匹配有一个AntPathMatcher类,它有较好的性能优化,且支持通配符。WebFilter中我们可以基于AntPathMatcher实现匹配功能。

package com.gacfox.demo.filter;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;

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

@Slf4j
@Component
public class DemoWebFilter implements WebFilter {
    private static final AntPathMatcher matcher = new AntPathMatcher();
    private final List<String> patterns = Arrays.asList("/demo/hello", "/login");

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        if (isPathMatch(exchange)) {
            log.info("filter path {}", exchange.getRequest().getPath().value());
        }
        return chain.filter(exchange);
    }

    public boolean isPathMatch(ServerWebExchange exchange) {
        String path = exchange.getRequest().getPath().value();
        return patterns.stream().anyMatch(pattern -> matcher.match(pattern, path));
    }
}

代码中,如果路径匹配,我们会额外输出一条日志信息。

Webfilter的执行顺序

如果在一个HTTP处理流程中有多个Webfilter,我们可以使用Spring的@Order注解定义处理的顺序。

@Component
@Order(1)
public class FirstWebFilter implements WebFilter {  }

@Order指定的数字越小其优先级越高。

作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。