Spring Cloud Gatewayを徹底解説してみる

<head>

<link rel="canonical" href="https://tech-blog.yoshikiohashi.dev/posts/spring-cloud-gateway-explain" />

</head>

# はじめに

> 本記事は https://tech-blog.yoshikiohashi.dev/posts/spring-cloud-gateway-explain のクロスポスト記事になります。

この記事は[Gatsby](https://www.gatsbyjs.org/)というヘッドレスCMS技術で構成されています。

# Spring Cloud Gatewayとは？

一言でいうと**「マイクロサービス向けのOAuth2認証API基盤」**になります。

[公式](https://spring.pleiades.io/projects/spring-cloud-gateway)が親切に日本語で解説してるので見てみましょう。

> このプロジェクトは、Spring MVC の上に API Gateway を構築するためのライブラリを提供します。Spring Cloud Gateway は、API にルーティングするためのシンプルでありながら効果的な方法を提供し、セキュリティ、モニタリング / メトリック、復元力などの横断的な懸念を API に提供することを目的としています。

## つまり？

マイクロサービス間などでOAuth2などの認証問題を解決してくれるフレームワークになります。アプリ間のルーティングもしてくれるので認証機能を備えたAPI上のプロキシーのような存在になります。

# アーキテクチャ

それぞれの役割をわかりやすくするため図で見てみましょう。

## 全体的なアーキテクチャ図

![](https://img.esa.io/uploads/production/attachments/15569/2020/10/14/84487/69b4700a-b08f-4d0a-984a-1029ede21291.png)

仮にVue.jsなどのFront AppからGatewayのURLにアクセスするとCognito（AWSの場合）などのIDMとOAuth2認証を行い、指定のResource APIと通信ができるようになります。

後述しますが、**Front to Gateway間はSessionで状態管理**されており、**Gateway to API間はJWTの形式で認証のやり取り**がされます。

なのでAPI側はJWTの認証チェックだけ行えばOKということになります。（APIはSpringがBetterではあるが別言語でもSo Good）

## Workshop

この記事でWorkshopを作ってもよいのですが、大変長くなるので認証サーバのUAAを使用したこちらのRepositoryを進めると理解が深まると思います。

- https://spring.pleiades.io/guides/gs/gateway/
- https://github.com/spring-cloud-samples/sample-gateway-oauth2login

# 認証の手順

![](https://img.esa.io/uploads/production/attachments/15569/2020/10/14/84487/d5986ad7-614b-4b1b-b230-15388601b827.png)

# 導入する目的・メリット

- Front, BFFにAccessTokenを持たせないための設計ができる
- OAuth2の複雑な認証フローを自分で開発したくない
- 認証に必要な設定情報を埋め込むだけで認証を行う役割をもつ
- 認証部分が独立しているため他言語APIと連携も容易なのでマイクロサービスアーキテクチャの認証部分として適している。
- 再利用が可能！！！

## ちなみに

マイクロサービス関係なくAPI内に認証を入れる場合であれば、[Spring OAuth2 Client](https://qiita.com/yushi_koga/items/da707686d2051fd5ece4)を設定しても良い

# デメリット。。。

Spring Cloud GatewayというよりSpring 5のWebFluxの問題かもしれませんが、NettyというWebサーバ上でConnectionが切れる問題が多発したり（[こちらの記事で解説](https://tech-blog.yoshikiohashi.dev/posts/reactor-netty-spring)）、RefreshTokenの自動更新処理などは自分で入れる必要があります。

つまり、既存のISSUEがあり既存問題に対して自分たちの力で解消できるどうかが導入のキーになると思います。

# 認証の仕組み

わかりやすく図化してみました。一般的にFrontにJWTを直接持つとセキュリティ的にグレー（？）なのでSessionを保持してクレデンシャル情報をサーバ内に内包しているためかなりセキュアであると言えます。

![](https://img.esa.io/uploads/production/attachments/15569/2020/10/14/84487/8336b3b0-a679-4f41-9418-1ec8ca47bd85.png)

> 今回はCognito User Poolを使用していると仮定しているためAWS Resourceと疎通しています

## アクセストークンの自動更新処理はしてくれないの？

Spring Cloud GatewayのFilter機能により実現できます。通信間に処理を入れ込むことができる。HTTP通信の際に有効期限を確認し、切れていれば更新を行う処理を入れることができます。

[該当ISSUE](https://github.com/spring-cloud/spring-cloud-security/issues/175#issuecomment-557135243)

すでにCloseしてるので標準搭載されるかもしれません。

コードだとこんな感じ

```java
    private static ReactiveOAuth2AuthorizedClientManager createDefaultAuthorizedClientManager(
            ReactiveClientRegistrationRepository clientRegistrationRepository,
            ServerOAuth2AuthorizedClientRepository authorizedClientRepository) {

        final ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider =
                ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
                        .authorizationCode()
                        .refreshToken(configurer -> configurer.clockSkew(accessTokenExpiresSkew))
                        .clientCredentials(configurer -> configurer.clockSkew(accessTokenExpiresSkew))
                        .password(configurer -> configurer.clockSkew(accessTokenExpiresSkew))
                        .build();
        final DefaultReactiveOAuth2AuthorizedClientManager authorizedClientManager = new DefaultReactiveOAuth2AuthorizedClientManager(
                clientRegistrationRepository, authorizedClientRepository);
        authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);

        return authorizedClientManager;
    }

    public GatewayFilter apply() {
        return apply((Object) null);
    }

    @Override
    public GatewayFilter apply(Object config) {
        return (exchange, chain) -> exchange.getPrincipal()
                // .log("token-relay-filter")
                .filter(principal -> principal instanceof OAuth2AuthenticationToken)
                .cast(OAuth2AuthenticationToken.class)
                .flatMap(this::authorizeClient)
                .map(OAuth2AuthorizedClient::getAccessToken)
                .map(token -> withBearerAuth(exchange, token))
                // TODO: adjustable behavior if empty
                .defaultIfEmpty(exchange).flatMap(chain::filter);
    }

    private ServerWebExchange withBearerAuth(ServerWebExchange exchange, OAuth2AccessToken accessToken) {
        return exchange.mutate().request(r -> r.headers(headers -> headers.setBearerAuth(accessToken.getTokenValue()))).build();
    }

    private Mono<OAuth2AuthorizedClient> authorizeClient(OAuth2AuthenticationToken oAuth2AuthenticationToken) {
        final String clientRegistrationId = oAuth2AuthenticationToken.getAuthorizedClientRegistrationId();
        return Mono.defer(() -> authorizedClientManager.authorize(createOAuth2AuthorizeRequest(clientRegistrationId, oAuth2AuthenticationToken)));
    }

    private OAuth2AuthorizeRequest createOAuth2AuthorizeRequest(String clientRegistrationId, Authentication principal) {
        return OAuth2AuthorizeRequest.withClientRegistrationId(clientRegistrationId).principal(principal).build();
    }
```

> どうでも良いけどMonoとかFluxの非同期処理難しいよね。

# 使い方

基本的に**引数ゲー**です。こんな感じで設定すれば後々docker-composeファイルにも適用できます。

<div class="iframely-embed"><div class="iframely-responsive" style="padding-bottom: 56.2493%;"><a href="https://gist.github.com/yoshiki-0428/6bd354f6cbedf78bd8fb63a41642b2b8" data-iframely-url="//cdn.iframe.ly/api/iframe?url=https%3A%2F%2Fgist.github.com%2Fyoshiki-0428%2F6bd354f6cbedf78bd8fb63a41642b2b8&amp;key=4672f5d28011347a2549354aa5d0123d"></a></div></div><script async src="//cdn.iframe.ly/embed.js" charset="utf-8"></script>

## docker-compose

```yml
  spring-cloud-gateway-service:
    build: ./spring-cloud-gateway
    image: barathece91/gateway-service-k8s
    ports:
      - "9500:9500"
    depends_on: 
      - jio-microservice
      - airtel-microservice
      - vodaphone-microservice
    environment:
      SPRING_PROFILES_ACTIVE: path
      SPRING_CLOUD_GATEWAY_ROUTES[0]_URI: http://jio-microservice:9501
      SPRING_CLOUD_GATEWAY_ROUTES[0]_ID: jio-service
      SPRING_CLOUD_GATEWAY_ROUTES[0]_PREDICATES[0]: Path= /jio/*
      SPRING_CLOUD_GATEWAY_ROUTES[0]_FILTERS[0]: StripPrefix=1
      SPRING_CLOUD_GATEWAY_ROUTES[1]_URI: http://airtel-microservice:9502
      SPRING_CLOUD_GATEWAY_ROUTES[1]_ID: airtel-service
      SPRING_CLOUD_GATEWAY_ROUTES[1]_PREDICATES[0]: Path= /airtel/*
      SPRING_CLOUD_GATEWAY_ROUTES[1]_FILTERS[0]: StripPrefix=1
      SPRING_CLOUD_GATEWAY_ROUTES[2]_URI: http://vodaphone-microservice:9503
      SPRING_CLOUD_GATEWAY_ROUTES[2]_ID: vodaphone-service
      SPRING_CLOUD_GATEWAY_ROUTES[2]_PREDICATES[0]: Path= /vodaphone/*
      SPRING_CLOUD_GATEWAY_ROUTES[2]_FILTERS[0]: StripPrefix=1
```

[Sample](https://github.com/BarathArivazhagan/spring-cloud-gateway-routing/blob/master/docker-compose.yml#L38-L50)

## kubenetes

> kubenetesワカラナイ...サンプルをどうぞ。

https://github.com/spring-cloud/spring-cloud-kubernetes/tree/master/spring-cloud-kubernetes-examples

# 関連資料

- [Spring Cloud Gatewayとは](https://spring.pleiades.io/projects/spring-cloud-gateway)
- [GitHub OSS](https://github.com/spring-cloud/spring-cloud-gateway)
- [Qiita](https://qiita.com/Yoghurt/items/148fd3cd7fcdf002b933#%E7%94%B0%E7%94%BA%E9%A7%85%E3%81%B8%E3%83%AB%E3%83%BC%E3%83%86%E3%82%A3%E3%83%B3%E3%82%B0%E3%81%99%E3%82%8B)