Безопасность в интеграции весной

Безопасность в весенней интеграции

1. Вступление

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

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

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

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

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

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

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

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


    org.springframework.integration
    spring-integration-security
    5.0.3.RELEASE

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


    org.springframework.security
    spring-security-config
    5.0.3.RELEASE

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

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

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

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

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

Прежде всего,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:

@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>
          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: содержат политику для вызова действияsend илиreceive на канале.

В приведенном выше примере мы ожидаем, что только пользователи с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 {
    //....
}

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

Теперь мы можем защитить наш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.

Кроме того, метод доступен только в том случае, если текущий принципалAuthentication имеет рольROLE_LOGGER.

5. Распространение контекста безопасности

Spring SecurityContext is thread-bound by default. Это означает, чтоSecurityContext не будет передан дочернему потоку.

Во всех приведенных выше примерах мы используем какDirectChannel, так иServiceActivator - все они выполняются в одном потоке; таким образом,SecurityContext доступен по всему потоку.

Однакоwhen using QueueChannel, ExecutorChannel, and PublishSubscribeChannel with an Executor, messages will be transferred from one thread to others threads. В этом случае нам нужно распространить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, чтобы иметь возможность отправлять ему сообщения.

Точно так же мы можем вызвать службуchangeMessageToRole, только если принципалAuthentication имеет рольROLE_LOGGER.

Кроме того, служба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 будут выполняться в двух разных потоках. To propagate the SecurityContext to those threads, we need to add to our message channel a SecurityContextPropagationChannelInterceptor:

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

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

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

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

Давайте приступим к проверке наших потоков сообщений с помощью некоторых тестов JUnit.

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

Конечно, на этом этапе нам нужна зависимостьspring-security-test:


    org.springframework.security
    spring-security-test
    5.0.3.RELEASE
    test

Точно так же последнюю версию можно получить в Maven Central:spring-security-test.

6.2. Проверить защищенный канал

Сначала мы пытаемся отправить сообщение нашемуstartDirectChannel:

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

    startDirectChannel
      .send(new GenericMessage(DIRECT_CHANNEL_MESSAGE));
}

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

Затем мы предоставляем пользователя с рольюROLE_VIEWER, и отправляем сообщение нашемуstartDirectChannel:

@Test
@WithMockUser(roles = { "VIEWER" })
public void
  givenRoleViewer_whenSendToDirectChannel_thenAccessDenied() {
    expectedException.expectCause
      (IsInstanceOf. instanceOf(AccessDeniedException.class));

    startDirectChannel
      .send(new GenericMessage(DIRECT_CHANNEL_MESSAGE));
 }

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

В этом случае будет брошен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(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(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 иROLE_LOGGER:

@Test
@WithMockUser(username = "user", roles = { "LOGGER", "VIEWER" })
public void
  givenRoleUserAndLogger_whenSendMessageToPSChannel_then2GetMessages()
  throws IllegalStateException, InterruptedException {
    startPSChannel
      .send(new GenericMessage(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.

Как всегда, можно найти все примерыover on Github.