«1. Введение

В этой статье мы сосредоточимся на том, как мы можем использовать Spring Integration и Spring Security вместе в потоке интеграции.

Поэтому мы настроим простой защищенный поток сообщений, чтобы продемонстрировать использование Spring Security в Spring Integration. Также мы приведем пример распространения SecurityContext в многопоточных каналах сообщений.

Для получения более подробной информации об использовании фреймворка вы можете обратиться к нашему введению в Spring Integration.

2. Конфигурация интеграции Spring

2.1. Зависимости

Во-первых, нам нужно добавить в наш проект зависимости Spring Integration.

Поскольку мы настроим простые потоки сообщений с помощью DirectChannel, PublishSubscribeChannel и ServiceActivator, нам понадобится зависимость spring-integration-core.

Кроме того, нам также нужна зависимость spring-integration-security, чтобы иметь возможность использовать Spring Security в Spring Integration:

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

И мы также используем Spring Security, поэтому мы добавим spring- security-config в наш проект:

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

Мы можем проверить последнюю версию всех вышеперечисленных зависимостей в Maven Central: spring-integration-security, spring-security-config.

2.2. Конфигурация на основе Java

В нашем примере будут использоваться базовые компоненты Spring Integration. Таким образом, нам нужно только включить Spring Integration в нашем проекте, используя аннотацию @EnableIntegration:

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

3. Защищенный канал сообщений

Прежде всего, нам нужен экземпляр ChannelSecurityInterceptor, который будет перехватывать все отправленные и полученные вызовы на канале и решить, может ли этот вызов быть выполнен или отклонен:

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

    ChannelSecurityInterceptor 
      channelSecurityInterceptor = new ChannelSecurityInterceptor();

    channelSecurityInterceptor
      .setAuthenticationManager(authenticationManager);

    channelSecurityInterceptor
      .setAccessDecisionManager(customAccessDecisionManager);

    return channelSecurityInterceptor;
}

Bean-компоненты 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: относится к bean-компоненту ChannelSecurityInterceptor. Свойства sendAccess и receiveAccess: содержат политику для вызова действия отправки или получения на канале.

В приведенном выше примере мы ожидаем, что только пользователи, у которых есть ROLE_VIEWER или имя пользователя jane, могут отправлять сообщения из startDirectChannel.

Кроме того, только пользователи с ROLE_EDITOR могут отправлять сообщения в endDirectChannel.

Мы достигаем этого с помощью нашего пользовательского AccessDecisionManager: либо RoleVoter, либо UsernameAccessDecisionVoter возвращает утвердительный ответ, доступ предоставляется.

4. Защищенный ServiceActivator

Стоит отметить, что мы также можем защитить наш ServiceActivator с помощью Spring Method Security. Поэтому нам нужно включить аннотацию безопасности метода:

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

Для простоты в этой статье мы будем использовать только аннотации Spring до и после, поэтому мы добавим аннотацию @EnableGlobalMethodSecurity в наш класс конфигурации и установим prePostEnabled к истине.

Теперь мы можем защитить наш ServiceActivator с помощью аннотации @PreAuthorization:

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

Здесь ServiceActivator получает сообщение от startDirectChannel и выводит сообщение в endDirectChannel.

Кроме того, метод доступен только в том случае, если у текущего принципала аутентификации есть роль 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 мог отправить ему сообщение.

Аналогичным образом мы можем вызвать службу changeMessageToRole, только если субъект аутентификации имеет роль ROLE_LOGGER.

Кроме того, служба changeMessageToUserName может быть вызвана только в том случае, если субъект аутентификации имеет роль ROLE_VIEWER.

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

При этом startPSChannel будет работать с поддержкой ThreadPoolTaskExecutor:

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

Следовательно, два ServiceActivator будут работать в двух разных потоках. Чтобы распространить SecurityContext на эти потоки, нам нужно добавить в наш канал сообщений SecurityContextPropagationChannelInterceptor:

Обратите внимание, как мы украсили SecurityContextPropagationChannelInterceptor аннотацией @GlobalChannelInterceptor. Мы также добавили наш startPSChannel в его свойство Patterns.

Таким образом, приведенная выше конфигурация указывает, что SecurityContext из текущего потока будет распространяться на любой поток, производный от startPSChannel.

6. Тестирование

Давайте начнем проверять наши потоки сообщений, используя некоторые тесты JUnit.

6.1. Зависимость

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-test</artifactId>
    <version>5.0.3.RELEASE</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:

Поскольку канал защищен, мы ожидаем исключение AuthenticationCredentialsNotFoundException при отправке сообщения без предоставления объекта аутентификации.

@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:

Теперь, несмотря на то, что наш пользователь может отправить сообщение в startDirectChannel, поскольку у него есть роль ROLE_VIEWER, но он не может вызвать служба logMessage, которая запрашивает пользователя с ролью ROLE_LOGGER.

В этом случае будет выдано исключение MessageHandlingException, причиной которого является AcessDeniedException.

Тест выдаст MessageHandlingException с причиной AccessDeniedExcecption. Следовательно, мы используем экземпляр правила ExpectedException для проверки причины исключения.

Далее мы предоставляем пользователю имя пользователя 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:

Сообщение будет успешно проходить через наш поток, начиная с startDirectChannel до активатора logMessage, а затем переходит к endDirectChannel. Это связано с тем, что предоставленный объект проверки подлинности имеет все необходимые полномочия для доступа к этим компонентам.

6.3. Проверка распространения контекста безопасности

    Прежде чем объявить тестовый пример, мы можем просмотреть весь поток нашего примера с PublishSubscribeChannel:

Поток начинается с startPSChannel с политикой sendAccess = «ROLE_VIEWER» Два ServiceActivator подписываются на этот канал : у одного есть аннотация безопасности @PreAuthorize(\»hasRole(\»ROLE_LOGGER’)\» ) , а у другого есть аннотация безопасности @PreAuthorize(\»hasRole(\»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 и пытаемся отправить сообщение на наш канал:

Поскольку у нашего пользователя есть только роль ROLE_VIEWER, сообщение может пройти только через startPSChannel и один ServiceActivator.

Следовательно, в конце потока мы получаем только одно сообщение.

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

Давайте предоставим пользователю обе роли ROLE_VIEWER и ROLE_LOGGER:

Теперь мы можем получать оба сообщения в конце нашего потока, потому что у пользователя есть все необходимые полномочия.

7. Заключение

В этом руководстве мы рассмотрели возможность использования Spring Security в Spring Integration для защиты канала сообщений и ServiceActivator.