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 .