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 .