Security In Spring Integration – Spring集成中的安全问题

最后修改: 2018年 3月 4日

中文/混合/英文(键盘快捷键:t)

1. Introduction

1.介绍

In this article, we’ll focus on how we can use Spring Integration and Spring Security together in an integration flow.

在这篇文章中,我们将重点讨论如何在集成流程中一起使用Spring Integration和Spring Security。

Therefore, we’ll set up a simple secured message flow to demonstrate the use of Spring Security in Spring Integration. Also, we’ll provide the example of SecurityContext propagation in multithreading message channels.

因此,我们将建立一个简单的安全消息流来演示Spring Integration中Spring Security的使用。同时,我们将提供SecurityContext在多线程消息通道中传播的例子。

For more details of using the framework, you can refer to our introduction to Spring Integration.

有关使用该框架的更多细节,您可以参考我们的Spring Integration介绍

2. Spring Integration Configuration

2.Spring集成配置

2.1. Dependencies

2.1.依赖性

Firstly, we need to add the Spring Integration dependencies to our project.

首先我们需要将Spring集成的依赖关系添加到我们的项目中。

Since we’ll set up a simple message flows with DirectChannel, PublishSubscribeChannel, and ServiceActivator, we need spring-integration-core dependency.

由于我们将用DirectChannelPublishSubscribeChannelServiceActivator设置一个简单的消息流,我们需要spring-integration-core依赖。

Also, we also need the spring-integration-security dependency to be able to use Spring Security in Spring Integration:

此外,我们还需要spring-integration-security依赖,以便能够在Spring Integration中使用Spring Security。

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

And we ‘re also using Spring Security, so we’ll add spring-security-config to our project:

我们也在使用Spring Security,所以我们将在我们的项目中添加spring-security-config

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

We can check out the latest version of all above dependencies at Maven Central: spring-integration-security, spring-security-config.

我们可以在Maven中心查看上述所有依赖的最新版本: spring-integration-security, spring-security-config.

2.2. Java-Based Configuration

2.2.基于Java的配置

Our example will use basic Spring Integration components. Thus, we only need to enable Spring Integration in our project by using @EnableIntegration annotation:

我们的例子将使用基本的Spring集成组件。因此,我们只需要通过使用@EnableIntegration注解在我们的项目中启用Spring集成。

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

3. Secured Message Channel

3.安全的信息通道

First of all, we need an instance of ChannelSecurityInterceptor which will intercept all send and receive calls on a channel and decide if that call can be executed or denied:

首先,我们需要一个ChannelSecurityInterceptor的实例,它将拦截一个通道上的所有sendreceive调用,并决定该调用是否可以被执行或拒绝

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

    ChannelSecurityInterceptor 
      channelSecurityInterceptor = new ChannelSecurityInterceptor();

    channelSecurityInterceptor
      .setAuthenticationManager(authenticationManager);

    channelSecurityInterceptor
      .setAccessDecisionManager(customAccessDecisionManager);

    return channelSecurityInterceptor;
}

The AuthenticationManager and AccessDecisionManager beans are defined as:

AuthenticationManagerAccessDecisionManagerBean被定义为。

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

Here, we use two AccessDecisionVoter: RoleVoter and a custom UsernameAccessDecisionVoter.

这里,我们使用两个AccessDecisionVoterRoleVoter和一个自定义的UsernameAccessDecisionVoter.

Now, we can use that ChannelSecurityInterceptor to secure our channel. What we need to do is decorating the channel by @SecureChannel annotation:

现在,我们可以使用这个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();
}

The @SecureChannel accepts three properties:

@SecureChannel接受三个属性。

  • The interceptor property: refers to a ChannelSecurityInterceptor bean.
  • The sendAccess and receiveAccess properties: contains the policy for invoking send or receive action on a channel.

In the example above, we expect only users who have ROLE_VIEWER or have username jane can send a message from the startDirectChannel.

在上面的例子中,我们希望只有拥有ROLE_VIEWER或拥有用户名jane的用户才能从startDirectChannel发送消息。

Also, only users who have ROLE_EDITOR can send a message to the endDirectChannel.

另外,只有拥有ROLE_EDITOR的用户可以向endDirectChannel发送消息。

We achieve this with the support of our custom AccessDecisionManager: either RoleVoter or UsernameAccessDecisionVoter returns an affirmative response, the access is granted.

我们通过自定义的AccessDecisionManager的支持来实现这一点:RoleVoterUsernameAccessDecisionVoter返回一个肯定的响应,访问权就被授予。

4. Secured ServiceActivator

4.安全的ServiceActivator

