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
依赖:
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-core</artifactId>
<version>6.0.0</version>
</dependency>
同时,为了在Spring Integration中使用Spring Security,需要添加spring-integration-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
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
保护通道,通过@SecuredChannel
注解装饰通道:
@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();
}
@SecuredChannel
接受三个属性:
interceptor
:引用ChannelSecurityInterceptor
BeansendAccess
和receiveAccess
:定义调用通道send
或receive
操作的权限策略
示例中,只有拥有ROLE_VIEWER
或用户名为jane
的用户才能向startDirectChannel
发送消息,只有拥有ROLE_EDITOR
的用户才能向endDirectChannel
发送消息。这通过自定义AccessDecisionManager
实现:只要RoleVoter
或UsernameAccessDecisionVoter
任一返回肯定响应即授权通过。
4. 安全的ServiceActivator
注意:我们也可以使用Spring方法安全保护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
或带Executor
的PublishSubscribeChannel
时,消息会从一个线程传递到其他线程。此时需要将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
服务仅当主体拥有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
,并在其patterns
属性中添加startPSChannel
。此配置表示当前线程的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
服务。此时将抛出包含AccessDeniedException
原因的MessageHandlingException
。
现在提供用户名为jane
且拥有ROLE_LOGGER
和ROLE_EDITOR
角色的用户:
@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 测试安全上下文传播
回顾使用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 Integration中使用Spring Security保护消息通道和ServiceActivator
的可能性。
所有示例代码可在GitHub获取。