Spring Security 5 für reaktive Anwendungen

1. Einleitung

In diesem Artikel werden neue Funktionen des Spring Security 5-Frameworks zum Sichern reaktiver Anwendungen erläutert. Diese Version ist auf Spring 5 und Spring Boot 2 ausgerichtet.

In diesem Artikel werden wir nicht auf Details zu den reaktiven Anwendungen selbst eingehen, was eine neue Funktion des Spring 5-Frameworks ist. Weitere Informationen finden Sie im Artikel Einführung in den Reaktorkern.

2. Maven Setup

Wir werden Spring Boot-Starter verwenden, um unser Projekt zusammen mit allen erforderlichen Abhängigkeiten zu booten.

Die Grundeinstellung erfordert eine übergeordnete Deklaration, Webstarter- und Sicherheitsstarter-Abhängigkeiten. Wir benötigen außerdem das Spring Security-Testframework:

 org.springframework.boot spring-boot-starter-parent 2.2.6.RELEASE     org.springframework.boot spring-boot-starter-webflux   org.springframework.boot spring-boot-starter-security   org.springframework.security spring-security-test test  

Wir können die aktuelle Version des Spring Boot-Sicherheitsstarters bei Maven Central nachlesen.

3. Projekteinrichtung

3.1. Bootstrapping der reaktiven Anwendung

Wir verwenden nicht die Standardkonfiguration von @SpringBootApplication , sondern konfigurieren stattdessen einen Netty-basierten Webserver. Netty ist ein asynchrones NIO-basiertes Framework, das eine gute Grundlage für reaktive Anwendungen darstellt.

Die Annotation @EnableWebFlux aktiviert die Standardkonfiguration von Spring Web Reactive für die Anwendung:

@ComponentScan(basePackages = {"com.baeldung.security"}) @EnableWebFlux public class SpringSecurity5Application { public static void main(String[] args) { try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( SpringSecurity5Application.class)) { context.getBean(NettyContext.class).onClose().block(); } }

Hier erstellen wir einen neuen Anwendungskontext und warten, bis Netty heruntergefahren ist, indem wir die Kette .onClose (). Block () im Netty-Kontext aufrufen .

Nach dem Herunterfahren von Netty wird der Kontext mithilfe des Blocks " Try-with-Resources" automatisch geschlossen .

Wir müssen auch einen Netty-basierten HTTP-Server, einen Handler für die HTTP-Anforderungen und den Adapter zwischen dem Server und dem Handler erstellen:

@Bean public NettyContext nettyContext(ApplicationContext context) { HttpHandler handler = WebHttpHandlerBuilder .applicationContext(context).build(); ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(handler); HttpServer httpServer = HttpServer.create("localhost", 8080); return httpServer.newHandler(adapter).block(); }

3.2. Spring Security-Konfigurationsklasse

Für unsere grundlegende Spring Security-Konfiguration erstellen wir eine Konfigurationsklasse - SecurityConfig .

Um die WebFlux-Unterstützung in Spring Security 5 zu aktivieren, müssen Sie nur die Annotation @EnableWebFluxSecurity angeben :

