1. 序章

簡単に言えば、マイクロサービスアーキテクチャを使用すると、システムとAPIを一連の自己完結型サービスに分割できます。これらのサービスは完全に独立してデプロイできます。

これは継続的なデプロイと管理の観点からは優れていますが、APIの使いやすさに関してはすぐに複雑になる可能性があります。 管理するエンドポイントが異なる場合、依存するアプリケーションはCORS(クロスオリジンリソースシェアリング)とさまざまなエンドポイントのセットを管理する必要があります。

Zuulは、着信HTTPリクエストを複数のバックエンドマイクロサービスにルーティングできるようにするエッジサービスです。 一つには、これはバックエンドリソースの利用者に統一されたAPIを提供するために重要です。

基本的に、Zuulを使用すると、サービスの前に座ってプロキシとして機能することにより、すべてのサービスを統合できます。 すべてのリクエストを受信し、正しいサービスにルーティングします。 外部アプリケーションからは、APIは統合されたAPIサーフェス領域として表示されます。

このチュートリアルでは、 OAuth2.0およびJWTと組み合わせて、この正確な目的でWebサービスを保護するための最前線として使用する方法について説明します。 具体的には、 Password Grant フローを使用して、保護されたリソースへのアクセストークンを取得します。

簡単ですが重要な注意点は、単純なシナリオを調査するためにパスワード付与フローのみを使用しているということです。 ほとんどのクライアントは、実稼働シナリオで承認付与フローを使用する可能性が高くなります。

2. ZuulMavenの依存関係の追加

私たちのプロジェクトにZuulを追加することから始めましょう。 これを行うには、spring-cloud-starter-netflix-zuulアーティファクトを追加します。

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
    <version>2.0.2.RELEASE</version>
</dependency>

3. ズールを有効にする

Zuulを介してルーティングするアプリケーションには、アクセストークンを付与するOAuth 2.0認証サーバーと、アクセストークンを受け入れるリソースサーバーが含まれています。 これらのサービスは、2つの別々のエンドポイントに存在します。

これらのサービスのすべての外部クライアントに単一のエンドポイントを設定し、さまざまなパスをさまざまな物理エンドポイントに分岐させたいと考えています。 そのために、エッジサービスとしてZuulを紹介します。

これを行うには、GatewayApplicationという新しいSpring Bootアプリケーションを作成します。 次に、このアプリケーションクラスを @EnableZuulProxy アノテーションで装飾します。これにより、Zuulインスタンスが生成されます。

@EnableZuulProxy
@SpringBootApplication
public class GatewayApplication {
    public static void main(String[] args) {
	SpringApplication.run(GatewayApplication.class, args);
    }
}

4. ズールルートの設定

先に進む前に、いくつかのZuulプロパティを構成する必要があります。 最初に構成するのは、Zuulが着信接続をリッスンしているポートです。 それは/src/main/resources/application.ymlファイルに入れる必要があります:

server:
    port: 8080

さて、楽しいことのために、Zuulが転送する実際のルートを構成します。 そのためには、次のサービス、それらのパス、およびそれらがリッスンするポートに注意する必要があります。

AuthorizationServerは次の場所にデプロイされます。 http:// localhost:8081 / spring-security-oauth-server / oauth

リソースサーバーは次の場所に展開されます。 http:// localhost:8082 / spring-security-oauth-resource

AuthorizationServerはOAuthIDプロバイダーです。 リソースサーバーに認証トークンを提供するために存在し、リソースサーバーはいくつかの保護されたエンドポイントを提供します。

承認サーバーはクライアントにアクセストークンを提供し、クライアントはそのトークンを使用して、リソース所有者に代わってリソースサーバーに対して要求を実行します。 OAuthの用語を簡単に実行すると、これらの概念を把握するのに役立ちます。

次に、これらの各サービスにいくつかのルートをマッピングしましょう。