It’s worth to mention that we also can secure our ServiceActivator by Spring Method Security. Therefore, we need to enable method security annotation:

值得一提的是,我们也可以通过Spring方法安全来保护我们的ServiceActivator。因此,我们需要启用方法安全注解。

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

For simplicity, in this article, we’ll only use Spring pre and post annotations, so we’ll add the @EnableGlobalMethodSecurity annotation to our configuration class and set prePostEnabled to true.

为了简单起见,在本文中,我们将只使用Spring的pre post注解,所以我们将在配置类中添加@EnableGlobalMethodSecurity注解,并将prePostEnabled设为true

Now we can secure our ServiceActivator with a @PreAuthorization annotation:

现在我们可以用@PreAuthorization注解来保护我们的ServiceActivator

@ServiceActivator(
  inputChannel = "startDirectChannel", 
  outputChannel = "endDirectChannel")
@PreAuthorize("hasRole('ROLE_LOGGER')")
public Message<?> logMessage(Message<?> message) {
    Logger.getAnonymousLogger().info(message.toString());
    return message;
}

The ServiceActivator here receives the message from startDirectChannel and output the message to endDirectChannel.

这里的ServiceActivatorstartDirectChannel接收消息,并将消息输出到endDirectChannel

Besides, the method is accessible only if the current Authentication principal has role ROLE_LOGGER.

此外,只有当当前Authentication委托人具有ROLE_LOGGER角色时,才能访问该方法。

5. Security Context Propagation

5.安全上下文的传播

Spring SecurityContext is thread-bound by default. It means the SecurityContext won’t be propagated to a child-thread.

Spring的SecurityContext默认是线程绑定的。这意味着SecurityContext不会被传播到子线程。

For all above examples, we use both DirectChannel and ServiceActivator – which all run in a single thread; thus, the SecurityContext is available throughout the flow.

在上述所有例子中,我们都使用了DirectChannelServiceActivator–它们都在一个线程中运行;因此,SecurityContext在整个流程中是可用的。

However, when using QueueChannel, ExecutorChannel, and PublishSubscribeChannel with an Executor, messages will be transferred from one thread to others threads. In this case, we need to propagate the SecurityContext to all threads receiving the messages.

然而,当使用QueenChannelExecutorChannelPublishSubscribeChannelExecutor时,消息将从一个线程传输到其他线程。在这种情况下,我们需要将SecurityContext传播给所有接收消息的线程。

Let create another message flow which starts with a PublishSubscribeChannel channel, and two ServiceActivator subscribes to that channel:

让我们创建另一个消息流,它以一个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);
}

In the example above, we have two ServiceActivator subscribe to the startPSChannel. The channel requires an Authentication principal with role ROLE_VIEWER to be able to send a message to it.

在上面的例子中,我们有两个ServiceActivator订阅到startPSChannel。该通道需要一个具有ROLE_VIEWER角色的Authentication委托人,以便能够向其发送消息。

Likewise, we can invoke the changeMessageToRole service only if the Authentication principal has the ROLE_LOGGER role.

同样地,只有当Authentication委托人具有ROLE_LOGGER角色时,我们才能调用changeMessageToRole服务。

Also, the changeMessageToUserName service can only be invoked if the Authentication principal has the role ROLE_VIEWER.

此外,只有当Authentication委托人具有ROLE_VIEWER角色时,才能调用changeMessageToUserName服务。

Meanwhile, the startPSChannel will run with the support of a ThreadPoolTaskExecutor:

同时,startPSChannel将在ThreadPoolTaskExecutor:的支持下运行。

@Bean
public ThreadPoolTaskExecutor executor() {
    ThreadPoolTaskExecutor pool = new ThreadPoolTaskExecutor();
    pool.setCorePoolSize(10);
    pool.setMaxPoolSize(10);
    pool.setWaitForTasksToCompleteOnShutdown(true);
    return pool;
}

Consequently, two ServiceActivator will run in two different threads. To propagate the SecurityContext to those threads, we need to add to our message channel a SecurityContextPropagationChannelInterceptor:

因此,两个ServiceActivator将在两个不同的线程中运行。为了将SecurityContext传播给这些线程,我们需要向我们的消息通道添加一个SecurityContextPropagationChannelInterceptor

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

Notice how we decorated the SecurityContextPropagationChannelInterceptor with the @GlobalChannelInterceptor annotation. We also added our startPSChannel to its patterns property.

注意我们是如何用@GlobalChannelInterceptor注解来装饰SecurityContextPropagationChannelInterceptor 的。我们还将我们的startPSChannel添加到其patterns属性中。

Therefore, above configuration states that the SecurityContext from the current thread will be propagated to any thread derived from startPSChannel.