@EnableWebFluxSecurity public class SecurityConfig { // ... }

Jetzt können wir die Klasse ServerHttpSecurity nutzen , um unsere Sicherheitskonfiguration zu erstellen.

Diese Klasse ist eine neue Funktion von Spring 5. Sie ähnelt dem Builder für HttpSecurity , ist jedoch nur für WebFlux-Anwendungen aktiviert.

Die ServerHttpSecurity ist bereits mit einigen vernünftigen Standardeinstellungen vorkonfiguriert, sodass wir diese Konfiguration vollständig überspringen können. Für den Anfang stellen wir jedoch die folgende minimale Konfiguration bereit:

@Bean public SecurityWebFilterChain securitygWebFilterChain( ServerHttpSecurity http) { return http.authorizeExchange() .anyExchange().authenticated() .and().build(); }

Außerdem benötigen wir einen Benutzerdetailservice. Spring Security bietet uns einen praktischen Mock-User-Builder und eine speicherinterne Implementierung des Benutzerdetailservices:

@Bean public MapReactiveUserDetailsService userDetailsService() { UserDetails user = User .withUsername("user") .password(passwordEncoder().encode("password")) .roles("USER") .build(); return new MapReactiveUserDetailsService(user); }

Da wir uns in einem reaktiven Land befinden, sollte der Benutzerdetailservice auch reaktiv sein. Wenn wir uns die ReactiveUserDetailsService- Schnittstelle ansehen , werden wir feststellen , dass die findByUsername- Methode tatsächlich einen Mono- Publisher zurückgibt :

public interface ReactiveUserDetailsService { Mono findByUsername(String username); }

Jetzt können wir unsere Anwendung ausführen und ein reguläres HTTP-Basisauthentifizierungsformular beobachten.

4. Gestyltes Anmeldeformular

Eine kleine, aber bemerkenswerte Verbesserung in Spring Security 5 ist ein neues Anmeldeformular, das das Bootstrap 4-CSS-Framework verwendet. Die Stylesheets im Anmeldeformular sind mit CDN verknüpft, sodass wir die Verbesserung nur sehen, wenn eine Verbindung zum Internet besteht.

Um das neue Anmeldeformular zu verwenden, fügen wir dem Builder von ServerHttpSecurity die entsprechende Builder-Methode formLogin () hinzu :

public SecurityWebFilterChain securitygWebFilterChain( ServerHttpSecurity http) { return http.authorizeExchange() .anyExchange().authenticated() .and().formLogin() .and().build(); }

Wenn wir jetzt die Hauptseite der Anwendung öffnen, werden wir feststellen, dass sie viel besser aussieht als das Standardformular, das wir seit früheren Versionen von Spring Security gewohnt sind:

Beachten Sie, dass dies kein produktionsfertiges Formular ist, aber es ist ein guter Bootstrap unserer Anwendung.

Wenn wir uns jetzt anmelden und dann zur URL // localhost: 8080 / logout wechseln, wird das Bestätigungsformular für die Abmeldung angezeigt, das ebenfalls gestaltet ist.

5. Reaktive Controller-Sicherheit

Um etwas hinter dem Authentifizierungsformular zu sehen, implementieren wir einen einfachen reaktiven Controller, der den Benutzer begrüßt:

@RestController public class GreetController { @GetMapping("/") public Mono greet(Mono principal) { return principal .map(Principal::getName) .map(name -> String.format("Hello, %s", name)); } }

Nach dem Anmelden sehen wir die Begrüßung. Fügen wir einen weiteren reaktiven Handler hinzu, auf den nur der Administrator zugreifen kann:

@GetMapping("/admin") public Mono greetAdmin(Mono principal) { return principal .map(Principal::getName) .map(name -> String.format("Admin access: %s", name)); }

Erstellen wir nun einen zweiten Benutzer mit der Rolle ADMIN : in unserem Benutzerdetailservice:

UserDetails admin = User.withDefaultPasswordEncoder() .username("admin") .password("password") .roles("ADMIN") .build();

Wir können jetzt eine Matcher-Regel für die Administrator-URL hinzufügen, für die der Benutzer die Berechtigung ROLE_ADMIN haben muss .

Beachten Sie, dass wir Matcher vor den Kettenaufruf .anyExchange () setzen müssen . Dieser Aufruf gilt für alle anderen URLs, die noch nicht von anderen Matchern abgedeckt wurden:

return http.authorizeExchange() .pathMatchers("/admin").hasAuthority("ROLE_ADMIN") .anyExchange().authenticated() .and().formLogin() .and().build();

If we now log in with user or admin, we'll see that they both observe initial greeting, as we've made it accessible for all authenticated users.

But only the admin user can go to the //localhost:8080/admin URL and see her greeting.

6. Reactive Method Security

We've seen how we can secure the URLs, but what about methods?

To enable method-based security for reactive methods, we only need to add the @EnableReactiveMethodSecurity annotation to our SecurityConfig class:

@EnableWebFluxSecurity @EnableReactiveMethodSecurity public class SecurityConfig { // ... }

Now let's create a reactive greeting service with the following content:

@Service public class GreetService { public Mono greet() { return Mono.just("Hello from service!"); } }

We can inject it into the controller, go to //localhost:8080/greetService and see that it actually works:

@RestController public class GreetController { private GreetService greetService @GetMapping("/greetService") public Mono greetService() { return greetService.greet(); } // standard constructors... }

But if we now add the @PreAuthorize annotation on the service method with the ADMIN role, then the greet service URL won't be accessible to a regular user:

@Service public class GreetService { @PreAuthorize("hasRole('ADMIN')") public Mono greet() { // ... }

7. Mocking Users in Tests

Let's check out how easy it is to test our reactive Spring application.

First, we'll create a test with an injected application context:

@ContextConfiguration(classes = SpringSecurity5Application.class) public class SecurityTest { @Autowired ApplicationContext context; // ... }

Now we'll set up a simple reactive web test client, which is a feature of the Spring 5 test framework:

@Before public void setup() { this.rest = WebTestClient .bindToApplicationContext(this.context) .configureClient() .build(); }

This allows us to quickly check that the unauthorized user is redirected from the main page of our application to the login page:

@Test public void whenNoCredentials_thenRedirectToLogin() { this.rest.get() .uri("/") .exchange() .expectStatus().is3xxRedirection(); }

If we now add the @MockWithUser annotation to a test method, we can provide an authenticated user for this method.

Das Login und das Passwort dieses Benutzers sind Benutzer bzw. Passwort , und die Rolle ist USER . Dies kann natürlich alles mit den Annotationsparametern @MockWithUser konfiguriert werden.

Jetzt können wir überprüfen, ob der autorisierte Benutzer die Begrüßung sieht:

@Test @WithMockUser public void whenHasCredentials_thenSeesGreeting() { this.rest.get() .uri("/") .exchange() .expectStatus().isOk() .expectBody(String.class).isEqualTo("Hello, user"); }

Die Annotation @WithMockUser ist seit Spring Security 4 verfügbar. In Spring Security 5 wurde sie jedoch auch aktualisiert, um reaktive Endpunkte und Methoden abzudecken.

8. Fazit

In diesem Tutorial haben wir neue Funktionen der kommenden Version von Spring Security 5 entdeckt, insbesondere im Bereich der reaktiven Programmierung.

Wie immer ist der Quellcode für den Artikel auf GitHub verfügbar.