16
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Posted at

はじめに

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

この記事はGatsbyというヘッドレスCMS技術で構成されています。

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

使い方

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

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ワカラナイ...サンプルをどうぞ。

関連資料

16
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?