WebFluxのフィルタを実装してみる

引き続きWebFluxの話です。
前回までで主にController〜Service層でのリアクティブプログラミングの実装を紹介しました。

今回はリクエストの前後の共通処理として利用するフィルタの実装例を紹介します。

## FilterではなくWebFliterを実装する
Spring MVCでは `javax.servlet.Filter` を実装しますが、WebFluxでは `org.springframework.web.server.WebFilter` を実装します。

以下に、リクエスト処理の前後でログ出力するだけの、簡単なフィルタをそれぞれ実装してみました。
違いをみてみましょう。

### Spring MVCのFilter実装例

```java:リクエスト処理の前後にログを出力するフィルタ（Filter）

@Slf4j
public class TimestampFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest)request;
        // before
        doBeforeRequest(req);
        try {
            // do request
            chain.doFilter(request, response);
            // after (success)
            doAfterRequest(req);
        } catch (Throwable throwable) {
            // after (with error)
            doAfterRequestWithError(req, throwable);
            throw throwable;
        }
    }

    private void doBeforeRequest(HttpServletRequest request) {
        String uri = request.getRequestURI();
        long start = System.currentTimeMillis();
        // start log（ex: /hoge [IN]: 1582989973940）
        log.info(String.format("%s [IN]: %d", uri, start));
    }

    private void doAfterRequest(HttpServletRequest request) {
        String uri = request.getRequestURI();
        long end = System.currentTimeMillis();
        // end log（ex: /hoge [OUT]: 1582989974053）
        log.info(String.format("%s [OUT]: %d", uri, end));
    }

    private void doAfterRequestWithError(HttpServletRequest request, Throwable throwable) {
        String uri = request.getRequestURI();
        long end = System.currentTimeMillis();
        // end with error log（ex: /hoge [OUT]: 1582989974053 with Error(java.lang.RuntimeException)）
        log.info(String.format("%s [OUT]: %d with Error(%s)", uri, end, throwable.toString()));
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void destroy() {
    }
}
```

これと同等の処理を WebFilter で実装したのが以下です。

### Spring WebFluxのWebFilter実装例

