Безопасность в весенней интеграции
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.