Spring Cloud - 安全なサービス

1概要

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

Spring Session Redis を使用してセッションを共有するには、当然 Spring Security を使用します。この方法は設定が簡単で、多くのビジネスシナリオに簡単に拡張できます。もしあなたが Spring Session に慣れていないのなら、 この記事 をチェックしてください。

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

Redisや Spring Security__に慣れていない場合は、この時点でこれらのトピックについて簡単に検討することをお勧めします。この記事の大部分はアプリケーションのためにコピー&ペーストする準備ができていますが、内部で何が起こるかを理解するための代替はありません。

Redis read この チュートリアルの紹介のために。 Spring Security の概要については、read spring-security-login 春のためのロールと特権 セキュリティ登録]、およびリンク:/spring-security-session[spring-security-session]。 Springセキュリティの完全な理解を得るためには、 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番目のステップとして、https://search.maven.org/classic/#search%7Cgav%7C1%7Cg%3A%22org.springframework.session%22%20AND%20a%を使用して各アプリケーションの pom.xml を変更しましょう。 3A%22春セッション%22[春セッション]、https://search.maven.org/classic/#search%7Cgav%7C1%7Cg%3A%22org.springframework.boot%22%20AND%20a%3A% 22スプリングスタートスターターデータ再%22[スプリングブートスターターデータ再]依存関係:

<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つだけが Spring Session に結びつくでしょう:

  • discovery gateway book-service 、および rating-service ** 。

次に、メインアプリケーションファイルと同じディレクトリにある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:[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 Dashboardをセキュリティで保護する

私たちの発見アプリケーションは現在登録されているサービスを見るための素晴らしい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() - すべてを無効にするようSpring Securityに指示する

このフィルタの認証手順 ** 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

基本の認証情報を discovery サービスに追加して、 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 ステータスページのゲートウェイサービスから提供される静的リソースを保護することです。記事を使ってプロジェクトをビルドする場合は、https://github.com/eugenp/tutorials/tree/master/spring-cloud/spring-cloud-bootstrap[Github]にあるゲートウェイプロジェクトから resource/static フォルダーをコピーしますあなたのプロジェクト

設定クラスの @ EnableRedisHttpSession アノテーションを変更します。

@EnableRedisHttpSession(
  redisFlushMode = RedisFlushMode.IMMEDIATE)

セッションの変更をすぐに保持するためにフラッシュモードを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 Discovery Service による認証

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

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

次に、Gitリポジトリの__gateway.properties__を更新しましょう。

[source,text,gutter:,true]

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__クラスをコピーして、このコンテンツでメソッドを上書きします。

[source,java,gutter:,true]

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

====

==== **  6.2. プロパティ**

これらのプロパティを__bootstrap.properties__ファイルに追加します.
ブックサービスの__src/main/resources__:
[ソース、テキスト、溝:、true]----

spring.cloud.config.username = configUser
spring.cloud.config.password = configPassword
eureka.client.serviceUrl.defaultZone =
  http://discUser:[Eメールで保護された]:8082/eureka/----

gitの__book-service.properties__ファイルにプロパティを追加しましょう。
リポジトリ:
[ソース、テキスト、溝:、true]----

management.security.sessions = never

から serviceUrl.defaultZone プロパティを削除できます。 私たちの設定gitリポジトリの book-service.properties ファイル。この 値が bootstrap ファイルに重複しています。

ブックサービスがそれらを拾うようにこれらの変更をコミットすることを忘れないでください。

7。安全性評価サービス

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

7.1。セキュリティ設定

評価サービスを保護するために、 SecurityConfig クラスをコピーします。 ゲートウェイから、次の内容でメソッドを上書きします。 [source、java、gutter:、true]----

@オーバーライド protected void configure(HttpSecurity http){     http.httpBasic()。disable()。authorizeRequests()       .antMatchers( "/rating")。hasRole( "USER")       .antMatchers( "/ratings/all")。hasAnyRole( "USER"、 "ADMIN")。anyRequest()       .authenticated()。and()。csrf()。disable(); }

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

====

====  **  7.2. プロパティ**

これらのプロパティを評価サービスの__src/main/resources__の__bootstrap.properties__ファイルに追加します。

[source,text,gutter:,true]

spring.cloud.config.username=configUser spring.cloud.config.password=configPassword eureka.client.serviceUrl.defaultZone= 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<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 + "/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<String, String> form = new LinkedMultiValueMap<>();
form.add("username", "user");
form.add("password", "password");
response = testRestTemplate
  .postForEntity(testUrl + "/login", form, String.class);

それでは、クッキーからセッションを抽出し、それを次のリクエストに伝播しましょう。

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());

テストを再度実行してください。予想通り、普通のユーザーとしてadmin領域にアクセスすることは制限されています。

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

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());

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

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

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

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

成功!!!

あなたはそれが恋しいですか? 4つの別々のサーバーにログインしなくても、ゲートウェイサービスにログインし、書籍、評価、および発見サービスのコンテンツを閲覧したためです。

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

9結論

クラウドのセキュリティは確かにもっと複雑になります。しかし、 Spring Security Spring Session を利用することで、この重大な問題を簡単に解決できます。

私たちは今、私たちのサービスの周りにセキュリティを備えたクラウドアプリケーションを持っています。 Zuul Spring Session を使用すると、1つのサービスにのみユーザーをログインさせて、その認証をアプリケーション全体に伝達することができます。つまり、アプリケーションを適切なドメインに簡単に分割し、それぞれのドメインを適切に保護することができます。

いつものようにあなたはhttps://github.com/eugenp/tutorials/tree/master/spring-cloud/spring-cloud-bootstrap[GitHub]でソースコードを見つけることができます。