1. 概要

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

当然、 Spring Security を使用して、 SpringSessionおよびRedisを使用してセッションを共有します。 この方法は、セットアップが簡単で、多くのビジネスシナリオに簡単に拡張できます。 春のセッションに慣れていない場合は、この記事をチェックしてください。

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

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

Redis の概要については、thisチュートリアルをお読みください。 Spring Security の概要については、 spring-security-login role-and-privilege-for-spring-security-registration 、およびspringをお読みください。 -セキュリティセッション Spring Securityを完全に理解するには、[learn-spring-security-the-master-classをご覧ください。

2. Mavenのセットアップ

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

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

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

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

<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

春のセッションに結びつくのは、ディスカバリーゲートウェイブックサービス評価サービスの4つだけです。

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

@EnableRedisHttpSession
public class SessionConfig
  extends AbstractHttpSessionApplicationInitializer {
}

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

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

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

3. 構成サービスの保護

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

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

eureka.client.serviceUrl.defaultZone=
  http://discUser:discPassword@localhost: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」ユーザーでサービスがセットアップされます。 これは、いくつかの工夫を加えた基本的な SpringSecurity構成です。 それらのねじれを見てみましょう:

  • @Order(1) Spring に、このセキュリティフィルターを最初に配線して、他のどのフィルターよりも先に試行されるように指示します。
  • .sessionCreationPolicy Spring に、ユーザーがこのフィルターにログインしたときに常にセッションを作成するように指示します
  • .requestMatchers –このフィルターが適用されるエンドポイントを制限します

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

4.2. ユーレカダッシュボードの保護

検出アプリケーションには、現在登録されているサービスを表示するための優れた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

これらのプロパティにより、検出サービスは起動時にconfigサービスで認証されます。

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

eureka.client.serviceUrl.defaultZone=
  http://discUser:discPassword@localhost:8082/eureka/
eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false

ディスカバリーサービスに基本認証クレデンシャルを追加して、configサービスと通信できるようにしました。 さらに、 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のゲートウェイプロジェクトからプロジェクトにコピーします。

次に、構成クラスの@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 andDiscoveryServiceによる認証

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

spring.cloud.config.username=configUser
spring.cloud.config.password=configPassword
eureka.client.serviceUrl.defaultZone=
  http://discUser:discPassword@localhost: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 /resourcesにあるbootstrap.propertiesファイルに次のプロパティを追加します。

spring.cloud.config.username=configUser
spring.cloud.config.password=configPassword
eureka.client.serviceUrl.defaultZone=
  http://discUser:discPassword@localhost: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();
}

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

7.2. プロパティ

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

spring.cloud.config.username=configUser
spring.cloud.config.password=configPassword
eureka.client.serviceUrl.defaultZone=
  http://discUser:discPassword@localhost:8082/eureka/

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

management.security.sessions=never

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

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

8. 実行とテスト

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

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

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

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

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

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

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

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

response = testRestTemplate
  .getForEntity(testUrl + "/home/index.html", String.class);
Assert.assertEquals(HttpStatus.FOUND, response.getStatusCode());
Assert.assertEquals("http://localhost:8080/login", response.getHeaders()
  .get("Location").get(0));

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

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

MultiValueMap<String, String> 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<String> 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. 結論

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

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

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