Spring Integration 安全性

Spring Security
Remote
1
02:44 AM · Nov 30 ,2025

1. 簡介

在本文中,我們將重點介紹如何使用 Spring Integration 和 Spring Security 在集成流中協同工作。

因此,我們將設置一個簡單的安全消息流,以演示 Spring Security 在 Spring Integration 中的使用。 此外,我們還將提供 SecurityContext 在多線程消息通道中的傳播示例。

有關使用框架的更多詳細信息,您可以參考我們的 Spring Integration 簡介。

2. Spring Integration 配置

2.1. 依賴項

首先,我們需要將 Spring Integration 依賴項添加到我們的項目中。

由於我們將設置簡單的消息流,使用 DirectChannel, PublishSubscribeChannel, 和 ServiceActivator, 我們需要 spring-integration-core 依賴項。

此外,我們還需要 spring-integration-security 依賴項,以便在 Spring Integration 中使用 Spring Security:


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

我們還使用 Spring Security,因此我們將向我們的項目添加 spring-security-config


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

我們可以從 Maven Central 查看以上所有依賴項的最新版本: spring-integration-security, spring-security-config.

2.2. 基於 Java 的配置

我們的示例將使用基本的 Spring Integration 組件。因此,我們只需通過使用 @EnableIntegration 註解在我們的項目中啓用 Spring Integration:

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

3. 安全消息通道

首先,我們需要一個 ChannelSecurityInterceptor 實例,它將攔截所有通道上的 sendreceive 調用,並決定是否允許執行或拒絕這些調用:

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

    ChannelSecurityInterceptor 
      channelSecurityInterceptor = new ChannelSecurityInterceptor();

    channelSecurityInterceptor
      .setAuthenticationManager(authenticationManager);

    channelSecurityInterceptor
      .setAccessDecisionManager(customAccessDecisionManager);

    return channelSecurityInterceptor;
}

AuthenticationManagerAccessDecisionManager 豆定義如下:

@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;
    }
}

這裏,我們使用了兩個 AccessDecisionVoterRoleVoter 和一個自定義的 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 接受三個屬性:

  • interceptor 屬性:指向 ChannelSecurityInterceptor 豆。
  • sendAccessreceiveAccess 屬性:包含用於調用通道上的 sendreceive 操作的策略。

在上面的示例中,我們期望只有具有 ROLE_VIEWER 或用户名 jane 的用户才能從 startDirectChannel 發送消息。

此外,只有具有 ROLE_EDITOR 的用户才能將消息發送到 endDirectChannel

我們通過自定義的 AccessDecisionManagerRoleVoterUsernameAccessDecisionVoter 返回肯定響應,則訪問被授予。

4. 安全的 ServiceActivator

值得一提的是,我們還可以通過 Spring Method Security 來安全我們的 ServiceActivator。因此,我們需要啓用方法安全註解:

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

為了簡化,在本文中,我們將僅使用 Spring 的 prepost 註解,因此我們將向我們的配置類添加 @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. Security Context Propagation

Spring SecurityContext 默認情況下是線程綁定。這意味着 SecurityContext 不會傳遞到子線程。

對於以上所有示例,我們使用 DirectChannelServiceActivator – 它們都運行在一個線程中;因此,SecurityContext 在流程中可用。

但是,當使用 QueueChannelExecutorChannelPublishSubscribeChannelExecutor 結合時,消息將被從一個線程傳輸到其他線程。在這種情況下,我們需要將 SecurityContext 傳遞到所有接收消息的線程。

讓我們創建一個另一個消息流程,該流程從 PublishSubscribeChannel 渠道開始,並且兩個 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);
}

在上面的示例中,我們有兩個 ServiceActivator 訂閲到 startPSChannel。該渠道需要一個 Authentication 主體具有角色 ROLE_VIEWER 才能將消息發送到它。

同樣,只能在 Authentication 主體具有 ROLE_LOGGER 角色時調用 changeMessageToRole 服務。

此外,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;
}

因此,兩個 ServiceActivator 將在兩個不同的線程中運行。為了將 SecurityContext 傳遞到這些線程,我們需要將 SecurityContextPropagationChannelInterceptor 添加到消息渠道中

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

請注意,我們用 @GlobalChannelInterceptor 註解裝飾了 SecurityContextPropagationChannelInterceptor。我們還將 startPSChannel 添加到其 patterns 屬性中。

因此,上述配置説明,當前線程的 SecurityContext 將傳遞到任何從 startPSChannel 派生的線程。

6. 測試

讓我們使用 JUnit 測試來驗證我們的消息流。

6.1. 依賴

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

我們當然需要 spring-security-test 依賴項。

同樣,最新版本可以從 Maven Central 下載:spring-security-test.

6.2. 測試安全通道

@Test(expected = AuthenticationCredentialsNotFoundException.class) public void givenNoUser_whenSendToDirectChannel_thenCredentialNotFound() { startDirectChannel .send(new GenericMessage<String>(DIRECT_CHANNEL_MESSAGE)); }

首先,我們嘗試向我們的 startDirectChannel 發送消息。

由於通道已安全設置,當不提供身份驗證對象時,發送消息將導致 角色的用户,並將消息發送到我們的 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));
}

由於我們的用户可以向 發送消息,因為他擁有 角色,但他無法調用 服務,該服務需要擁有 角色。

在這種情況下,將拋出 異常,其原因

測試將拋出 異常,其原因 。因此,我們使用 規則來驗證異常原因。

接下來,我們提供一個用户名 和兩個角色:

然後再次嘗試向 發送消息:


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

消息將成功地在從 激活器的流程中傳輸,然後到達 。這是因為提供的身份驗證對象具有訪問這些組件所需的全部權限。

6.3. 測試 SecurityContext 傳播

startPSChannel which have the policy sendAccess = “ROLE_VIEWER”</li> <li>Two ServiceActivator subscribe to that channel: one has security annotation @PreAuthorize(“hasRole(‘ROLE_LOGGER’)” , and one has security annotation @PreAuthorize(“hasRole(‘ROLE_VIEWER’)” )</li>

@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, the message can only pass through startPSChannel and one ServiceActivator.</strong>

@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. 結論

在本教程中,我們探討了使用 Spring Security 在 Spring Integration 中安全消息通道和 ServiceActivator 的可能性。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.