Spring Cloud - 安全なサービス

Spring Cloud –セキュリティサービス

1. 概要

前回の記事Spring Cloud – Bootstrappingでは、基本的なSpring Cloudアプリケーションを作成しました。 この記事では、それを保護する方法を示します。

当然、Spring Securityを使用して、Spring SessionRedisを使用してセッションを共有します。 この方法は、セットアップが簡単で、多くのビジネスシナリオに簡単に拡張できます。 Spring Sessionに慣れていない場合は、this articleを確認してください。

セッションを共有すると、ゲートウェイサービスにユーザーを記録し、その認証をシステムの他のサービスに伝達することができます。

Redis orSpring Securityに慣れていない場合は、この時点でこれらのトピックを簡単に確認することをお勧めします。 記事の多くはアプリケーションにコピー&ペーストできる状態ですが、内部で何が起こるかを理解するための代替手段はありません。

Redisの概要については、thisチュートリアルをお読みください。 Spring Securityの概要については、spring-security-loginrole-and-privilege-for-spring-security-registration、およびspring-security-sessionをお読みください。 Spring Security,を完全に理解するには、learn-spring-security-the-master-classを見てください。

2. Mavenセットアップ

システム内の各モジュールにspring-boot-starter-security依存関係を追加することから始めましょう。


    org.springframework.boot
    spring-boot-starter-security

Spring依存関係管理を使用しているため、spring-boot-starter依存関係のバージョンを省略できます。

2番目のステップとして、各アプリケーションのpom.xmlspring-sessionspring-boot-starter-data-redisの依存関係で変更しましょう。


    org.springframework.session
    spring-session


    org.springframework.boot
    spring-boot-starter-data-redis

discoverygatewaybook-service、およびrating-serviceの4つのアプリケーションのみがSpring Sessionに関連付けられます。

次に、メインアプリケーションファイルと同じディレクトリ内の3つのサービスすべてにセッション構成クラスを追加します。

@EnableRedisHttpSession
public class SessionConfig
  extends AbstractHttpSessionApplicationInitializer {
}

最後に、これらのプロパティをgitリポジトリの3つの*.propertiesファイルに追加します。

spring.redis.host=localhost
spring.redis.port=6379

それでは、サービス固有の構成に移りましょう。

3. 構成サービスの保護

構成サービスには、多くの場合、データベース接続とAPIキーに関連する機密情報が含まれています。 この情報を危険にさらすことはできないので、このサービスに飛び込んで保護しましょう。

構成サービスのsrc/main/resourcesapplication.propertiesファイルにセキュリティプロパティを追加しましょう。

eureka.client.serviceUrl.defaultZone=
  http://discUser:[email protected]:8082/eureka/
security.user.name=configUser
security.user.password=configPassword
security.user.role=SYSTEM

これにより、ディスカバリーでログインするようにサービスがセットアップされます。 さらに、application.propertiesファイルを使用してセキュリティを構成しています。

それでは、検出サービスを構成しましょう。

4. ディスカバリーサービスの保護

ディスカバリサービスは、アプリケーション内のすべてのサービスの場所に関する機密情報を保持しています。 また、これらのサービスの新しいインスタンスを登録します。

悪意のあるクライアントがアクセスすると、システム内のすべてのサービスのネットワークロケーションを学習し、自分の悪意のあるサービスをアプリケーションに登録できるようになります。 ディスカバリサービスを保護することが重要です。

4.1. セキュリティ構成

他のサービスが使用するエンドポイントを保護するためのセキュリティフィルターを追加しましょう。

@Configuration
@EnableWebSecurity
@Order(1)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

   @Autowired
   public void configureGlobal(AuthenticationManagerBuilder auth) {
       auth.inMemoryAuthentication().withUser("discUser")
         .password("discPassword").roles("SYSTEM");
   }

   @Override
   protected void configure(HttpSecurity http) {
       http.sessionManagement()
         .sessionCreationPolicy(SessionCreationPolicy.ALWAYS)
         .and().requestMatchers().antMatchers("/eureka/**")
         .and().authorizeRequests().antMatchers("/eureka/**")
         .hasRole("SYSTEM").anyRequest().denyAll().and()
         .httpBasic().and().csrf().disable();
   }
}

これにより、 ‘SYSTEM‘ユーザーでサービスが設定されます。 これは、いくつかの工夫を加えた基本的なSpring Security構成です。 それらのねじれを見てみましょう:

  • @Order(1)Springに、このセキュリティフィルターを最初に配線して、他のフィルターよりも先に試行されるように指示します。

  • .sessionCreationPolicy –ユーザーがこのフィルターにログインしたときに常にセッションを作成するようにSpringに指示します

  • .requestMatchers –このフィルターが適用されるエンドポイントを制限します

セットアップしたばかりのセキュリティフィルターは、検出サービスのみに関連する分離された認証環境を構成します。

