Integração de segurança na primavera
1. Introdução
Neste artigo, vamos nos concentrar em como podemos usar Spring Integration e Spring Security juntos em um fluxo de integração.
Portanto, vamos configurar um fluxo de mensagens simples e seguro para demonstrar o uso de Spring Security na integração Spring. Além disso, forneceremos o exemplo de propagação deSecurityContext em canais de mensagem multithreading.
Para obter mais detalhes sobre o uso da estrutura, você pode consultar nossointroduction to Spring Integration.
2. Configuração de integração Spring
2.1. Dependências
Primeiramente,, precisamos adicionar as dependências de integração do Spring ao nosso projeto.
Como vamos configurar fluxos de mensagens simples comDirectChannel,PublishSubscribeChannel eServiceActivator,, precisamos da dependência despring-integration-core.
Além disso, também precisamos da dependênciaspring-integration-security para poder usar Spring Security na integração Spring:
org.springframework.integration
spring-integration-security
5.0.3.RELEASE
E também estamos usando Spring Security, então adicionaremosspring-security-config ao nosso projeto:
org.springframework.security
spring-security-config
5.0.3.RELEASE
Podemos verificar a versão mais recente de todas as dependências acima na Central Maven:spring-integration-security, spring-security-config.
2.2. Configuração baseada em Java
Nosso exemplo usará componentes básicos do Spring Integration. Portanto, só precisamos habilitar a Integração Spring em nosso projeto usando a anotação@EnableIntegration:
@Configuration
@EnableIntegration
public class SecuredDirectChannel {
//...
}
3. Canal de mensagem seguro
Em primeiro lugar,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;
}
Os beansAuthenticationManagereAccessDecisionManager são definidos como:
@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;
}
}
Aqui, usamos doisAccessDecisionVoter:RoleVoter e umUsernameAccessDecisionVoter. personalizado
Agora, podemos usar esseChannelSecurityInterceptor para proteger nosso canal. O que precisamos fazer é decorar o canal com a anotação@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();
}
O@SecureChannel aceita três propriedades:
-
A propriedadeinterceptor: refere-se a um beanChannelSecurityInterceptor.
-
As propriedadessendAccessereceiveAccess: contém a política para invocar a açãosend oureceive em um canal.
No exemplo acima, esperamos que apenas usuários comROLE_VIEWER ou nome de usuáriojane possam enviar uma mensagem destartDirectChannel.
Além disso, apenas os usuários que têmROLE_EDITOR podem enviar uma mensagem paraendDirectChannel.
Conseguimos isso com o suporte de nossoAccessDecisionManager: personalizado, tantoRoleVoter ouUsernameAccessDecisionVoter retorna uma resposta afirmativa, o acesso é concedido.
4. ProtegidoServiceActivator
Vale a pena mencionar que também podemos proteger nossoServiceActivator por Spring Method Security. Portanto, precisamos ativar a anotação de segurança do método:
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends GlobalMethodSecurityConfiguration {
//....
}
Para simplificar, neste artigo, usaremos apenas as anotações Springpreepost, então adicionaremos a anotação@EnableGlobalMethodSecurity à nossa classe de configuração e definiremosprePostEnabled para true.
Agora podemos proteger nossoServiceActivator com uma anotação@PreAuthorization:
@ServiceActivator(
inputChannel = "startDirectChannel",
outputChannel = "endDirectChannel")
@PreAuthorize("hasRole('ROLE_LOGGER')")
public Message> logMessage(Message> message) {
Logger.getAnonymousLogger().info(message.toString());
return message;
}
OServiceActivator aqui recebe a mensagem destartDirectChannele envia a mensagem paraendDirectChannel.
Além disso, o método é acessível apenas se o principalAuthentication atual tiver a funçãoROLE_LOGGER.
5. Propagação do contexto de segurança
Spring SecurityContext is thread-bound by default. Isso significa queSecurityContext não será propagado para um thread filho.
Para todos os exemplos acima, usamosDirectChannel eServiceActivator - todos executados em um único encadeamento; assim, oSecurityContext está disponível em todo o fluxo.
No entanto,when using QueueChannel, ExecutorChannel, and PublishSubscribeChannel with an Executor, messages will be transferred from one thread to others threads. Neste caso, precisamos propagar oSecurityContext para todos os threads que recebem as mensagens.
Vamos criar outro fluxo de mensagens que começa com um canalPublishSubscribeChannel, e doisServiceActivator se inscreve nesse canal:
@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);
}
No exemplo acima, temos doisServiceActivator inscritos nostartPSChannel. O canal requer um principalAuthentication com funçãoROLE_VIEWER para poder enviar uma mensagem para ele.
Da mesma forma, podemos invocar o serviçochangeMessageToRole apenas se o principalAuthentication tiver a funçãoROLE_LOGGER.
Além disso, o serviçochangeMessageToUserName só pode ser chamado se o principalAuthentication tiver a funçãoROLE_VIEWER.
Enquanto isso, ostartPSChannel será executado com o suporte de umThreadPoolTaskExecutor:
@Bean
public ThreadPoolTaskExecutor executor() {
ThreadPoolTaskExecutor pool = new ThreadPoolTaskExecutor();
pool.setCorePoolSize(10);
pool.setMaxPoolSize(10);
pool.setWaitForTasksToCompleteOnShutdown(true);
return pool;
}
Consequentemente, doisServiceActivator serão executados em dois threads diferentes. 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();
}
Observe como decoramosSecurityContextPropagationChannelInterceptor com a anotação@GlobalChannelInterceptor. Também adicionamos nossostartPSChannel à sua propriedadepatterns.
Portanto, a configuração acima indica queSecurityContext do encadeamento atual será propagado para qualquer encadeamento derivado destartPSChannel.
6. Teste
Vamos começar a verificar nossos fluxos de mensagens usando alguns testes JUnit.
6.1. Dependência
Obviamente, precisamos da dependência despring-security-test neste ponto:
org.springframework.security
spring-security-test
5.0.3.RELEASE
test
Da mesma forma, a versão mais recente pode ser verificada no Maven Central:spring-security-test.
6.2. Canal seguro de teste
Em primeiro lugar, tentamos enviar uma mensagem para o nossostartDirectChannel:
@Test(expected = AuthenticationCredentialsNotFoundException.class)
public void
givenNoUser_whenSendToDirectChannel_thenCredentialNotFound() {
startDirectChannel
.send(new GenericMessage(DIRECT_CHANNEL_MESSAGE));
}
Como o canal está protegido, esperamos uma exceçãoAuthenticationCredentialsNotFoundException ao enviar a mensagem sem fornecer um objeto de autenticação.
Em seguida, fornecemos um usuário que tem a funçãoROLE_VIEWER, e envia uma mensagem para o nossostartDirectChannel:
@Test
@WithMockUser(roles = { "VIEWER" })
public void
givenRoleViewer_whenSendToDirectChannel_thenAccessDenied() {
expectedException.expectCause
(IsInstanceOf. instanceOf(AccessDeniedException.class));
startDirectChannel
.send(new GenericMessage(DIRECT_CHANNEL_MESSAGE));
}
Agora, embora nosso usuário possa enviar a mensagem parastartDirectChannel porque ele tem a funçãoROLE_VIEWER, ele não pode chamar o serviçologMessage que solicita o usuário com a funçãoROLE_LOGGER.
Neste caso, umMessageHandlingException que tem a causa éAcessDeniedException será lançado.
O teste lançaráMessageHandlingException com a causaAccessDeniedExcecption. Portanto, usamos uma instância da regraExpectedException para verificar a exceção de causa.
A seguir, fornecemos a um usuário o nome de usuáriojanee duas funções:ROLE_LOGGEReROLE_EDITOR.
Em seguida, tente enviar uma mensagem parastartDirectChannel novamente:
@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());
}
A mensagem viajará com sucesso por todo o nosso fluxo, começando comstartDirectChannel até o ativadorlogMessage, e então vá paraendDirectChannel. Isso ocorre porque o objeto de autenticação fornecido tem todas as autoridades necessárias para acessar esses componentes.
6.3. Teste de propagaçãoSecurityContext
Antes de declarar o caso de teste, podemos revisar todo o fluxo do nosso exemplo comPublishSubscribeChannel:
-
O fluxo começa com umstartPSChannel que tem a políticasendAccess = “ROLE_VIEWER”
-
DoisServiceActivator se inscrevem nesse canal: um tem a anotação de segurança@PreAuthorize(“hasRole(‘ROLE_LOGGER')”) e o outro tem a anotação de segurança@PreAuthorize(“hasRole(‘ROLE_VIEWER')”)
E então, primeiro fornecemos a um usuário a funçãoROLE_VIEWER e tentamos enviar uma mensagem ao nosso canal:
@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.
Portanto, no final do fluxo, recebemos apenas uma mensagem.
Vamos fornecer a um usuário as funçõesROLE_VIEWER eROLE_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"));
}
Agora, podemos receber as duas mensagens no final do nosso fluxo, porque o usuário possui todas as autoridades necessárias de que precisa.
7. Conclusão
Neste tutorial, exploramos a possibilidade de usar Spring Security na integração Spring para proteger o canal de mensagem eServiceActivator.
Como sempre, podemos encontrar todos os exemplosover on Github.