Spring統合におけるセキュリティ

Spring Integrationのセキュリティ

1. 前書き

この記事では、統合フローでSpringIntegrationとSpringSecurityを一緒に使用する方法に焦点を当てます。

したがって、SpringIntegrationでのSpringSecurityの使用方法を示すために、簡単なセキュリティで保護されたメッセージフローを設定します。 また、マルチスレッドメッセージチャネルでのSecurityContextの伝播の例を示します。

フレームワークの使用の詳細については、introduction to Spring Integrationを参照してください。

2. Spring統合構成

2.1. 依存関係

まず、,で、SpringIntegrationの依存関係をプロジェクトに追加する必要があります。

DirectChannelPublishSubscribeChannel、およびServiceActivator,を使用して単純なメッセージフローを設定するため、spring-integration-coreの依存関係が必要です。

また、SpringIntegrationでSpringSecurityを使用できるようにするには、spring-integration-securityの依存関係も必要です。


    org.springframework.integration
    spring-integration-security
    5.0.3.RELEASE

また、Spring Securityも使用しているため、プロジェクトにspring-security-configを追加します。


    org.springframework.security
    spring-security-config
    5.0.3.RELEASE

上記のすべての依存関係の最新バージョンは、Maven Centralで確認できます:spring-integration-security, spring-security-config.

2.2. Javaベースの構成

この例では、基本的なSpring Integrationコンポーネントを使用します。 したがって、@EnableIntegrationアノテーションを使用して、プロジェクトでSpringIntegrationを有効にするだけで済みます。

@Configuration
@EnableIntegration
public class SecuredDirectChannel {
    //...
}

3. 保護されたメッセージチャネル

まず第一に、we need an instance of ChannelSecurityInterceptor which will intercept all send and receive calls on a channel and decide if that call can be executed or denied

@Autowired
@Bean
public ChannelSecurityInterceptor channelSecurityInterceptor(
  AuthenticationManager authenticationManager,
  AccessDecisionManager customAccessDecisionManager) {

    ChannelSecurityInterceptor
      channelSecurityInterceptor = new ChannelSecurityInterceptor();

    channelSecurityInterceptor
      .setAuthenticationManager(authenticationManager);

    channelSecurityInterceptor
      .setAccessDecisionManager(customAccessDecisionManager);

    return channelSecurityInterceptor;
}

AuthenticationManagerおよびAccessDecisionManager Beanは、次のように定義されます。

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends GlobalMethodSecurityConfiguration {

    @Override
    @Bean
    public AuthenticationManager
      authenticationManager() throws Exception {
        return super.authenticationManager();
    }

    @Bean
    public AccessDecisionManager customAccessDecisionManager() {
        List>
          decisionVoters = new ArrayList<>();
        decisionVoters.add(new RoleVoter());
        decisionVoters.add(new UsernameAccessDecisionVoter());
        AccessDecisionManager accessDecisionManager
          = new AffirmativeBased(decisionVoters);
        return accessDecisionManager;
    }
}

ここでは、2つのAccessDecisionVoterを使用します:RoleVoterとカスタムUsernameAccessDecisionVoter.

これで、そのChannelSecurityInterceptorを使用してチャネルを保護できます。 行う必要があるのは、@SecureChannelアノテーションでチャネルを装飾することです。

@Bean(name = "startDirectChannel")
@SecuredChannel(
  interceptor = "channelSecurityInterceptor",
  sendAccess = { "ROLE_VIEWER","jane" })
public DirectChannel startDirectChannel() {
    return new DirectChannel();
}

@Bean(name = "endDirectChannel")
@SecuredChannel(
  interceptor = "channelSecurityInterceptor",
  sendAccess = {"ROLE_EDITOR"})
public DirectChannel endDirectChannel() {
    return new DirectChannel();
}

@SecureChannelは、次の3つのプロパティを受け入れます。

  • interceptorプロパティ:ChannelSecurityInterceptorBeanを参照します。

  • sendAccessおよびreceiveAccessプロパティ:チャネルでsendまたはreceiveアクションを呼び出すためのポリシーが含まれています。

上記の例では、ROLE_VIEWERまたはユーザー名janeを持つユーザーのみがstartDirectChannelからメッセージを送信できると想定しています。

また、ROLE_EDITORを持つユーザーのみがendDirectChannelにメッセージを送信できます。

これは、カスタムAccessDecisionManager:のサポートにより、RoleVoterまたはUsernameAccessDecisionVoterのいずれかが肯定応答を返し、アクセスが許可されます。

4. 保護されたServiceActivator

SpringメソッドセキュリティによってServiceActivatorを保護することもできます。 したがって、メソッドセキュリティアノテーションを有効にする必要があります。

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends GlobalMethodSecurityConfiguration {
    //....
}