4.2. Eurekaダッシュボードの保護

検出アプリケーションには、現在登録されているサービスを表示するための優れたUIがあるため、2番目のセキュリティフィルターを使用してそれを公開し、これをアプリケーションの残りの認証に関連付けます。 @Order()タグがないということは、これが評価される最後のセキュリティフィルターであることを意味することに注意してください。

@Configuration
public static class AdminSecurityConfig
  extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) {
   http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.NEVER)
     .and().httpBasic().disable().authorizeRequests()
     .antMatchers(HttpMethod.GET, "/").hasRole("ADMIN")
     .antMatchers("/info", "/health").authenticated().anyRequest()
     .denyAll().and().csrf().disable();
   }
}

この構成クラスをSecurityConfigクラス内に追加します。 これにより、UIへのアクセスを制御する2番目のセキュリティフィルターが作成されます。 このフィルターにはいくつかの変わった特徴があります。それらを見てみましょう。

  • httpBasic().disable() –このフィルターのすべての認証手順を無効にするようにSpringSecurityに指示します

  • sessionCreationPolicy –これをNEVERに設定して、このフィルターで保護されているリソースにアクセスする前に、ユーザーがすでに認証されている必要があることを示します。

このフィルターはユーザーセッションを設定することはなく、共有セキュリティコンテキストにデータを入力するためにRedisに依存します。 そのため、認証を提供するために別のサービスであるゲートウェイに依存しています。

4.3. 構成サービスによる認証

検出プロジェクトで、src / main / resourcesのbootstrap.propertiesに2つのプロパティを追加しましょう。

spring.cloud.config.username=configUser
spring.cloud.config.password=configPassword

これらのプロパティを使用すると、起動時に検出サービスが構成サービスで認証されます。

Gitリポジトリのdiscovery.propertiesを更新しましょう

eureka.client.serviceUrl.defaultZone=
  http://discUser:[email protected]:8082/eureka/
eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false

configサービスと通信できるように、discoveryサービスに基本認証資格情報を追加しました。 さらに、サービスに登録しないように指示することにより、スタンドアロンモードで実行するようにEurekaを構成します。

ファイルをgitリポジトリにコミットしましょう。 そうしないと、変更は検出されません。

5. ゲートウェイサービスの保護

ゲートウェイサービスは、アプリケーションを世界に公開したい唯一の部分です。 そのため、認証されたユーザーのみが機密情報にアクセスできるようにするには、セキュリティが必要です。

5.1. セキュリティ構成

検出サービスのようなSecurityConfigクラスを作成し、メソッドを次のコンテンツで上書きしてみましょう。

@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) {
    auth.inMemoryAuthentication().withUser("user").password("password")
      .roles("USER").and().withUser("admin").password("admin")
      .roles("ADMIN");
}

@Override
protected void configure(HttpSecurity http) {
    http.authorizeRequests().antMatchers("/book-service/books")
      .permitAll().antMatchers("/eureka/**").hasRole("ADMIN")
      .anyRequest().authenticated().and().formLogin().and()
      .logout().permitAll().logoutSuccessUrl("/book-service/books")
      .permitAll().and().csrf().disable();
}

この構成は非常に簡単です。 さまざまなエンドポイントを保護するフォームログインでセキュリティフィルターを宣言します。

/ eureka / **のセキュリティは、Eurekaステータスページのゲートウェイサービスから提供する静的リソースを保護することです。 記事を使用してプロジェクトをビルドしている場合は、Githubのゲートウェイプロジェクトからプロジェクトにresource/staticフォルダーをコピーします。

次に、構成クラスの@EnableRedisHttpSessionアノテーションを変更します。

@EnableRedisHttpSession(
  redisFlushMode = RedisFlushMode.IMMEDIATE)

フラッシュモードを即時に設定して、セッションの変更をすぐに保持します。 これは、リダイレクト用の認証トークンの準備に役立ちます。

最後に、ログイン後に認証トークンを転送するZuulFilterを追加しましょう。

@Component
public class SessionSavingZuulPreFilter
  extends ZuulFilter {

    @Autowired
    private SessionRepository repository;

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() {
        RequestContext context = RequestContext.getCurrentContext();
        HttpSession httpSession = context.getRequest().getSession();
        Session session = repository.getSession(httpSession.getId());

        context.addZuulRequestHeader(
          "Cookie", "SESSION=" + httpSession.getId());
        return null;
    }

    @Override
    public String filterType() {
        return "pre";
    }

    @Override
    public int filterOrder() {
        return 0;
    }
}

このフィルターは、ログイン後にリダイレクトされるリクエストを取得し、セッションキーをCookieとしてヘッダーに追加します。 これにより、ログイン後に認証がすべてのバッキングサービスに伝達されます。

