Перейти к основному содержимому

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

· 8 мин. чтения

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

Компоненты 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 принимает три свойства:

  • Свойство перехватчика : относится к 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 значение 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 .

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

Тем временем 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();
}

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

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

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

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

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

На данный момент нам, конечно, нужна зависимость spring-security-test :

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

Теперь, несмотря на то, что наш пользователь может отправить сообщение в 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<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 Security в Spring Integration для защиты канала сообщений и ServiceActivator .

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