Spring Cloud Gatewayで独自認証をした上でバックエンドにリクエストを流す

# 目的
Locustなどの特にユーザー認証の必要がなく使える諸々のWebUIツールに対して、
独自の認証を挟んだ上で利用するようにしたかったのですが、
ツールの種類が増えるたびに個々のツールに認証機能を実装していくのも面倒かなと思いました。

そこで各ツールの前段にSpring Cloud Gatewayのアプリケーションを配置して
ここで各ツールに対するリクエストを全て受け付けて独自認証を行い、
認証がOKであればバックエンドのツールにリクエストを流すようにしてみました。

# プロジェクトのセットアップ
[Spring initializr](https://start.spring.io/)でDependenciesに"Gateway"を追加した上で雛形を作成してダウンロードします。

# 実装1: まずは認証無しでリクエストの振り分け
まずは認証機能無しで、単純にSprin Cloud Gatewayでリクエストを受け付けて、
バックエンドのツールにリクエストを振り分けるところまでやってみます。

ここまではJavaクラスの実装は必要なく、application.yamlの設定だけで実現できます。

今回はSpring Cloud Gatewayのアプリケーションは8080ポートでリッスンし、
http://localhost:8080 
でアクセスできるようにしておきます。

バックエンドのWebUIツールのURLは
http://localhost:9001
および
http://localhost:9002
とします。

## application.yaml
```
# Spring Cloud Gatewayのポート番号の設定(なくても良い)
server:
  port: 8080

# Spring Cloud Gatewayのメインの設定
spring:
  cloud:
    gateway:
      routes:
        # -----------------------------------------------------
        # http://localhost:8080/tool1/hoge/fuga/...のリクエストを
        # http://localhost:9001/hoge/fuga/...に流す
        # -----------------------------------------------------
        - id: tool1
          # プロキシ先
          uri: http://localhost:9001
          # ルーティング
          predicates:
            - Path=/tool1/**
          # フィルタ(パスの書き換えや独自処理を挟み込む)
          filters:
            - StripPrefix=1 # パスの先頭部分を切り取る。今回の場合"/tool1"を取り除く
        # -----------------------------------------------------
        # http://localhost:8080/tool2/hoge/fuga/...のリクエストを
        # http://localhost:9002/hoge/fuga/...に流す
        # -----------------------------------------------------
        - id: tool2
          # プロキシ先
          uri: http://localhost:9002
          # ルーティング
          predicates:
            - Path=/tool2/**
          # フィルタ(パスの書き換えや独自処理を挟み込む)
          filters:
            - StripPrefix=1 # パスの先頭部分を切り取る。今回の場合"/tool2"を取り除く
```

重要なのはspring.cloud.gateway.routesで、各項目の概要は以下の通り

| 項目        | 内容 |
| ---------- | ---- |
| id         | 任意のIDを設定する |
| uri        | リクエストを送る先(バックエンド)を設定する |
| predicates | Spring Cloud Gatewayに対してどういうリクエストがきたら、このルーティングルールに適用させるかを定義する。 |
| filters    | バックエンドにリクエストを送る前や後に処理を挟み込む時に指定する。自作のフィルタクラスを利用することも可能(後述) |

組み込みのpredicatesとfiltersにどういう物があるかは、[Spring Cloud Gatewayの公式ドキュメント](https://cloud.spring.io/spring-cloud-gateway/reference/html/)を参考にしてみると良いと思います。

## 動作確認
実際のWebUIツールの代わりに、ncコマンドで固定の文字列を返すスタブサーバーを立ち上げます。

```
$ (echo "HTTP/1.1 200 ok"; echo; echo "hello") | nc -l 9001
```

次にSpring Cloud Gatewayに対してcurlリクエストします。

```
$ curl -v http://localhost:8080/tool1/hoge/fuga
```

Spring Cloud Gatewayの設定内容が正しければ、
localhost:8080/tool1/hoge/fugaのリクエストが
localhost:9001/hoge/fugaに流れるようになるはずです。

### 結果
スタブサーバーにやってきたリクエストの内容は以下の通り。
URLのパスは/tool/hoge/fugaではなく、/hoge/fugaになっていることがわかります。

```
$ (echo "HTTP/1.1 200 ok"; echo; echo "hello") | nc -l 9001
GET /hoge/fuga HTTP/1.1
Host: localhost:8080
User-Agent: curl/7.64.1
Accept: */*
Forwarded: proto=http;host="localhost:8080";for="0:0:0:0:0:0:0:1:50024"
X-Forwarded-For: 0:0:0:0:0:0:0:1
X-Forwarded-Proto: http
X-Forwarded-Prefix: /tool1
X-Forwarded-Port: 8080
X-Forwarded-Host: localhost:8080
content-length: 0
```

curlリクエストの結果は以下の通りで、
Spring Cloud Gatewayから、バックエンドにあるスタブサーバーのレスポンスが返されていることがわかります。

```
$ curl -v http://localhost:8080/tool1/hoge/fuga
> GET /tool1/hoge/fuga HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 200 OK
< transfer-encoding: chunked
<
hello
```

# 実装2: 独自のフィルタを追加して認証を行う
次に自作の認証フィルタを作って適用してみます。
今回はHTTPリクエストヘッダから"Authorization"キーの値を取り出し、
その値が"xxx"なら認証OKでバックエンドにリクエストを投げ、
それ以外なら認証NGとして401 Unauthorizaedのレスポンスを返すフィルタを作ってみます。

※ 実際の認証の機能を作る場合はこんな単純なロジックではNGですが、今回は説明のため、認証のロジックを簡略化して実装コードを書いています。

## MyAuthFilter.java
フィルタの実装は以下の通りで、AbstractGatewayFilterFactoryを継承したクラスを実装します。

```
@Component
public class MyAuthFilter extends AbstractGatewayFilterFactory<MyAuthFilter.Config> {
    public MyAuthFilter() {
        super(Config.class);
    }

    @Override
    public GatewayFilter apply(Config config) {
        return (exchange, chain) -> {
            // Authorization ヘッダの取得
            ServerHttpRequest request = exchange.getRequest();
            String authorizationHeader = Optional.ofNullable(request.getHeaders().get("Authorization"))
                    .map(h -> {return h.get(0);}).orElse("");

            // Authorizationヘッダがxxxなら認証成功でそのままリクエストを流す。
            // そうでなければ401 Unauthorizedのレスポンスを返す
            if(authorizationHeader.equals("xxx")) {
                return chain.filter(exchange.mutate().request(request).build());
            } else {
                ServerHttpResponse response = exchange.getResponse();
                response.setStatusCode(HttpStatus.UNAUTHORIZED);
                return response.setComplete();
            }
        };
    }

    public static class Config {

    }
}
```

## application.yaml
上記のように自作のフィルタクラスを作っただけではまだ反映されません。
自作フィルタを適用したい場合はapplication.yamlのfiltersの項目で、
この独自フィルタのクラス名を書いてあげます。

application.yamlの全容は以下の通りで、"[追加]"と書いてある2行だけを追記しています。

```
# Spring Cloud Gatewayのメインの設定
spring:
  cloud:
    gateway:
      routes:
        # -----------------------------------------------------
        # http://localhost:8080/tool1/hoge/fuga/...のリクエストを
        # http://localhost:9001/hoge/fuga/...に流す
        # -----------------------------------------------------
        - id: tool1
          # プロキシ先
          uri: http://localhost:9001
          # ルーティング
          predicates:
            - Path=/tool1/**
          # フィルタ(パスの書き換えや独自処理を挟み込む)
          filters:
            - MyAuthFilter  # [追加]自作の認証フィルタを挟む
            - StripPrefix=1 # パスの先頭部分を切り取る。今回の場合"/tool1"を取り除く
        # -----------------------------------------------------
        # http://localhost:8080/tool2/hoge/fuga/...のリクエストを
        # http://localhost:9002/hoge/fuga/...に流す
        # -----------------------------------------------------
        - id: tool2
          # プロキシ先
          uri: http://localhost:9002
          # ルーティング
          predicates:
            - Path=/tool2/**
          # フィルタ(パスの書き換えや独自処理を挟み込む)
          filters:
            - MyAuthFilter  # [追加]自作の認証フィルタを挟む
            - StripPrefix=1 # パスの先頭部分を切り取る。今回の場合"/tool2"を取り除く

# Spring Cloud Gatewayのポート番号の設定(なくても良い)
server:
  port: 8080
```

## 動作確認
先ほどと同様に9001番ポートでスタブサーバーを立ち上げ、
以下のようにAuthorizationヘッダ無しでリクエストしたり、
Authorizationヘッダありでcurlリクエストします。

```
$ curl -v http://localhost:8080/tool1/hoge/fuga
```
```
$ curl -H 'Authorization: xxx' -v http://localhost:8080/tool1/hoge/fuga
```

### 結果
Authrozationヘッダ無しでリクエストすると、MyAuthFilterで実装した通り、
401 Unauthorizedのレスポンスが返ります。

```
$ curl -v http://localhost:8080/tool1/hoge/fuga
> GET /tool1/hoge/fuga HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 401 Unauthorized
< content-length: 0
<
```

また、スタブサーバー側にはリクエストが来ていないこともわかります。

```
$ (echo "HTTP/1.1 200 ok"; echo; echo "hello") | nc -l 9001
※ 変化無し
```

次にAuthrozationヘッダにxxxの値をセットしてリクエストすると、
スタブサーバーが返しているhelloの文字列が返ります。

```
$ curl -H 'Authorization: xxx' -v http://localhost:8080/tool1/hoge/fuga
> GET /tool1/hoge/fuga HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.64.1
> Accept: */*
> Authorization: xxx
>
< HTTP/1.1 200 OK
< transfer-encoding: chunked
<
hello
```

スタブサーバー側にもリクエストが来ていることが確認できます。

```
$ (echo "HTTP/1.1 200 ok"; echo; echo "hello") | nc -l 9001
GET /hoge/fuga HTTP/1.1
Host: localhost:8080
User-Agent: curl/7.64.1
Accept: */*
Authorization: xxx
Forwarded: proto=http;host="localhost:8080";for="0:0:0:0:0:0:0:1:50517"
X-Forwarded-For: 0:0:0:0:0:0:0:1
X-Forwarded-Proto: http
X-Forwarded-Prefix: /tool1
X-Forwarded-Port: 8080
X-Forwarded-Host: localhost:8080
content-length: 0
```

とりあえず認証OK・NGの判定をしてバックエンドにリクエストを流す・流さないを制御するのは、これで実現できそう。

# 参考
| サイト | 概要
| ----- | ----
| [Spring Cloud Gatewayで遊ぶ](https://kazuhira-r.hatenablog.com/entry/2018/11/12/235917) | 最初に目を通したページ
| [Spring Cloud Gateway](https://cloud.spring.io/spring-cloud-gateway/reference/html/) | Springの公式ドキュメント。組み込みのPredicatesとFilterの情報に関して色々載ってます。自作Filterに関する説明も少し載っています。
| [Spring Cloud Gateway - Creating Custom Route Filters (AbstractGatewayFilterFactory)](https://www.woolha.com/tutorials/spring-cloud-gateway-creating-custom-route-filters-abstractgatewayfilterfactory) | 自作Filterの実装とapplication.yamlでの設定方法は、主にこちらのページを参考にしました。