1. введение
В этой статье мы сосредоточимся на том, как мы можем использовать Spring Integration и Spring Security вместе в потоке интеграции.
Поэтому мы настроим простой защищенный поток сообщений, чтобы продемонстрировать использование безопасности Spring в интеграции Spring. Кроме того, мы приведем пример Контекста безопасности распространения в многопоточных каналах сообщений.
Для получения более подробной информации об использовании фреймворка вы можете обратиться к нашему введению в интеграцию Spring .
2. Конфигурация Интеграции Пружин
2.1. Зависимости
Во-первых, , нам нужно добавить зависимости интеграции Spring в наш проект.
Поскольку мы настроим простые потоки сообщений с Прямым каналом , 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. Таким образом, нам нужно только включить весеннюю интеграцию в нашем проекте, используя @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>
decisionVoters = new ArrayList<>();
decisionVoters.add(new RoleVoter());
decisionVoters.add(new UsernameAccessDecisionVoter());
AccessDecisionManager accessDecisionManager
= new AffirmativeBased(decisionVoters);
return accessDecisionManager;
}
} Здесь мы используем два AccessDecisionVoter : RoleVoter и пользовательский Username AccessDecisionVoter.
Теперь мы можем использовать этот 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 : относится к компоненту ChannelSecurityInterceptor|/. Свойства
- send Access и receive Access : содержит политику для вызова send или receive действия на канале.
В приведенном выше примере мы ожидаем, что только пользователи, у которых есть ROLE_VIEWER или есть имя пользователя jane , могут отправить сообщение с канала startDirectChannel .
Кроме того, только пользователи, у которых есть ROLE_EDITOR , могут отправить сообщение на конечный прямой канал .
Мы достигаем этого при поддержке нашего пользовательского AccessDecisionManager: либо RoleVoter или UsernameAccessDecisionVoter возвращает утвердительный ответ, доступ предоставлен.
4. Активатор Защищенного сервиса
Стоит отметить, что мы также можем обеспечить безопасность нашего активатора Service/| методом Spring. Поэтому нам необходимо включить аннотацию безопасности метода:
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends GlobalMethodSecurityConfiguration {
//....
}Для простоты в этой статье мы будем использовать только аннотации Spring pre и post , поэтому мы добавим аннотацию @enableglobalmetodsecurity в наш класс конфигурации и установим prePostEnabled в true .
Теперь мы можем защитить наш Активатор услуг с помощью @PreAuthorization аннотации:
@ServiceActivator(
inputChannel = "startDirectChannel",
outputChannel = "endDirectChannel")
@PreAuthorize("hasRole('ROLE_LOGGER')")
public Message> logMessage(Message> message) {
Logger.getAnonymousLogger().info(message.toString());
return message;
}Активатор Службы здесь получает сообщение из start Direct Channel и выводит сообщение в end Direct Channel .
Кроме того, метод доступен только в том случае, если текущий Аутентификация принципал имеет роль ROLE_LOGGER .
5. Распространение контекста безопасности
Spring Контекст безопасности по умолчанию привязан к потоку . Это означает, что контекст безопасности не будет распространяться на дочерний поток.
Для всех приведенных выше примеров мы используем как DirectChannel , так и ServiceActivator – которые все выполняются в одном потоке; таким образом, SecurityContext доступен на протяжении всего потока.
Однако при использовании Канала очереди , ExecutorChannel и Канала подписки на публикацию с Исполнителем сообщения будут передаваться из одного потока в другие потоки . В этом случае нам нужно распространить контекст безопасности на все потоки, получающие сообщения.
Давайте создадим еще один поток сообщений, который начинается с PublishSubscribeChannel канала, и два Активатора службы подписываются на этот канал:
@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);
}В приведенном выше примере у нас есть два Активатора службы подписаться на канал start PS. Каналу требуется Аутентификация принципал с ролью ROLE_VIEWER , чтобы иметь возможность отправить ему сообщение.
Аналогично, мы можем вызвать сообщение change на роль service только в том случае, если Authentication principal имеет роль ROLE_LOGGER .
Кроме того, сообщение change На имя пользователя service может быть вызвано только в том случае, если Аутентификация принципал имеет роль ROLE_VIEWER .
Между тем, канал start PS будет работать при поддержке ThreadPoolTaskExecutor:
@Bean
public ThreadPoolTaskExecutor executor() {
ThreadPoolTaskExecutor pool = new ThreadPoolTaskExecutor();
pool.setCorePoolSize(10);
pool.setMaxPoolSize(10);
pool.setWaitForTasksToCompleteOnShutdown(true);
return pool;
}Следовательно, два Активатора служб будут выполняться в двух разных потоках. Чтобы распространить контекст безопасности на эти потоки, нам нужно добавить в наш канал сообщений SecurityContextPropagationChannelInterceptor :
@Bean
@GlobalChannelInterceptor(patterns = { "startPSChannel" })
public ChannelInterceptor securityContextPropagationInterceptor() {
return new SecurityContextPropagationChannelInterceptor();
}Обратите внимание, как мы украсили перехватчик канала распространения контекста безопасности аннотацией @GlobalChannelInterceptor|/. Мы также добавили наш канал start PS в его свойство patterns .
Поэтому в приведенной выше конфигурации указано, что контекст безопасности из текущего потока будет распространяться на любой поток, производный от 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. Проверка Защищенного Канала
Во-первых, мы пытаемся отправить сообщение на наш прямой канал start:
@Test(expected = AuthenticationCredentialsNotFoundException.class)
public void
givenNoUser_whenSendToDirectChannel_thenCredentialNotFound() {
startDirectChannel
.send(new GenericMessage(DIRECT_CHANNEL_MESSAGE));
} Поскольку канал защищен, мы ожидаем AuthenticationCredentialsNotFoundException исключение при отправке сообщения без предоставления объекта аутентификации.
Затем мы предоставляем пользователю, у которого есть роль ROLE_VIEWER, и отправляем сообщение на наш прямой канал запуска :
@Test
@WithMockUser(roles = { "VIEWER" })
public void
givenRoleViewer_whenSendToDirectChannel_thenAccessDenied() {
expectedException.expectCause
(IsInstanceOf. instanceOf(AccessDeniedException.class));
startDirectChannel
.send(new GenericMessage(DIRECT_CHANNEL_MESSAGE));
} Теперь, хотя наш пользователь может отправить сообщение в start Direct Channel , потому что у него есть роль ROLE_VIEWER , но он не может вызвать службу LogMessage , которая запрашивает пользователя с ролью ROLE_LOGGER .
В этом случае будет вызвано исключение MessageHandlingException , причиной которого является исключение Accessdeniedexception .
Тест выдаст Исключение MessageHandlingException с причиной Исключение отказа в доступе . Следовательно, мы используем экземпляр правила ExpectedException для проверки исключения причины.
Затем мы предоставляем пользователю имя пользователя jane и две роли: ROLE_LOGGER и ROLE_EDITOR.
Затем попробуйте отправить сообщение на запустить прямой канал снова :
@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());
} Сообщение будет успешно перемещаться по всему нашему потоку, начиная с start Direct Channel до LogMessage activator, затем перейдите в endDirectChannel . Это связано с тем, что предоставленный объект аутентификации имеет все необходимые полномочия для доступа к этим компонентам.
6.3. Проверка распространения Контекста Безопасности
Перед объявлением тестового случая мы можем просмотреть весь поток нашего примера с помощью канала Publish Subscribe :
- Поток начинается с канала start PS , который имеет доступ к политике send
- Два Активатора службы подписаться на этот канал: у одного есть аннотация безопасности @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"));
} Поскольку у нашего пользователя есть только роль ROLE_VIEWER , сообщение может проходить только через Канал запуска PS и один Активатор службы .
Следовательно, в конце потока мы получаем только одно сообщение.
Давайте предоставим пользователю обе роли 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 .
Как всегда, мы можем найти все примеры на Github .