zuul:
  routes:
    spring-security-oauth-resource:
      path: /spring-security-oauth-resource/**
      url: http://localhost:8082/spring-security-oauth-resource
    oauth:
      path: /oauth/**
      url: http://localhost:8081/spring-security-oauth-server/oauth	 

この時点で、 localhost:8080 / oauth / ** でZuulに到達するすべてのリクエストは、ポート8081で実行されている承認サービスにルーティングされます。 localhost:8080 / spring-security-oauth-resource / ** へのリクエストはすべて、8082で実行されているリソースサーバーにルーティングされます。

5. ズールの外部トラフィックパスの保護

Zuulエッジサービスはリクエストを正しくルーティングするようになりましたが、承認チェックなしでルーティングしています。 / oauth / * の背後にある認証サーバーは、認証が成功するたびにJWTを作成します。 当然、匿名でアクセスできます。

一方、 / spring-security-oauth-resource / ** にあるリソースサーバーは、承認されたクライアントが保護されたリソースにアクセスしていることを確認するために、常にJWTでアクセスする必要があります。

まず、JWTを通過してその背後にあるサービスに渡るようにZuulを構成します。 この場合、これらのサービス自体がトークンを検証する必要があります。

これを行うには、 sensitiveHeaders:Cookie、Set-Cookieを追加します。

これでZuulの構成は完了です。

server:
  port: 8080
zuul:
  sensitiveHeaders: Cookie,Set-Cookie
  routes:
    spring-security-oauth-resource:
      path: /spring-security-oauth-resource/**
      url: http://localhost:8082/spring-security-oauth-resource
    oauth:
      path: /oauth/**
      url: http://localhost:8081/spring-security-oauth-server/oauth

それが邪魔になった後、エッジで承認に対処する必要があります。 現在、ZuulはJWTをダウンストリームサービスに渡す前に検証しません。 これらのサービスはJWT自体を検証しますが、理想的には、エッジサービスに最初にそれを実行させ、許可されていないリクエストがアーキテクチャに深く伝播する前に拒否するようにします。

Springセキュリティを設定して、Zuulで承認がチェックされるようにします。

まず、Springセキュリティの依存関係をプロジェクトに取り込む必要があります。 spring-security-oauth2spring-security-jwt:が必要です

<dependency>
    <groupId>org.springframework.security.oauth</groupId>
    <artifactId>spring-security-oauth2</artifactId>
    <version>2.3.3.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-jwt</artifactId>
    <version>1.0.9.RELEASE</version>
</dependency>

次に、 ResourceServerConfigurerAdapter:を拡張して、保護するルートの構成を記述します。

@Configuration
@Configuration
@EnableResourceServer
public class GatewayConfiguration extends ResourceServerConfigurerAdapter {
    @Override
    public void configure(final HttpSecurity http) throws Exception {
	http.authorizeRequests()
          .antMatchers("/oauth/**")
          .permitAll()
          .antMatchers("/**")
	  .authenticated();
    }
}

GatewayConfiguration クラスは、SpringSecurityがZuulを介して着信HTTP要求を処理する方法を定義します。 configure メソッド内では、最初に antMatchers を使用して最も制限の厳しいパスを照合し、次にpermitAllを介して匿名アクセスを許可しました。

つまり、 / oauth / ** に着信するすべてのリクエストは、認証トークンをチェックせずに許可される必要があります。 これは、認証トークンが生成されるパスであるため、理にかなっています。

次に、他のすべてのパスを/ ** と照合し、認証済みへの呼び出しを通じて、他のすべての呼び出しにアクセストークンを含める必要があると主張しました。

6. JWT検証に使用されるキーの構成

構成が完了したので、 / oauth / ** パスにルーティングされたすべてのリクエストは匿名で許可されますが、他のすべてのリクエストには認証が必要になります。

ただし、ここで欠落していることが1つあります。それは、JWTが有効であることを確認するために必要な実際の秘密です。 そのためには、JWTの署名に使用されるキー(この場合は対称)を提供する必要があります。 構成コードを手動で記述する代わりに、spring-security-oauth2-autoconfigureを使用できます。

プロジェクトにアーティファクトを追加することから始めましょう。

<dependency>
    <groupId>org.springframework.security.oauth.boot</groupId>
    <artifactId>spring-security-oauth2-autoconfigure</artifactId>
    <version>2.1.2.RELEASE</version>
</dependency>

次に、 application.yaml ファイルに数行の構成を追加して、JWTの署名に使用されるキーを定義する必要があります。

security:
  oauth2:
    resource:
      jwt:
        key-value: 123

key-value:123 は、承認サーバーがJWTに署名するために使用する対称鍵を設定します。 このキーは、spring-security-oauth2-autoconfigureがトークンの解析を構成するために使用します。

本番システムでは、 アプリケーションのソースコードで指定されている対称鍵は使用しないでください。 当然、外部で構成する必要があります。

7. エッジサービスのテスト

7.1. アクセストークンの取得

次に、いくつかのcurlコマンドを使用して、Zuulエッジサービスがどのように動作するかをテストしましょう。

まず、 password grant を使用して、認証サーバーから新しいJWTを取得する方法を確認します。

ここでは、ユーザー名とパスワードをアクセストークンと交換します。 この場合、ユーザー名として「 john 」を使用し、パスワードとして「123」を使用します。

curl -X POST \
  http://localhost:8080/oauth/token \
  -H 'Authorization: Basic Zm9vQ2xpZW50SWRQYXNzd29yZDpzZWNyZXQ=' \
  -H 'Cache-Control: no-cache' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'grant_type=password&password=123&username=john'

この呼び出しによりJWTトークンが生成され、リソースサーバーに対する認証済みリクエストに使用できます。

「Authorization:Basic…」ヘッダーフィールドに注意してください。 これは、どのクライアントが認証サーバーに接続しているかを認証サーバーに通知するために存在します。

ユーザーにとってのユーザー名とパスワードは、クライアント(この場合はcURLリクエスト)に対してです。

{    
    "access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpX...",
    "token_type":"bearer",    
    "refresh_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpX...",
    "expires_in":3599,
    "scope":"foo read write",
    "organization":"johnwKfc",
    "jti":"8e2c56d3-3e2e-4140-b120-832783b7374b"
}

7.2. リソースサーバーリクエストのテスト

次に、承認サーバーから取得したJWTを使用して、リソースサーバーに対してクエリを実行できます。

curl -X GET \
curl -X GET \
  http:/localhost:8080/spring-security-oauth-resource/users/extra \
  -H 'Accept: application/json, text/plain, */*' \
  -H 'Accept-Encoding: gzip, deflate' \
  -H 'Accept-Language: en-US,en;q=0.9' \
  -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV...' \
  -H 'Cache-Control: no-cache' \