簡単にするために、この記事では、Springpreおよびpostアノテーションのみを使用するため、構成クラスに@EnableGlobalMethodSecurityアノテーションを追加し、prePostEnabledをに設定します。 true

これで、@PreAuthorizationアノテーションを使用してServiceActivatorを保護できます。

@ServiceActivator(
  inputChannel = "startDirectChannel",
  outputChannel = "endDirectChannel")
@PreAuthorize("hasRole('ROLE_LOGGER')")
public Message logMessage(Message message) {
    Logger.getAnonymousLogger().info(message.toString());
    return message;
}

ここでのServiceActivatorは、startDirectChannelからメッセージを受信し、endDirectChannelにメッセージを出力します。

さらに、このメソッドは、現在のAuthenticationプリンシパルがロールROLE_LOGGERを持っている場合にのみアクセスできます。

5. セキュリティコンテキストの伝播

Spring SecurityContext is thread-bound by default。 これは、SecurityContextが子スレッドに伝播されないことを意味します。

上記のすべての例では、DirectChannelServiceActivatorの両方を使用しています。これらはすべて単一のスレッドで実行されます。したがって、SecurityContextはフロー全体で使用できます。

ただし、when using QueueChannel, ExecutorChannel, and PublishSubscribeChannel with an Executor, messages will be transferred from one thread to others threads。 この場合、メッセージを受信するすべてのスレッドにSecurityContextを伝播する必要があります。

PublishSubscribeChannelチャネルで始まり、2つのServiceActivatorがそのチャネルにサブスクライブする別のメッセージフローを作成しましょう。

@Bean(name = "startPSChannel")
@SecuredChannel(
  interceptor = "channelSecurityInterceptor",
  sendAccess = "ROLE_VIEWER")
public PublishSubscribeChannel startChannel() {
    return new PublishSubscribeChannel(executor());
}

@ServiceActivator(
  inputChannel = "startPSChannel",
  outputChannel = "finalPSResult")
@PreAuthorize("hasRole('ROLE_LOGGER')")
public Message changeMessageToRole(Message message) {
    return buildNewMessage(getRoles(), message);
}

@ServiceActivator(
  inputChannel = "startPSChannel",
  outputChannel = "finalPSResult")
@PreAuthorize("hasRole('ROLE_VIEWER')")
public Message changeMessageToUserName(Message message) {
    return buildNewMessage(getUsername(), message);
}

上記の例では、2つのServiceActivatorstartPSChannel.にサブスクライブしています。チャネルにメッセージを送信するには、ロールROLE_VIEWERを持つAuthenticationプリンシパルが必要です。

同様に、AuthenticationプリンシパルがROLE_LOGGERロールを持っている場合にのみ、changeMessageToRoleサービスを呼び出すことができます。

また、changeMessageToUserNameサービスは、AuthenticationプリンシパルがロールROLE_VIEWERを持っている場合にのみ呼び出すことができます。

一方、startPSChannelThreadPoolTaskExecutor:のサポートで実行されます

@Bean
public ThreadPoolTaskExecutor executor() {
    ThreadPoolTaskExecutor pool = new ThreadPoolTaskExecutor();
    pool.setCorePoolSize(10);
    pool.setMaxPoolSize(10);
    pool.setWaitForTasksToCompleteOnShutdown(true);
    return pool;
}

その結果、2つのServiceActivatorが2つの異なるスレッドで実行されます。 To propagate the SecurityContext to those threads, we need to add to our message channel a SecurityContextPropagationChannelInterceptor

@Bean
@GlobalChannelInterceptor(patterns = { "startPSChannel" })
public ChannelInterceptor securityContextPropagationInterceptor() {
    return new SecurityContextPropagationChannelInterceptor();
}

SecurityContextPropagationChannelInterceptor@GlobalChannelInterceptorアノテーションでどのように装飾したかに注目してください。 また、startPSChannelをそのpatternsプロパティに追加しました。

したがって、上記の構成では、現在のスレッドからのSecurityContextは、startPSChannelから派生したすべてのスレッドに伝播されると述べています。

6. テスト

いくつかのJUnitテストを使用してメッセージフローの検証を始めましょう。

6.1. 依存

もちろん、この時点でspring-security-testの依存関係が必要です。


    org.springframework.security
    spring-security-test
    5.0.3.RELEASE
    test

同様に、最新バージョンはMaven Centralからチェックアウトできます:spring-security-test.

6.2. 保護されたチャネルをテストする

まず、startDirectChannel:にメッセージを送信しようとします

@Test(expected = AuthenticationCredentialsNotFoundException.class)
public void
  givenNoUser_whenSendToDirectChannel_thenCredentialNotFound() {

    startDirectChannel
      .send(new GenericMessage(DIRECT_CHANNEL_MESSAGE));
}

