Intégration printanière de la sécurité

Intégration de la sécurité au printemps

1. introduction

Dans cet article, nous allons nous concentrer sur la façon dont nous pouvons utiliser Spring Integration et Spring Security ensemble dans un flux d'intégration.

Par conséquent, nous allons configurer un flux de messages sécurisé simple pour démontrer l'utilisation de Spring Security dans Spring Integration. Nous allons également donner l'exemple de la propagation deSecurityContext dans les canaux de messages multithreading.

Pour plus de détails sur l'utilisation du framework, vous pouvez vous référer à nosintroduction to Spring Integration.

2. Configuration de l'intégration Spring

2.1. Les dépendances

Tout d'abord, nous devons ajouter les dépendances Spring Integration à notre projet.

Puisque nous allons configurer un flux de messages simple avecDirectChannel,PublishSubscribeChannel etServiceActivator,, nous avons besoin de la dépendancespring-integration-core.

En outre, nous avons également besoin de la dépendancespring-integration-security pour pouvoir utiliser Spring Security dans Spring Integration:


    org.springframework.integration
    spring-integration-security
    5.0.3.RELEASE

Et nous utilisons également Spring Security, nous allons donc ajouterspring-security-config à notre projet:


    org.springframework.security
    spring-security-config
    5.0.3.RELEASE

Nous pouvons consulter la dernière version de toutes les dépendances ci-dessus sur Maven Central:spring-integration-security, spring-security-config.

2.2. Configuration basée sur Java

Notre exemple utilisera des composants de base d'intégration Spring. Ainsi, nous devons uniquement activer Spring Integration dans notre projet en utilisant l'annotation@EnableIntegration:

@Configuration
@EnableIntegration
public class SecuredDirectChannel {
    //...
}

3. Canal de message sécurisé

Tout d'abord,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;
}

Les beansAuthenticationManager etAccessDecisionManager sont définis comme suit:

@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;
    }
}

Ici, nous utilisons deuxAccessDecisionVoter:RoleVoter et unUsernameAccessDecisionVoter. personnalisé

Maintenant, nous pouvons utiliser cesChannelSecurityInterceptor pour sécuriser notre canal. Ce que nous devons faire est de décorer le canal par l'annotation@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();
}

Le@SecureChannel accepte trois propriétés:

  • La propriétéinterceptor: fait référence à un beanChannelSecurityInterceptor.

  • Les propriétéssendAccess etreceiveAccess: contient la politique pour appeler l'actionsend oureceive sur un canal.

Dans l'exemple ci-dessus, nous nous attendons à ce que seuls les utilisateurs qui ontROLE_VIEWER ou ont le nom d'utilisateurjane puissent envoyer un message à partir desstartDirectChannel.

De plus, seuls les utilisateurs qui ontROLE_EDITOR peuvent envoyer un message auxendDirectChannel.

Nous y parvenons avec le support de notre personnaliséAccessDecisionManager: soitRoleVoter ouUsernameAccessDecisionVoter renvoie une réponse affirmative, l'accès est accordé.

4. ServiceActivator sécurisé

Il convient de mentionner que nous pouvons également sécuriser nosServiceActivator par Spring Method Security. Par conséquent, nous devons activer l'annotation de sécurité de méthode:

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends GlobalMethodSecurityConfiguration {
    //....
}

Pour plus de simplicité, dans cet article, nous n'utiliserons que les annotations Springpre etpost, nous allons donc ajouter l'annotation@EnableGlobalMethodSecurity à notre classe de configuration et définirprePostEnabled sur true.

Maintenant, nous pouvons sécuriser nosServiceActivator avec une annotation@PreAuthorization:

@ServiceActivator(
  inputChannel = "startDirectChannel",
  outputChannel = "endDirectChannel")
@PreAuthorize("hasRole('ROLE_LOGGER')")
public Message logMessage(Message message) {
    Logger.getAnonymousLogger().info(message.toString());
    return message;
}

LeServiceActivator reçoit ici le message destartDirectChannel et le transmet àendDirectChannel.

De plus, la méthode n'est accessible que si le principalAuthentication actuel a le rôleROLE_LOGGER.

5. Propagation du contexte de sécurité

Spring SecurityContext is thread-bound by default. Cela signifie que lesSecurityContextne seront pas propagés à un thread enfant.

Pour tous les exemples ci-dessus, nous utilisons à la foisDirectChannel etServiceActivator - qui s'exécutent tous dans un seul thread; ainsi, leSecurityContext est disponible dans tout le flux.

Cependant,when using QueueChannel, ExecutorChannel, and PublishSubscribeChannel with an Executor, messages will be transferred from one thread to others threads. Dans ce cas, nous devons propager lesSecurityContext à tous les threads recevant les messages.

Créons un autre flux de messages qui commence par un canalPublishSubscribeChannel, et deuxServiceActivator s'abonnent à ce 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);
}

