Sicherheit im Frühling Integration

Sicherheit im Frühjahr Integration

1. Einführung

In diesem Artikel konzentrieren wir uns darauf, wie wir Spring Integration und Spring Security zusammen in einem Integrationsablauf verwenden können.

Daher richten wir einen einfachen gesicherten Nachrichtenfluss ein, um die Verwendung von Spring Security in Spring Integration zu demonstrieren. Außerdem geben wir ein Beispiel für die Weitergabe vonSecurityContextin Multithreading-Nachrichtenkanälen.

Weitere Informationen zur Verwendung des Frameworks finden Sie in unserenintroduction to Spring Integration.

2. Spring Integration Konfiguration

2.1. Abhängigkeiten

Zunächst, müssen wir die Spring Integration-Abhängigkeiten zu unserem Projekt hinzufügen.

Da wir einen einfachen Nachrichtenfluss mitDirectChannel,PublishSubscribeChannel undServiceActivator, einrichten, benötigen wir die Abhängigkeit vonspring-integration-core.

Außerdem benötigen wir die Abhängigkeit vonspring-integration-security, um Spring Security in Spring Integration verwenden zu können:


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

Außerdem verwenden wir Spring Security, sodass wir unserem Projektspring-security-config hinzufügen:


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

Wir können die neueste Version aller oben genannten Abhängigkeiten bei Maven Central überprüfen:spring-integration-security, spring-security-config.

2.2. Java-basierte Konfiguration

In unserem Beispiel werden grundlegende Spring Integration-Komponenten verwendet. Daher müssen wir die Spring-Integration in unserem Projekt nur mithilfe der Annotation@EnableIntegrationaktivieren:

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

3. Gesicherter Nachrichtenkanal

Zunächstwe 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;
}

Die BohnenAuthenticationManager undAccessDecisionManager sind wie folgt definiert:

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

Hier verwenden wir zweiAccessDecisionVoter:RoleVoter und ein benutzerdefiniertesUsernameAccessDecisionVoter.

Jetzt können wir dieseChannelSecurityInterceptor verwenden, um unseren Kanal zu sichern. Was wir tun müssen, ist den Kanal mit@SecureChannel Annotation zu dekorieren:

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

Das@SecureChannel akzeptiert drei Eigenschaften:

  • Die Eigenschaftinterceptor: bezieht sich auf eineChannelSecurityInterceptor-Bean.

  • Die EigenschaftensendAccess undreceiveAccess: Enthält die Richtlinie zum Aufrufen der Aktionsend oderreceive auf einem Kanal.

Im obigen Beispiel erwarten wir, dass nur Benutzer mitROLE_VIEWER oder Benutzernamejane eine Nachricht vonstartDirectChannel senden können.

Außerdem können nur Benutzer mitROLE_EDITOR eine Nachricht anendDirectChannel senden.

Wir erreichen dies mit der Unterstützung unserer benutzerdefiniertenAccessDecisionManager: entwederRoleVoter oderUsernameAccessDecisionVoter gibt eine positive Antwort zurück, der Zugriff wird gewährt.

4. GesicherteServiceActivator

Es ist erwähnenswert, dass wir unsereServiceActivatorauch durch Spring Method Security sichern können. Daher müssen wir die Annotation zur Methodensicherheit aktivieren:

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

Der Einfachheit halber werden in diesem Artikel nur die Annotationen Springpre undpost verwendet. Daher fügen wir die Annotation@EnableGlobalMethodSecurity unserer Konfigurationsklasse hinzu und setzenprePostEnabled auf true.

Jetzt können wir unsereServiceActivator mit einer@PreAuthorization-Annotation sichern:

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

DieServiceActivator empfangen hier die Nachricht vonstartDirectChannel und geben die Nachricht anendDirectChannel aus.

Außerdem ist die Methode nur dann zugänglich, wenn der aktuelleAuthentication-Prinzipal die RolleROLE_LOGGER besitzt.

5. Weitergabe des Sicherheitskontexts

Spring SecurityContext is thread-bound by default. Dies bedeutet, dassSecurityContextnicht an einen untergeordneten Thread weitergegeben werden.

Für alle obigen Beispiele verwenden wir sowohlDirectChannel als auchServiceActivator - die alle in einem einzigen Thread ausgeführt werden. Somit istSecurityContext während des gesamten Flusses verfügbar.

when using QueueChannel, ExecutorChannel, and PublishSubscribeChannel with an Executor, messages will be transferred from one thread to others threads. In diesem Fall müssen wir dieSecurityContext an alle Threads weitergeben, die die Nachrichten empfangen.

Erstellen Sie einen weiteren Nachrichtenfluss, der mit einemPublishSubscribeChannel-Kanal beginnt und zweiServiceActivator diesen Kanal abonniert:

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