チャネルは保護されているため、認証オブジェクトを提供せずにメッセージを送信すると、AuthenticationCredentialsNotFoundException例外が発生することが予想されます。

次に、ロールROLE_VIEWER,を持つユーザーを提供し、startDirectChannelにメッセージを送信します。

@Test
@WithMockUser(roles = { "VIEWER" })
public void
  givenRoleViewer_whenSendToDirectChannel_thenAccessDenied() {
    expectedException.expectCause
      (IsInstanceOf. instanceOf(AccessDeniedException.class));

    startDirectChannel
      .send(new GenericMessage(DIRECT_CHANNEL_MESSAGE));
 }

これで、ユーザーはロールROLE_VIEWERを持っているため、メッセージをstartDirectChannelに送信できますが、ロールROLE_LOGGERを持つユーザーを要求するlogMessageサービスを呼び出すことはできません。

この場合、原因がAcessDeniedExceptionであるMessageHandlingExceptionがスローされます。

テストはMessageHandlingExceptionをスローし、原因はAccessDeniedExcecptionです。 したがって、ExpectedExceptionルールのインスタンスを使用して、原因の例外を確認します。

次に、ユーザーにユーザー名janeと2つの役割(ROLE_LOGGERROLE_EDITOR.)を提供します

次に、startDirectChannelにもう一度メッセージを送信してみてください:

@Test
@WithMockUser(username = "jane", roles = { "LOGGER", "EDITOR" })
public void
  givenJaneLoggerEditor_whenSendToDirectChannel_thenFlowCompleted() {
    startDirectChannel
      .send(new GenericMessage(DIRECT_CHANNEL_MESSAGE));
    assertEquals
      (DIRECT_CHANNEL_MESSAGE, messageConsumer.getMessageContent());
}

メッセージは、startDirectChannelからlogMessageアクティベーターまでのフロー全体で正常に送信され、次にendDirectChannelに移動します。 これは、提供された認証オブジェクトに、これらのコンポーネントにアクセスするために必要なすべての権限があるためです。

6.3. SecurityContext伝搬をテストします

テストケースを宣言する前に、PublishSubscribeChannelを使用して例のフロー全体を確認できます。

  • フローは、ポリシーsendAccess = “ROLE_VIEWER”を持つstartPSChannelで始まります

  • 2つのServiceActivatorがそのチャネルにサブスクライブします。1つにはセキュリティアノテーション@PreAuthorize(“hasRole(‘ROLE_LOGGER')”)があり、もう1つにはセキュリティアノテーション@PreAuthorize(“hasRole(‘ROLE_VIEWER')”)があります。

したがって、最初にユーザーにロールROLE_VIEWERを提供し、チャネルにメッセージを送信しようとします。

@Test
@WithMockUser(username = "user", roles = { "VIEWER" })
public void
  givenRoleUser_whenSendMessageToPSChannel_thenNoMessageArrived()
  throws IllegalStateException, InterruptedException {

    startPSChannel
      .send(new GenericMessage(DIRECT_CHANNEL_MESSAGE));

    executor
      .getThreadPoolExecutor()
      .awaitTermination(2, TimeUnit.SECONDS);

    assertEquals(1, messageConsumer.getMessagePSContent().size());
    assertTrue(
      messageConsumer
      .getMessagePSContent().values().contains("user"));
}

Since our user only has role ROLE_VIEWER, the message can only pass through startPSChannel and one ServiceActivator

したがって、フローの最後で、1つのメッセージのみを受信します。

ユーザーにROLE_VIEWERROLE_LOGGERの両方の役割を与えましょう。

@Test
@WithMockUser(username = "user", roles = { "LOGGER", "VIEWER" })
public void
  givenRoleUserAndLogger_whenSendMessageToPSChannel_then2GetMessages()
  throws IllegalStateException, InterruptedException {
    startPSChannel
      .send(new GenericMessage(DIRECT_CHANNEL_MESSAGE));

    executor
      .getThreadPoolExecutor()
      .awaitTermination(2, TimeUnit.SECONDS);

    assertEquals(2, messageConsumer.getMessagePSContent().size());
    assertTrue
      (messageConsumer
      .getMessagePSContent()
      .values().contains("user"));
    assertTrue
      (messageConsumer
      .getMessagePSContent()
      .values().contains("ROLE_LOGGER,ROLE_VIEWER"));
}

ユーザーには必要なすべての必要な権限があるため、フローの最後に両方のメッセージを受信できるようになりました。

7. 結論

このチュートリアルでは、SpringIntegrationでSpringSecurityを使用して、メッセージチャネルとServiceActivatorを保護する可能性について説明しました。

いつものように、すべての例over on Githubを見つけることができます。