因此,上述配置指出,当前线程的SecurityContext将被传播到任何从startPSChannel衍生的线程。

6. Testing

6.测试

Let’s start verifying our message flows using some JUnit tests.

让我们开始使用一些JUnit测试来验证我们的消息流。

6.1. Dependency

6.1.依赖性

We, of course, need the spring-security-test dependency at this point:

当然,我们在这一点上需要spring-security-test依赖。

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

Likewise, the latest version can be checked out from Maven Central: spring-security-test.

同样,最新版本也可以从Maven中心检查出来: spring-security-test.

6.2. Test Secured Channel

6.2.测试安全通道

Firstly, we try to send a message to our startDirectChannel:

首先,我们尝试向我们的startDirectChannel发送一个消息:

@Test(expected = AuthenticationCredentialsNotFoundException.class)
public void 
  givenNoUser_whenSendToDirectChannel_thenCredentialNotFound() {

    startDirectChannel
      .send(new GenericMessage<String>(DIRECT_CHANNEL_MESSAGE));
}

Since the channel is secured, we expect an AuthenticationCredentialsNotFoundException exception when sending the message without providing an authentication object.

由于通道是安全的,当发送消息而不提供认证对象时,我们期望出现AuthenticationCredentialsNotFoundException异常。

Next, we provide a user who has role ROLE_VIEWER, and sends a message to our startDirectChannel:

接下来,我们提供一个拥有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));
 }

Now, even though our user can send the message to startDirectChannel because he has role ROLE_VIEWER, but he cannot invoke the logMessage service which requests user with role ROLE_LOGGER.

现在,即使我们的用户可以向startDirectChannel发送消息,因为他有ROLE_VIEWER角色,但他不能调用logMessage服务,该服务要求用户有ROLE_LOGGER角色。

In this case, a MessageHandlingException which has the cause is AcessDeniedException will be thrown.

在这种情况下,将抛出一个MessageHandlingException,其原因是AcessDeniedException

The test will throw MessageHandlingException with the cause is AccessDeniedExcecption. Hence, we use an instance of ExpectedException rule to verify the cause exception.

该测试将抛出MessageHandlingException,其原因是AccessDeniedExcecption。因此,我们使用ExpectedException规则的一个实例来验证异常的原因。

Next, we provide a user with username jane and two roles: ROLE_LOGGER and ROLE_EDITOR.

接下来,我们提供一个用户名为jane的用户和两个角色。ROLE_LOGGERROLE_EDITOR。

Then try to send a message to startDirectChannel again:

然后尝试向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());
}

The message will travel successfully throughout our flow starting with startDirectChannel to logMessage activator, then go to endDirectChannel. That’s because the provided authentication object has all required authorities to access those components.

消息将成功地贯穿我们的流程,从startDirectChannellogMessage激活器,然后到endDirectChannel。这是因为所提供的认证对象拥有访问这些组件的所有必要权限。

6.3. Test SecurityContext Propagation

6.3.测试SecurityContext的传播

Before declaring the test case, we can review the whole flow of our example with the PublishSubscribeChannel:

在声明测试用例之前,我们可以通过PublishSubscribeChannel来回顾我们的例子的整个流程。

  • The flow starts with a startPSChannel which have the policy sendAccess = “ROLE_VIEWER”
  • Two ServiceActivator subscribe to that channel: one has security annotation @PreAuthorize(“hasRole(‘ROLE_LOGGER’)”) , and one has security annotation @PreAuthorize(“hasRole(‘ROLE_VIEWER’)”)

And so, first we provide a user with role ROLE_VIEWER and try to send a message to our channel:

因此,首先我们提供一个具有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"));
}

Since our user only has role ROLE_VIEWER, the message can only pass through startPSChannel and one ServiceActivator.

由于我们的用户只有角色ROLE_VIEWER,消息只能通过startPSChannel和一个ServiceActivator

Hence, at the end of the flow, we only receive one message.

因此,在流程结束时,我们只收到一条信息。

Let’s provide a user with both roles ROLE_VIEWER and ROLE_LOGGER:

让我们为一个用户提供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"));
}

Now, we can receive both messages at the end of our flow because the user has all required authorities it needs.

现在,我们可以在流程结束时收到这两条信息,因为用户拥有它所需要的所有权限。

7. Conclusion

7.结论

In this tutorial, we’ve explored the possibility of using Spring Security in Spring Integration to secure message channel and ServiceActivator.

在本教程中,我们探讨了在Spring Integration中使用Spring Security来保护消息通道和ServiceActivator的可能性。

As always, we can find all examples over on Github.

一如既往,我们可以在Github上找到所有的例子over