(更新日:)

まだ認証で疲弊してるの?マイクロサービス時代のSpring Cloud Gatewayを徹底解説してみる

Programming

Spring Cloud Gatewayとは?

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

公式が親切に日本語で解説してるので見てみましょう。

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

つまり?

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

アーキテクチャ

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

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

仮に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を進めると理解が深まると思います。

認証の手順

導入する目的・メリット

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

ちなみに

マイクロサービス関係なくAPI内に認証を入れる場合であれば、Spring OAuth2 Clientを設定しても良い

デメリット。。。

Spring Cloud GatewayというよりSpring 5のWebFluxの問題かもしれませんが、NettyというWebサーバ上でConnectionが切れる問題が多発したり(こちらの記事で解説)、RefreshTokenの自動更新処理などは自分で入れる必要があります。

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

認証の仕組み

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

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

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

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

該当ISSUE

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

コードだとこんな感じ

    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の非同期処理難しいよね。

Spring Cloud Gatewayの使い方

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

これの何が言いたいかというと基本的に一度作ってしまえば、別のプロジェクトで別の認証サーバと通信したいなんてときに引数を変えるだけで対応できてしまうので、アプリのための認証APIが再利用できてしまいます。

docker-composeはこんなかんじ

  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ソースはこちら

kubenetes

kubenetesワカラナイ…以下のサンプルをどうぞ。

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

まとめ

いかがでしたでしょうか?

AWS AmplifyのようにローカルストレージにJWTを保持するやり方も散見されますが一番確実なのはSession内にトークンを隠してあげることでさらにセキュアなアーキテクトになると思います。

スタートアップ的なセキュリティを(あまり)重要視しないアプリであれば最初は不要ですが、BtoBのようなお堅いシステムを構築するのであれば、Spring Cloud Gatewayは向いているアーキテクトだと思います。

結局は用途に応じて最善なモノを選ぶことが必要になってくるので用法容量を守って正しくお使いください。

それでは良い認証ライフを!

関連資料

最後に他に聞いてみたいことがある、こんなユースケースはどうしたらいいか、など質問があれば是非コメントよりお寄せください。

#Java#OAuth2#Spring#認証処理

Related Links


【Reactor.netty x Spring】 環境変数をコントロールする
Yoshiki Ohashi
2x歳の個人事業主エンジニア。SI企業1年, Webベンチャー企業2年で上流から下流の経験を経て独立。 エンジニアらしく性格は温和。プロジェクトチームに心理的安全性を求める。go gin | Spring | Java | Kotlin | Vue | Python | 筋トレ | キャンプ | 個人開発 | 新潟出身
© 2020 Yoshiki Ohashi All rights reserved.