5.2. Config and DiscoveryServiceを使用した認証

ゲートウェイサービスのsrc/main/resourcesbootstrap.propertiesファイルに次の認証プロパティを追加しましょう。

spring.cloud.config.username=configUser
spring.cloud.config.password=configPassword
eureka.client.serviceUrl.defaultZone=
  http://discUser:[email protected]:8082/eureka/

次に、Gitリポジトリのgateway.propertiesを更新しましょう

management.security.sessions=always

zuul.routes.book-service.path=/book-service/**
zuul.routes.book-service.sensitive-headers=Set-Cookie,Authorization
hystrix.command.book-service.execution.isolation.thread
    .timeoutInMilliseconds=600000

zuul.routes.rating-service.path=/rating-service/**
zuul.routes.rating-service.sensitive-headers=Set-Cookie,Authorization
hystrix.command.rating-service.execution.isolation.thread
    .timeoutInMilliseconds=600000

zuul.routes.discovery.path=/discovery/**
zuul.routes.discovery.sensitive-headers=Set-Cookie,Authorization
zuul.routes.discovery.url=http://localhost:8082
hystrix.command.discovery.execution.isolation.thread
    .timeoutInMilliseconds=600000

プロパティファイルで設定できるセキュリティフィルタは1つしかないため、セッションを常に生成するセッション管理を追加しました。 次に、Redisのホストとサーバーのプロパティを追加します。

さらに、リクエストをディスカバリサービスにリダイレクトするルートを追加しました。 スタンドアロンの検出サービスはそれ自体に登録されないため、URLスキームでそのサービスを見つける必要があります。

構成gitリポジトリのgateway.propertiesファイルからserviceUrl.defaultZoneプロパティを削除できます。 この値はbootstrapファイルに複製されます。

ファイルをGitリポジトリにコミットしましょう。そうしないと、変更が検出されません。

6. ブックサービスの保護

書籍サービスサーバーは、さまざまなユーザーによって制御される機密情報を保持します。 このサービスは、システム内の保護された情報の漏洩を防ぐために保護する必要があります。

6.1. セキュリティ構成

ブックサービスを保護するために、ゲートウェイからSecurityConfigクラスをコピーし、メソッドを次のコンテンツで上書きします。

@Override
protected void configure(HttpSecurity http) {
    http.httpBasic().disable().authorizeRequests()
      .antMatchers("/books").permitAll()
      .antMatchers("/books/*").hasAnyRole("USER", "ADMIN")
      .authenticated().and().csrf().disable();
}

===

6.2. プロパティ

ブックサービスのsrc/main/resourcesbootstrap.propertiesファイルに次のプロパティを追加します。

spring.cloud.config.username=configUser
spring.cloud.config.password=configPassword
eureka.client.serviceUrl.defaultZone=
  http://discUser:[email protected]:8082/eureka/

gitリポジトリのbook-service.propertiesファイルにプロパティを追加しましょう。

management.security.sessions=never

構成gitリポジトリのbook-service.propertiesファイルからserviceUrl.defaultZoneプロパティを削除できます。 この値はbootstrapファイルに複製されます。

これらの変更をコミットすることを忘れないでください。そうすれば本サービスはそれらを拾います。

7. 評価サービスの確保

評価サービスも保護する必要があります。

7.1. セキュリティ構成

評価サービスを保護するために、ゲートウェイからSecurityConfigクラスをコピーし、メソッドを次のコンテンツで上書きします。

@Override
protected void configure(HttpSecurity http) {
    http.httpBasic().disable().authorizeRequests()
      .antMatchers("/ratings").hasRole("USER")
      .antMatchers("/ratings/all").hasAnyRole("USER", "ADMIN").anyRequest()
      .authenticated().and().csrf().disable();
}

gatewayサービスからconfigureGlobal()メソッドを削除できます。

===

7.2. プロパティ

これらのプロパティを、評価サービスのsrc/main/resourcesbootstrap.propertiesファイルに追加します。

spring.cloud.config.username=configUser
spring.cloud.config.password=configPassword
eureka.client.serviceUrl.defaultZone=
  http://discUser:[email protected]:8082/eureka/

gitリポジトリのrating-service.propertiesファイルにプロパティを追加しましょう。

management.security.sessions=never

構成gitリポジトリのrating-service.propertiesファイルからserviceUrl.defaultZoneプロパティを削除できます。 この値はbootstrapファイルに複製されます。

これらの変更をコミットすることを忘れないでください。これにより、評価サービスがそれらを取得します。

8. 実行とテスト

Redisおよびアプリケーションのすべてのサービス(config, discovery,gateway, book-service,およびrating-service)を開始します。 それではテストしましょう!

まず、gatewayプロジェクトでテストクラスを作成し、テスト用のメソッドを作成しましょう。

public class GatewayApplicationLiveTest {
    @Test
    public void testAccess() {
        ...
    }
}

次に、テストを設定し、テストメソッド内に次のコードスニペットを追加して、保護されていない/book-service/booksリソースにアクセスできることを検証しましょう。

TestRestTemplate testRestTemplate = new TestRestTemplate();
String testUrl = "http://localhost:8080";

ResponseEntity response = testRestTemplate
  .getForEntity(testUrl + "/book-service/books", String.class);
Assert.assertEquals(HttpStatus.OK, response.getStatusCode());
Assert.assertNotNull(response.getBody());

このテストを実行し、結果を確認します。 失敗した場合は、アプリケーション全体が正常に起動し、構成が構成gitリポジトリからロードされたことを確認します。

次に、テストメソッドの最後に次のコードを追加して、保護されたリソースに認証されていないユーザーとしてアクセスしたときに、ユーザーがログインにリダイレクトされることをテストしましょう。

response = testRestTemplate
  .getForEntity(testUrl + "/book-service/books/1", String.class);
Assert.assertEquals(HttpStatus.FOUND, response.getStatusCode());
Assert.assertEquals("http://localhost:8080/login", response.getHeaders()
  .get("Location").get(0));

テストを再度実行し、成功することを確認します。

次に、実際にログインしてから、セッションを使用してユーザー保護された結果にアクセスしましょう。

MultiValueMap form = new LinkedMultiValueMap<>();
form.add("username", "user");
form.add("password", "password");
response = testRestTemplate
  .postForEntity(testUrl + "/login", form, String.class);

ここで、Cookieからセッションを抽出し、次のリクエストに伝達します。

String sessionCookie = response.getHeaders().get("Set-Cookie")
  .get(0).split(";")[0];
HttpHeaders headers = new HttpHeaders();
headers.add("Cookie", sessionCookie);
HttpEntity httpEntity = new HttpEntity<>(headers);

保護されたリソースを要求します。

response = testRestTemplate.exchange(testUrl + "/book-service/books/1",
  HttpMethod.GET, httpEntity, String.class);
Assert.assertEquals(HttpStatus.OK, response.getStatusCode());
Assert.assertNotNull(response.getBody());

テストを再度実行して、結果を確認します。

それでは、同じセッションでadminセクションにアクセスしてみましょう。

response = testRestTemplate.exchange(testUrl + "/rating-service/ratings/all",
  HttpMethod.GET, httpEntity, String.class);
Assert.assertEquals(HttpStatus.FORBIDDEN, response.getStatusCode());

テストを再度実行すると、予想どおり、単純な古いユーザーとしての管理領域へのアクセスが制限されます。

次のテストでは、管理者としてログインし、管理者が保護したリソースにアクセスできることを検証します。

form.clear();
form.add("username", "admin");
form.add("password", "admin");
response = testRestTemplate
  .postForEntity(testUrl + "/login", form, String.class);

sessionCookie = response.getHeaders().get("Set-Cookie").get(0).split(";")[0];
headers = new HttpHeaders();
headers.add("Cookie", sessionCookie);
httpEntity = new HttpEntity<>(headers);

response = testRestTemplate.exchange(testUrl + "/rating-service/ratings/all",
  HttpMethod.GET, httpEntity, String.class);
Assert.assertEquals(HttpStatus.OK, response.getStatusCode());
Assert.assertNotNull(response.getBody());

私たちのテストは大きくなっています! しかし、管理者としてログインすることで、管理リソースにアクセスできることがわかります。

最後のテストは、ゲートウェイを介して検出サーバーにアクセスすることです。 これを行うには、テストの最後にこのコードを追加します。

response = testRestTemplate.exchange(testUrl + "/discovery",
  HttpMethod.GET, httpEntity, String.class);
Assert.assertEquals(HttpStatus.OK, response.getStatusCode());

このテストを最後にもう一度実行して、すべてが機能していることを確認します。 成功!!!

見逃しましたか? ゲートウェイサービスにログインし、4つの別個のサーバーにログインしなくても、ブック、レーティング、およびディスカバリサービスのコンテンツを表示したためです!

Spring Sessionを使用してサーバー間で認証オブジェクトを伝播することにより、ゲートウェイに1回ログインし、その認証を使用して任意の数のバッキングサービスのコントローラーにアクセスできます。

9. 結論

クラウドのセキュリティは確かに複雑になります。 しかし、Spring SecuritySpring Sessionの助けを借りて、この重大な問題を簡単に解決できます。

これで、サービスにセキュリティを備えたクラウドアプリケーションができました。 ZuulSpring Sessionを使用すると、1つのサービスにのみユーザーをログインさせ、その認証をアプリケーション全体に伝達できます。 これは、アプリケーションを適切なドメインに簡単に分割し、適切と思われる各ドメインを保護できることを意味します。

いつものように、ソースコードはGitHubにあります。