知識庫 / Spring / Spring Security RSS 訂閱

Spring Integration 安全性

Spring Security
HongKong
10
02:05 PM · Dec 06 ,2025

1. 引言

本文將重點介紹如何結合使用 Spring Integration 和 Spring Security 在集成流中進行應用。

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

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

2. Spring Integration 配置

2.1. 依賴項

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

由於我們將設置簡單的消息流,包括 DirectChannelPublishSubscribeChannelServiceActivator,因此我們需要 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;
}

身份驗證管理器授權決策管理器 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<AccessDecisionVoter<? extends Object>> 
          decisionVoters = new ArrayList<>();
        decisionVoters.add(new RoleVoter());
        decisionVoters.add(new UsernameAccessDecisionVoter());
        AccessDecisionManager accessDecisionManager
          = new AffirmativeBased(decisionVoters);
        return accessDecisionManager;
    }
}

在這裏,我們使用了兩個 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 接受三個屬性:

  • interceptor 屬性:指代ChannelSecurityInterceptor Bean。
  • sendAccessreceiveAccess 屬性:包含用於在 Channel 上調用 sendreceive 操作的策略。

如上例所示,我們期望只有擁有 ROLE_VIEWER 角色或用户名是 jane 的用户才能從 startDirectChannel 發送消息。

此外,只有擁有 ROLE_EDITOR 角色才能將消息發送到 endDirectChannel

我們通過使用自定義的AccessDecisionManager 實現:RoleVoterUsernameAccessDecisionVoter 返回積極響應,則訪問權限被授予。

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. 安全上下文傳播

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。該通道需要一個具有 ROLE_VIEWER 角色的 Authentication 身份驗證主體才能向其發送消息。

同樣,只能在 Authentication 身份驗證主體擁有 ROLE_LOGGER 角色的情況下調用 changeMessageToRole 服務。

此外,只能在 Authentication 身份驗證主體擁有 ROLE_VIEWER 角色的情況下調用 changeMessageToUserName 服務。

與此同時,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();
}

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

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

6. 測試

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

6.1. 依賴

我們需要在此時添加 spring-security-test 依賴項:

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-test</artifactId>
    <version>6.0.0</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,但他無法調用請求擁有 ROLE_LOGGER 角色的 logMessage 服務。

在這種情況下,將會拋出 MessageHandlingException,其原因是由 AcessDeniedException 引起的。

測試將會拋出 MessageHandlingException,其原因是由 AccessDeniedExcecption 引起的。因此,我們使用 ExpectedException 實例來驗證該異常。

接下來,我們為用户名 jane 的用户提供兩個角色: ROLE_LOGGERROLE_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());
}

消息將成功地在我們的流程中流動,從 startDirectChannellogMessage 激活器開始,然後流向 endDirectChannel。 這是因為提供的認證對象擁有訪問這些組件的所有必要權限。

6.3. 測試 SecurityContext 傳播

在聲明測試用例之前,我們可以回顧我們示例的整個流程,使用 PublishSubscribeChannel

  • 流程從 startPSChannel 開始,該通道具有 sendAccess = “ROLE_VIEWER” 策略。
  • 兩個 ServiceActivator 訂閲該通道:一個具有安全註解 @PreAuthorize(“hasRole(‘ROLE_LOGGER’)”),另一個具有安全註解 @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 和一個 ServiceActivator 傳遞。

因此,流程結束時我們只會收到一條消息。

讓我們為用户提供同時擁有 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. 結論

在本教程中,我們探討了如何使用 Spring Security 在 Spring Integration 中安全地保護消息通道和 ServiceActivator

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

發佈 評論

Some HTML is okay.