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 實例,它將攔截通道上的所有 send 和 receive 調用,並決定是否允許執行或拒絕這些調用:
@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。
- sendAccess 和 receiveAccess 屬性:包含用於在 Channel 上調用 send 或 receive 操作的策略。
如上例所示,我們期望只有擁有 ROLE_VIEWER 角色或用户名是 jane 的用户才能從 startDirectChannel 發送消息。
此外,只有擁有 ROLE_EDITOR 角色才能將消息發送到 endDirectChannel。
我們通過使用自定義的AccessDecisionManager 實現:RoleVoter 或 UsernameAccessDecisionVoter 返回積極響應,則訪問權限被授予。
4. 安全的 ServiceActivator
值得一提的是,我們還可以通過 Spring Method Security 來安全地保護我們的 ServiceActivator。因此,我們需要啓用方法安全註解:
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends GlobalMethodSecurityConfiguration {
//....
}為了簡化,本文檔僅使用 Spring 的 pre 和 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 默認情況下是線程綁定。這意味着 SecurityContext 不會傳遞到子線程。
對於以上所有示例,我們使用 DirectChannel 和 ServiceActivator – 它們都運行在一個線程中;因此,SecurityContext 在整個流程中可用。
但是,當使用 QueueChannel、ExecutorChannel 和 PublishSubscribeChannel 與 Executor 一起時,消息將從一個線程傳輸到其他線程。在這種情況下,我們需要將 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_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:
- 流程從 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_VIEWER 和 ROLE_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。