Dans l'exemple ci-dessus, nous avons deuxServiceActivator souscrits auxstartPSChannel. Le canal nécessite un principalAuthentication avec le rôleROLE_VIEWER pour pouvoir lui envoyer un message.

De même, nous ne pouvons appeler le servicechangeMessageToRole que si le principalAuthentication a le rôleROLE_LOGGER.

De plus, le servicechangeMessageToUserName ne peut être appelé que si le principalAuthentication a le rôleROLE_VIEWER.

Pendant ce temps, lestartPSChannel fonctionnera avec le support d'unThreadPoolTaskExecutor:

@Bean
public ThreadPoolTaskExecutor executor() {
    ThreadPoolTaskExecutor pool = new ThreadPoolTaskExecutor();
    pool.setCorePoolSize(10);
    pool.setMaxPoolSize(10);
    pool.setWaitForTasksToCompleteOnShutdown(true);
    return pool;
}

Par conséquent, deuxServiceActivator s'exécuteront dans deux threads différents. 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();
}

Remarquez comment nous avons décoré lesSecurityContextPropagationChannelInterceptor avec l'annotation@GlobalChannelInterceptor. Nous avons également ajouté notrestartPSChannel à sa propriétépatterns.

Par conséquent, la configuration ci-dessus indique que lesSecurityContext du thread actuel seront propagés à n'importe quel thread dérivé destartPSChannel.

6. Essai

Commençons par vérifier nos flux de messages à l'aide de certains tests JUnit.

6.1. Dépendance

Nous avons bien sûr besoin de la dépendancespring-security-test à ce stade:


    org.springframework.security
    spring-security-test
    5.0.3.RELEASE
    test

De même, la dernière version peut être extraite de Maven Central:spring-security-test.

6.2. Tester le canal sécurisé

Tout d'abord, nous essayons d'envoyer un message à nosstartDirectChannel:

@Test(expected = AuthenticationCredentialsNotFoundException.class)
public void
  givenNoUser_whenSendToDirectChannel_thenCredentialNotFound() {

    startDirectChannel
      .send(new GenericMessage(DIRECT_CHANNEL_MESSAGE));
}

Puisque le canal est sécurisé, nous attendons une exceptionAuthenticationCredentialsNotFoundException lors de l'envoi du message sans fournir d'objet d'authentification.

Ensuite, nous fournissons un utilisateur qui a le rôleROLE_VIEWER, et envoie un message à nosstartDirectChannel:

@Test
@WithMockUser(roles = { "VIEWER" })
public void
  givenRoleViewer_whenSendToDirectChannel_thenAccessDenied() {
    expectedException.expectCause
      (IsInstanceOf. instanceOf(AccessDeniedException.class));

    startDirectChannel
      .send(new GenericMessage(DIRECT_CHANNEL_MESSAGE));
 }

Maintenant, même si notre utilisateur peut envoyer le message àstartDirectChannel car il a le rôleROLE_VIEWER, mais il ne peut pas invoquer le servicelogMessage qui demande l'utilisateur avec le rôleROLE_LOGGER.

Dans ce cas, unMessageHandlingException dont la cause estAcessDeniedException sera lancé.

Le test lanceraMessageHandlingException avec la causeAccessDeniedExcecption. Par conséquent, nous utilisons une instance de la règleExpectedException pour vérifier l'exception de cause.

Ensuite, nous fournissons à un utilisateur le nom d'utilisateurjane et deux rôles:ROLE_LOGGER etROLE_EDITOR.

Ensuite, essayez à nouveau d'envoyer un message à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());
}

Le message voyagera avec succès tout au long de notre flux en commençant parstartDirectChannel à l'activateurlogMessage, puis passera àendDirectChannel. En effet, l’objet d’authentification fourni dispose de toutes les autorisations requises pour accéder à ces composants.

6.3. Propagation du testSecurityContext

Avant de déclarer le cas de test, nous pouvons revoir tout le flux de notre exemple avec lesPublishSubscribeChannel:

  • Le flux commence par unstartPSChannel qui a la politiquesendAccess = “ROLE_VIEWER”

  • DeuxServiceActivator souscrivent à ce canal: l'un a l'annotation de sécurité@PreAuthorize(“hasRole(‘ROLE_LOGGER')”) et l'autre a l'annotation de sécurité@PreAuthorize(“hasRole(‘ROLE_VIEWER')”)

Et donc, d'abord nous fournissons à un utilisateur le rôleROLE_VIEWER et essayons d'envoyer un message à notre 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.

Par conséquent, à la fin du flux, nous ne recevons qu'un seul message.

Fournissons à un utilisateur les deux rôlesROLE_VIEWER etROLE_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"));
}

Nous pouvons désormais recevoir les deux messages à la fin de notre flux, car l'utilisateur dispose de toutes les autorisations requises.

7. Conclusion

Dans ce didacticiel, nous avons exploré la possibilité d'utiliser Spring Security dans Spring Integration pour sécuriser le canal de message et lesServiceActivator.

Comme toujours, nous pouvons trouver tous les exemplesover on Github.