Zuulエッジサービスは、リソースサーバーにルーティングする前にJWTを検証するようになりました。

次に、これによりJWTからキーフィールドが抽出され、リクエストに応答する前に、より詳細な認証がチェックされます。

{
    "user_name":"john",
    "scope":["foo","read","write"],
    "organization":"johnwKfc",
    "exp":1544584758,
    "authorities":["ROLE_USER"],
    "jti":"8e2c56d3-3e2e-4140-b120-832783b7374b",
    "client_id":"fooClientIdPassword"
}

8. レイヤー間のセキュリティ

JWTは、リソースサーバーに渡される前にZuulエッジサービスによって検証されていることに注意することが重要です。 JWTが無効な場合、リクエストはエッジサービス境界で拒否されます。

一方、JWTが実際に有効である場合、リクエストはダウンストリームに渡されます。 次に、リソースサーバーはJWTを再度検証し、ユーザースコープ、組織(この場合はカスタムフィールド)、権限などの主要なフィールドを抽出します。 これらのフィールドを使用して、ユーザーが実行できることと実行できないことを決定します。

明確にするために、多くのアーキテクチャでは、実際にJWTを2回検証する必要はありません。これは、トラフィックパターンに基づいて行う必要がある決定です。

たとえば、一部の本番プロジェクトでは、個々のリソースサーバーに直接アクセスすることも、プロキシを介してアクセスすることもできます。両方の場所でトークンを確認する必要がある場合があります。 他のプロジェクトでは、トラフィックはプロキシ経由でのみ送信される場合があります。その場合、トークンが十分にあることを確認します。

9. 概要

これまで見てきたように、Zuulは、サービスのルートを抽象化および定義するための簡単で構成可能な方法を提供します。 Spring Securityとともに、サービス境界でのリクエストを承認することができます。

最後に、いつものように、コードはGithub利用できます。