1. 序章

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

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

フレームワークの使用の詳細については、 SpringIntegrationの概要を参照してください。

2. Spring統合構成

2.1. 依存関係

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

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

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

<dependency>
    <groupId>org.springframework.integration</groupId>
    <artifactId>spring-integration-security</artifactId>
    <version>5.0.3.RELEASE</version>
</dependency>

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

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-config</artifactId>
    <version>5.0.3.RELEASE</version>
</dependency>

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

2.2. Javaベースの構成

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

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

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

まず、チャネル上のすべての送受信呼び出しをインターセプトし、その呼び出しを実行または拒否できるかどうかを決定するChannelSecurityInterceptorのインスタンスが必要です

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

    ChannelSecurityInterceptor 
      channelSecurityInterceptor = new ChannelSecurityInterceptor();

    channelSecurityInterceptor
      .setAuthenticationManager(authenticationManager);

    channelSecurityInterceptor
      .setAccessDecisionManager(customAccessDecisionManager);

    return channelSecurityInterceptor;
}

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

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

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

    @Bean
    public AccessDecisionManager customAccessDecisionManager() {
        List<AccessDecisionVoter<? extends Object>> 
          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

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

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

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

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

@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. セキュリティコンテキストの伝播

SpringSecurityContextはデフォルトでスレッドにバインドされています。 これは、SecurityContextが子スレッドに伝播されないことを意味します。

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

ただし、 QueueChannel、ExecutorChannel、およびPublishSubscribeChannelをExecutorで使用する場合、メッセージは1つのスレッドから他のスレッドに転送されます。 この場合、メッセージを受信するすべてのスレッドに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つあります ServiceActivator 購読する startPSChannel。 チャネルには、 認証役割を持つプリンシパル ROLE_VIEWER それにメッセージを送信できるようにします。

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

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

一方、 startPSChannel は、 ThreadPoolTaskExecutor:のサポートで実行されます。

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

したがって、2つのServiceActivatorは2つの異なるスレッドで実行されます。 これらのスレッドにSecurityContextを伝播するには、メッセージチャネルにSecurityContextPropagationChannelInterceptorを追加する必要があります。

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

SecurityContextPropagationChannelInterceptor@GlobalChannelInterceptorアノテーションで装飾したことに注目してください。 また、startPSChannelpatternsプロパティに追加しました。

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

6. テスト

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

6.1. 依存

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

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-test</artifactId>
    <version>5.0.3.RELEASE</version>
    <scope>test</scope>
</dependency>

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

6.2. 保護されたチャネルのテスト

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

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

    startDirectChannel
      .send(new GenericMessage<String>(DIRECT_CHANNEL_MESSAGE));
}

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

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

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

    startDirectChannel
      .send(new GenericMessage<String>(DIRECT_CHANNEL_MESSAGE));
 }

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

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

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

次に、ユーザーにユーザー名を提供しますジェーンおよび2つの役割: ROLE_LOGGER ROLE_EDITOR。

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

@Test
@WithMockUser(username = "jane", roles = { "LOGGER", "EDITOR" })
public void 
  givenJaneLoggerEditor_whenSendToDirectChannel_thenFlowCompleted() {
    startDirectChannel
      .send(new GenericMessage<String>(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<String>(DIRECT_CHANNEL_MESSAGE));

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

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

ユーザーにはROLE_VIEWERの役割しかないため、メッセージはstartPSChannelと1つのServiceActivatorのみを通過できます。

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

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

@Test
@WithMockUser(username = "user", roles = { "LOGGER", "VIEWER" })
public void 
  givenRoleUserAndLogger_whenSendMessageToPSChannel_then2GetMessages() 
  throws IllegalStateException, InterruptedException {
    startPSChannel
      .send(new GenericMessage<String>(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を保護する可能性について説明しました。

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