1. MockMvc mit der echten SecurityFilterChain verbinden
Wenn wir anfangen, Tests zu schreiben, möchte das Gehirn, besonders das eines Anfänger-Entwicklers, nur eines: dass die Tests grün sind, der Build durchläuft und das Leben beherrschbar erscheint. Das Problem ist, dass Security-Tests aus den falschen Gründen „grün“ werden können: Sie testen versehentlich den Controller ohne Security, und die Tests bestätigen nicht die Sicherheit, sondern Ihre Fähigkeit, das Drehkreuz durch den Serviceeingang zu umgehen.
Stellen Sie sich ein reales Szenario vor. In der Produktion läuft eine Anfrage so ab: zuerst die Filter, dann trifft Spring Security die Entscheidung „durchlassen / nicht durchlassen“, und erst dann kommt die Anfrage zum DispatcherServlet und zum Controller. Wenn Sie im Test den Controller direkt testen oder MockMvc im Modus „nur Controller“ zusammenbauen, testen Sie sozusagen ein Museum, nachdem Sie den Metalldetektor entfernt haben. Das Museum wird sehr „gastfreundlich“ … aber das ist ein zweifelhaftes Kompliment.
Das Ergebnis sind besonders tückische Fehler: Sie schreiben einen Test „öffentlicher Endpunkt gibt 200 zurück“, und er ist grün, selbst wenn die Security gar nicht eingebunden ist, weil der öffentliche Endpunkt auch ohne Security 200 zurückgibt. Und dann fügen Sie eine Regel in SecurityFilterChain hinzu, schließen versehentlich den öffentlichen Bereich – und der Test beginnt plötzlich zu scheitern. Sie verstehen nicht, „was kaputt ist“: der Endpunkt? die Security? die Tests? Tatsächlich ist die grundlegende Annahme kaputt: Ihr Testaufbau sollte maximal ähnlich zur echten Web-Umgebung sein, sonst werden Tests zu Belletristik.
2. MockMvc im Lebenszyklus einer Anfrage und Filter
MockMvc wird oft so verstanden: „Das ist ja wie ein HTTP-Request, nur ohne Server“. Und das ist fast richtig. Aber das Schlüsselwort hier ist fast. MockMvc ermöglicht es tatsächlich, Anfragen wie GET/POST/PATCH zu senden und eine Antwort zu erhalten, ohne Tomcat/Jetty/Netty zu starten. Allerdings versucht es, den Server-Pfad der Anfrageverarbeitung innerhalb von Spring MVC so originalgetreu wie möglich nachzubilden. Und im Server-Pfad stehen vor dem Controller immer Filter.
Halten wir ein einfaches Schema fest. In einer echten Anwendung kommt die Anfrage vom Client, gelangt in die Kette der Servlet-Filter, und wenn sie nicht „abgewiesen“ wird, erreicht sie den DispatcherServlet, der dann alles zu Ihrem Controller weiterleitet. Spring Security lebt genau in den Filtern: Es ist keine „Annotation am Controller“, sondern eine vollwertige Filter-Infrastruktur.
Wenn MockMvc richtig aufgebaut ist, durchläuft die Testanfrage ungefähr diesen Weg:
flowchart TD
T["JUnit Jupiter test"] --> M["MockMvc.perform(...)"]
M --> F["Servlet-Filter
inkl. springSecurityFilterChain"]
F --> D["DispatcherServlet"]
D --> C["Controller"]
C --> R["MockMvcResult / Antwort"]
Wenn MockMvc falsch aufgebaut ist – ein häufiger Fall ist standaloneSetup –, dann verschwindet der Block Servlet-Filter. Und Sie testen nicht mehr die „Anwendung“, sondern ein „Anwendungsstück“.
Daraus ergibt sich die praktische Regel für heute: Security-Tests müssen die Anfrage durch die echte SecurityFilterChain laufen lassen, sonst testen Sie nicht Sicherheit, sondern Hoffnung.
3. Umgebung einrichten und MockMvc aufbauen
3.1. Projekt vorbereiten: Abhängigkeiten für Security-Tests
Bevor wir MockMvc aufbauen, müssen wir sicherstellen, dass wir überhaupt die Werkzeuge für Security-bewusste Tests haben. In der Spring-Welt sieht das als separater Gedanke aus: Spring Test liefert uns MockMvc, und Spring Security liefert separat ein Modul, das sich korrekt in die Test-Infrastruktur einbinden kann. Und ja, wenn Sie die Abhängigkeit vergessen, wird alles wie im echten Leben: „Ich mache anscheinend alles richtig, aber warum wird springSecurity() nicht gefunden?“
In build.gradle.kts oder build.gradle wird normalerweise der Standard-Test-Stack von Spring Boot und eine separate Abhängigkeit spring-security-test benötigt. Im Rahmen des Boot-Dependency-Managements müssen keine Versionen angegeben werden – Boot wählt selbst passende Versionen aus.
Beispiel:
dependencies {
// Basis-Set für Spring Boot Tests (JUnit, AssertJ, MockMvc usw.)
testImplementation("org.springframework.boot:spring-boot-starter-test")
// Integration von Spring Security in die Test-Infrastruktur (springSecurity(), @WithMockUser, user() usw.)
testImplementation("org.springframework.security:spring-security-test")
}
Wenn Sie in der IDE sehen, dass SecurityMockMvcConfigurers.springSecurity() nicht aufgelöst wird oder Sie user() / @WithMockUser nicht haben, dann liegt das Problem in 9 von 10 Fällen genau hier. Und ja, das ist der Moment, in dem „eine Zeile in Gradle“ Ihnen den Abend und Ihr Nervensystem rettet.
3.2. Grundlegender Weg für den Kurs: @SpringBootTest + @AutoConfigureMockMvc
Der entspannteste Start für ein Spring Boot Projekt ist, nicht das Rad neu zu erfinden, sondern eine Basis-Testvorlage zu nehmen, die den Anwendungskontext hochfährt und Ihnen einen fertigen MockMvc liefert. Das ist besonders wichtig in einem Security-Kurs: Wir brauchen die echte SecurityFilterChain, und sie ist in einer normalen Anwendung Teil des Kontexts – mit Beans, Einstellungen, Error-Handlern und all dem, was Sie bereits im Projekt zusammengestellt haben.
In unserem Kurs beginnen wir normalerweise mit einem solchen Testgerüst:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
@SpringBootTest // Hebt den vollständigen Spring Boot Kontext hoch (inklusive der echten SecurityFilterChain)
@AutoConfigureMockMvc // Registriert MockMvc als Bean und bindet die Filter ein
class SecurityAccessTests {
@Autowired
MockMvc mockMvc; // Mit diesem MockMvc führen wir Anfragen so aus, als ob sie durch eine reale Web-Umgebung liefen
}
Einige wichtige und praktische Beobachtungen. @SpringBootTest hebt den Spring Boot Kontext hoch, das heißt, Sie erhalten die gleiche SecurityFilterChain wie zur Laufzeit. @AutoConfigureMockMvc fügt dem Kontext eine MockMvc-Bean hinzu, die bereits für die Arbeit mit Spring MVC konfiguriert ist.
Wenn Sie die Tests hinsichtlich der Konfiguration vorhersagbarer machen möchten, fügen Sie oft @ActiveProfiles("test") hinzu, um eine separate application-test.yml zu verwenden. Aber heute konzentrieren wir uns auf das Wesentliche: Die Anfrage im Test muss durch dieselben Security-Filter laufen wie in der Anwendung.
3.3. Manueller Aufbau von MockMvc und springSecurity()
Die automatische Konfiguration ist praktisch, aber manchmal gibt es Situationen, in denen MockMvc manuell aufgebaut werden muss: zum Beispiel, wenn Sie etwas mehr Kontrolle möchten oder wenn es sich nicht um einen Spring Boot Test handelt. Und hier lauert die klassische Falle: Sie bauen MockMvc auf, er führt Anfragen aus, die Tests laufen … aber Security nimmt nicht an diesen Anfragen teil.
Damit der manuelle Aufbau „professionell“ ist, muss MockMvc nicht über standaloneSetup, sondern über WebApplicationContext aufgebaut werden. Und unbedingt muss springSecurity() angewendet werden – das ist ein spezieller Test-Configurator, der Spring Security und MockMvc korrekt verbindet.
Minimalbeispiel:
import org.junit.jupiter.api.BeforeEach;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.web.context.WebApplicationContext;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup;
class SecurityAccessTests {
@Autowired
WebApplicationContext context; // Echter Web-Kontext der Anwendung (mit registrierten Filtern)
MockMvc mockMvc;
@BeforeEach
void setUp() {
mockMvc = webAppContextSetup(context)
// Wichtig: Integration von Spring Security Test einbinden,
// sonst könnten Anfragen an der SecurityFilterChain vorbeigehen
.apply(springSecurity())
.build();
}
}
Hier geht es nicht um Magie, sondern um technische Ehrlichkeit. webAppContextSetup(context) bedeutet: „Baue mir eine Test-Web-Umgebung auf Basis des echten Spring-Kontexts mit registrierten Filtern“. .apply(springSecurity()) bedeutet: „Schraube in diese Umgebung die Integration von Spring Security Test ein, damit Security-Kontext und Filter korrekt funktionieren“.
standaloneSetup(...) hingegen ist bei Security-Tests fast immer eine schlechte Idee, weil es praktisch ist, um den Controller als „reine Klasse“ zu testen, aber per Definition nichts über Ihre Filter-Infrastruktur wissen muss. Das heißt, es testet den „Controller“, nicht die „Anwendung an der Grenze“.
4. Smoke-Tests: Prüfen, ob die Security eingebunden ist
Das ist noch kein Regression Pack. Die Aufgabe ist bescheidener: schnell zu überprüfen, dass der Testaufbau die Anfrage überhaupt durch die Security leitet und ehrlich die Grenze zwischen öffentlichen und geschützten Endpunkten zeigt.
Bevor Sie eine schöne Testmatrix für USER/EDITOR/ADMIN schreiben, ist es sinnvoll, zwei kurze Prüfungen zu machen, die als „Kontrollleuchte“ dienen. Das sind keine „vollwertigen Security-Tests“, sondern eine Diagnose: „Habe ich die Filter überhaupt eingebunden oder laufe ich durch einen leeren Korridor?“
Der erste Test nimmt sich normalerweise den öffentlichen Bereich. Wenn das öffentliche Lesen kaputt ist, sehen Sie das sofort.
import org.junit.jupiter.api.Test;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@Test
void publicArticlesAreAccessibleForAnonymous() throws Exception {
// Anonyme Anfrage an den öffentlichen Bereich muss durchkommen
mockMvc.perform(get("/api/public/articles"))
.andExpect(status().isOk());
}
Der zweite Test nimmt sich den persönlichen Bereich, der eine Authentifizierung verlangen muss. In unserem Projekt ist die REST-Semantik so eingestellt, dass ein anonymer Benutzer 401 erhalten soll, keine Weiterleitung auf eine HTML-Login-Seite.
import org.junit.jupiter.api.Test;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@Test
void meEndpointRequiresAuthentication() throws Exception {
// Ohne Authentifizierung muss der private Endpunkt 401 Unauthorized zurückgeben
mockMvc.perform(get("/api/me"))
.andExpect(status().isUnauthorized());
}
Diese Prüfungen beschreiben noch nicht die gesamte Zugriffsmatrix. Sie beantworten eine grundlegende Frage: Ist Security tatsächlich am Test beteiligt, oder laufen Sie immer noch an den Filtern vorbei?
Wenn der zweite Test plötzlich 200 zurückgibt, ist das ein sehr starkes Signal: Entweder sind die Filter nicht eingebunden, oder /api/me ist aus irgendeinem Grund öffentlich geworden, was für unsere Zugriffsmatrix mit ziemlicher Sicherheit ein Fehler ist. Genau deshalb ist es sinnvoll, diese beiden Tests direkt nach der Einrichtung der Umgebung zu schreiben, noch vor allem „Interessanten“.
5. Automatische Konfiguration und manueller Aufbau: Wie wählt man aus?
Wenn ein Anfänger zwei Wege sieht, wählt er oft den dritten: Er nimmt beide und mischt sie „für den Fall der Fälle“. In Tests ist das besonders gefährlich: Sie können in eine Situation geraten, in der ein Teil der Tests über den einen MockMvc läuft, ein anderer Teil über einen anderen, bei dem die Security anders eingebunden ist, und dann verbringen Sie Zeit mit der Frage „Warum verhalten sich identische Anfragen unterschiedlich?“
Damit der Kopf nicht raucht, vergleichen wir die Ansätze in einer Tabelle:
| Ansatz | Was er bietet | Was man beachten sollte |
|---|---|---|
| @SpringBootTest + @AutoConfigureMockMvc | Schneller Start, MockMvc als Bean, hohe Ähnlichkeit zur Laufzeit | Vollständiger Kontext, Tests können etwas schwerer sein, aber für ein Regression Pack ist das normal |
| webAppContextSetup(context) .apply(springSecurity()) | Volle Kontrolle über den Aufbau von MockMvc, man kann benutzerdefinierte Einstellungen hinzufügen | Man vergisst leicht springSecurity() und erhält „Tests ohne Security“ |
Die Logik hier ist einfach. Wenn Sie keinen Grund haben, manuell aufzubauen – lassen Sie es. Schreiben Sie grundlegende Tests mit der automatischen Konfiguration, und lassen Sie Ihre Energie in den Sinn fließen, also in die Zugriffsprüfungen, und nicht in die Ingenieurs-Archäologie „Warum haben die Filter hier nicht funktioniert?“
Und noch eine Sache, die man laut aussprechen sollte. Manchmal deaktivieren Entwickler aus den besten Absichten die Filter, um „Tests zu beschleunigen“. In @AutoConfigureMockMvc wird das mit einem Parameter gemacht … und das ist der Knopf, um „Security-Tests sinnlos zu machen“. Wenn Sie genau Security-Tests schreiben, dürfen Sie die Filter nicht deaktivieren, sonst testen Sie buchstäblich die Anwendung in einem Paralleluniversum.
6. Typische Fehler beim Einbinden von MockMvc in die SecurityFilterChain
Fehler in diesem Thema sind unangenehm, weil sie oft so aussehen: „Na ja, die Tests sind doch grün“. Ja, sie sind grün. Sie prüfen nur nicht das Richtige. Deshalb hier die häufigsten Stolpersteine, die Sie besser umgehen sollten, solange sie noch auf dem Weg liegen und Sie nicht am Kopf getroffen haben.
Fehler Nr. 1: MockMvc mit standaloneSetup aufbauen und erwarten, dass die Security funktioniert.
standaloneSetup ist gut, wenn Sie den Controller als isoliertes Objekt testen und Mapping, Parameter und Serialisierung prüfen möchten. Aber es garantiert keine reale Filter-Infrastruktur. Bei Security-Tests führt das zum Effekt „Controller antwortet, also ist Zugriff erlaubt“, obwohl in der Realität der Zugriff vor dem Controller entschieden wird.
Fehler Nr. 2: Die Abhängigkeit spring-security-test vergessen.
Das klingt banal, kommt aber ständig vor: Sie versuchen, .apply(springSecurity()) zu verwenden, aber die Methode scheint nicht zu existieren. Oder Sie möchten zukünftige Werkzeuge wie user() und @WithMockUser nutzen, und die IDE schaut Sie an, als ob sie fragen würde: „Was ist das überhaupt?“. Normalerweise wird das Problem mit einer einzigen Zeile in den dependencies gelöst, aber solange Sie sie nicht hinzugefügt haben, versuchen Sie buchstäblich, Borschtsch ohne Topf zu kochen.
Fehler Nr. 3: MockMvc manuell aufbauen, aber .apply(springSecurity()) nicht anwenden.
Optisch können die Tests weiterlaufen, besonders bei öffentlichen Endpunkten. Genau deshalb ist der Fehler tückisch: Sie schreiben einen Test „öffentlicher Endpunkt OK“ – grün, Sie freuen sich. Dann schreiben Sie einen Test „/api/me gibt 401“ – und bekommen 200. Sie beginnen, den Controller, die Security-Konfiguration, die Mondphase zu verdächtigen … dabei liegt das Problem darin, dass Security überhaupt nicht an der Anfrageverarbeitung beteiligt war.
Fehler Nr. 4: Filter in @AutoConfigureMockMvc deaktivieren und es nicht bemerken.
Das Deaktivieren von Filtern ist ein beliebter „Beschleuniger“, aber bei Security-Tests ist es wie ein „Auto-Beschleuniger“ durch entfernte Bremsen: Ja, es ist schneller, aber die Freude ist zweifelhaft. Wenn Filter deaktiviert sind, laufen Anfragen nicht durch die SecurityFilterChain, und Sie prüfen keine Zugriffsregeln. Sie prüfen nur, dass der Controller Antworten zurückgeben kann.
Fehler Nr. 5: Aufgrund nicht abgestimmter Security-Semantik den falschen Status erwarten.
Wenn Ihre API bereits auf REST-freundliches Verhalten umgestellt ist, sollte ein anonymer Benutzer auf einem privaten Endpunkt 401 erhalten, und ein authentifizierter Benutzer ohne Berechtigung 403. Wenn Sie plötzlich eine 302-Weiterleitung zu /login sehen, deutet das darauf hin, dass Sie Browser-Semantik testen oder eine Konfiguration verwenden, die einen HTML-Redirect anstelle eines JSON-Fehlers liefert. Wichtig ist, nicht „den Test an den seltsamen Status anzupassen“, sondern zu verstehen, welche Semantik in Ihrem Projekt gelten soll.
GO TO FULL VERSION