Im obigen Beispiel haben wir zweiServiceActivator, diestartPSChannel. abonnieren. Der Kanal benötigt einenAuthentication-Prinzipal mit der RolleROLE_VIEWER, um eine Nachricht an ihn senden zu können.

Ebenso können wir den Service vonchangeMessageToRolenur aufrufen, wenn der Principal vonAuthenticationdie Rolle vonROLE_LOGGERhat.

Außerdem kann der DienstchangeMessageToUserNamenur aufgerufen werden, wenn der PrinzipalAuthenticationdie RolleROLE_VIEWER hat.

In der Zwischenzeit werden diestartPSChannel mit der Unterstützung vonThreadPoolTaskExecutor: ausgeführt

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

Folglich werden zweiServiceActivator in zwei verschiedenen Threads ausgeführt. 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();
}

Beachten Sie, wie wir dieSecurityContextPropagationChannelInterceptor mit der Annotation@GlobalChannelInterceptor dekoriert haben. Wir haben auch unserestartPSChannel zu seinerpatterns-Eigenschaft hinzugefügt.

Daher besagt die obige Konfiguration, dassSecurityContext vom aktuellen Thread an jeden vonstartPSChannel abgeleiteten Thread weitergegeben werden.

6. Testen

Beginnen wir mit der Überprüfung unserer Nachrichtenflüsse mithilfe einiger JUnit-Tests.

6.1. Abhängigkeit

Wir brauchen an dieser Stelle natürlich die Abhängigkeit vonspring-security-test:


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

Ebenso kann die neueste Version von Maven Central ausgecheckt werden:spring-security-test.

6.2. Testen Sie den gesicherten Kanal

Zunächst versuchen wir, eine Nachricht an unserestartDirectChannel: zu senden

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

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

Da der Kanal gesichert ist, erwarten wir beim Senden der Nachricht ohne Angabe eines Authentifizierungsobjekts eine Ausnahme vonAuthenticationCredentialsNotFoundException.

Als Nächstes stellen wir einen Benutzer bereit, der die RolleROLE_VIEWER, hat und eine Nachricht an unserestartDirectChannel sendet:

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

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

Obwohl unser Benutzer die Nachricht anstartDirectChannel senden kann, weil er die RolleROLE_VIEWER hat, kann er jetzt nicht den DienstlogMessageaufrufen, der den Benutzer mit der RolleROLE_LOGGER anfordert.

In diesem Fall wird einMessageHandlingException ausgelöst, dessen UrsacheAcessDeniedException ist.

Der Test wirftMessageHandlingException mit der UrsacheAccessDeniedExcecption. Daher verwenden wir eine Instanz derExpectedException-Regel, um die Ursachenausnahme zu überprüfen.

Als Nächstes geben wir einem Benutzer den Benutzernamenjane und zwei Rollen:ROLE_LOGGER undROLE_EDITOR.

Versuchen Sie dann erneut, eine Nachricht anstartDirectChannel: zu senden

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

Die Nachricht wird erfolgreich durch unseren Fluss übertragen, beginnend mitstartDirectChannel bislogMessage Aktivator, und geht dann zuendDirectChannel. Dies liegt daran, dass das bereitgestellte Authentifizierungsobjekt alle erforderlichen Berechtigungen hat, um auf diese Komponenten zuzugreifen.

6.3. TestSecurityContext Ausbreitung

Bevor wir den Testfall deklarieren, können wir den gesamten Ablauf unseres Beispiels mitPublishSubscribeChannel überprüfen:

  • Der Fluss beginnt mit einemstartPSChannel, der die RichtliniesendAccess = “ROLE_VIEWER” hat

  • ZweiServiceActivator abonnieren diesen Kanal: einer hat Sicherheitsanmerkungen@PreAuthorize(“hasRole(‘ROLE_LOGGER')”) und einer hat Sicherheitsanmerkungen@PreAuthorize(“hasRole(‘ROLE_VIEWER')”)

Also geben wir zuerst einem Benutzer die RolleROLE_VIEWER und versuchen, eine Nachricht an unseren Kanal zu senden:

@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.

Daher erhalten wir am Ende des Flusses nur eine Nachricht.

Stellen Sie einem Benutzer beide RollenROLE_VIEWER undROLE_LOGGER zur Verfügung:

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

Jetzt können wir beide Nachrichten am Ende unseres Flusses empfangen, da der Benutzer alle erforderlichen Berechtigungen hat, die er benötigt.

7. Fazit

In diesem Tutorial haben wir die Möglichkeit untersucht, Spring Security in Spring Integration zum Sichern des Nachrichtenkanals und vonServiceActivator zu verwenden.

Wie immer finden wir alle Beispieleover on Github.