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;
}
AuthenticationManager 和 AccessDecisionManager 豆定義如下:
@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 豆。
- sendAccess 和 receiveAccess 屬性:包含用於調用通道上的 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. Security Context Propagation
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。該渠道需要一個 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 的可能性。