```java:リクエスト処理の前後にログを出力するフィルタ（WebFilter）

@Slf4j
@Component
public class TimestampFilter implements WebFilter {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        return chain.filter(exchange).transformDeferred(call -> doFilter(exchange, call));
    }

    private Publisher<Void> doFilter(ServerWebExchange exchange, Mono<Void> call) {
        // before
        return Mono.fromRunnable(() -> doBeforeRequest(exchange))
                // do request
                .then(call)
                // after (success)
                .doOnSuccess((done) -> doAfterRequest(exchange))
                // after (with error)
                .doOnError((throwable -> doAfterRequestWithError(exchange, throwable)));
    }

    private void doBeforeRequest(ServerWebExchange exchange) {
        String uri = exchange.getRequest().getURI().toString();
        long start = System.currentTimeMillis();
        // start log（ex: /hoge [IN]: 1582989973940）
        log.info(String.format("%s [IN]: %d", uri, start));
    }

    private void doAfterRequest(ServerWebExchange exchange) {
        String uri = exchange.getRequest().getURI().toString();
        long end = System.currentTimeMillis();
        // end log（ex: /hoge [OUT]: 1582989974053）
        log.info(String.format("%s [OUT]: %d", uri, end));
    }

    private void doAfterRequestWithError(ServerWebExchange exchange, Throwable throwable) {
        String uri = exchange.getRequest().getURI().toString();
        long end = System.currentTimeMillis();
        // end with error log（ex: /hoge [OUT]: 1582989974053 with Error(java.lang.RuntimeException)）
        log.info(String.format("%s [OUT]: %d with Error(%s)", uri, end, throwable.toString()));
    }
}
```
(Mono#compose(Function)は非推奨となったため、Mono#transformDeferred(Function)に修正しました。2020/04/01）

`Mono.fromRunnable(~)` は、戻り値の無い処理（void）をリアクティブに処理し、その後続を `then(~)` に記述します。
例では .then( `call` ) を指定し、次のフィルタまたはコントローラに処理を移譲しています。

さらに後続を `doOnSuccess(~)`、 `doOnError(~)` で繋げることで、事後処理を実装しています。


最後に、もう少し実践的なフィルタの実装サンプルを見てみましょう。

## カスタム認証フィルタの実装例
以下は、保護されたリソースへのアクセス時にユーザ認証済みであるかをチェックする、いわゆる認証チェックフィルタの実装例です。

```java:カスタム認証チェックフィルタ

@Slf4j
@Component
@Order(1)
public class AuthFilter implements WebFilter {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        return chain.filter(exchange).transformDeferred(call -> doFilter(exchange, call));
    }

    private Publisher<Void> doFilter(ServerWebExchange exchange, Mono<Void> call) {
        return exchange.getSession().flatMap(webSession -> {
            Map<String, Object> attributes = webSession.getAttributes();
            // セッションからユーザ情報を取得
            Optional<User> optionalUser =
                    Optional.ofNullable((User)attributes.get(User.class.getName()));
            return optionalUser
                    // ユーザがある場合はリクエストを処理
                    .map(user -> call)
                    // ユーザが無い場合は未認証エラーをスロー
                    .orElseThrow(UnauthorizedException::new);
        });
    }
}

```

最初のセッション情報の取得 `exchange.getSession()` は `Mono<WebSession>` を返すため、後続の書き方は以前紹介した [複数の順次APIコール](https://qiita.com/kilvis/items/fb18be963da6cac03ee9#%E8%A4%87%E6%95%B0%E3%81%AE%E9%A0%86%E6%AC%A1api%E3%82%B3%E3%83%BC%E3%83%AB) が参考になるかと思います。

また、この例では未認証時はカスタムエラー（UnauthorizedException）をスローするので、共通のExceptionHandlerで `401:Unauthrized` レスポンスに変換してあげる想定です。

もし、フィルタ内でレスポンスまで生成したい場合は以下のように実装できます。

```java:WebFilter内でレスポンスまで生成
    ...省略...

    private Publisher<Void> doFilter(ServerWebExchange exchange, Mono<Void> call) {
        return exchange.getSession().flatMap(webSession -> {
            Map<String, Object> attributes = webSession.getAttributes();
            // セッションからユーザ情報を取得
            Optional<User> optionalUser =
                    Optional.ofNullable((User)attributes.get(User.class.getName()));
            return optionalUser
                    // ユーザがある場合はリクエストを処理
                    .map(user -> call) // do request
                    // ユーザが無い場合は未認証エラーレスポンスを生成
                    .orElse(writeUnauthorizedResponse(exchange));
        });
    }

    private Mono<Void> writeUnauthorizedResponse(ServerWebExchange exchange) {
        // 401:UnauthorizedエラーとしてJSONレスポンスを生成
        ServerHttpResponse response = exchange.getResponse();
        String body = "{\"message\":\"ログイン認証が必要です。\"}";
        return writeResponse(response, HttpStatus.UNAUTHORIZED, body);
    }

    private Mono<Void> writeResponse(ServerHttpResponse response, HttpStatus status, String jsonBody) {
        response.setStatusCode(status);
        response.getHeaders().add(HttpHeaders.CONTENT_TYPE, "application/json;charset=UTF-8");
        DataBufferFactory dbf = response.bufferFactory();
        return response.writeWith(Mono.just(dbf.wrap(jsonBody.getBytes())));
    }

```

## 今回のまとめ
WebFluxでフィルタは

- `WebFilter` のサブクラスとして実装する
- 中身はやはりリアクティブに処理を記述する （[前々回の記事](https://qiita.com/kilvis/items/fb18be963da6cac03ee9)を参考）

となります。