1. spring-security-test für Security-Tests
Die Zugriffsmatrix prüft sich nicht von selbst: Im Test muss man explizit angeben können, wer die Anfrage stellt.
Wenn Sie Spring Security zum ersten Mal in einer Spring-Boot-Anwendung einschalten, passiert Magie, die zunächst erfreut und dann ein wenig nervt. Erfreut – weil „irgendwie ist ja alles geschützt“. Nervt – weil plötzlich fast jede Anfrage in Tests zu 401 oder 403 wird, und Sie sitzen da und denken: „Teste ich den Controller … oder plötzlich die gesamte Sicherheitszivilisation?“ In diesem Moment beginnen viele entweder, Filter abzuschalten, „damit die Tests durchlaufen“, oder versuchen, den SecurityContextHolder manuell zu befüllen. Und da kommt spring-security-test ins Spiel: eine Bibliothek, die Ihnen eine offizielle und bequeme Möglichkeit bietet, einen Benutzer in Tests zu modellieren – und das so, dass der Test lesbar bleibt.
In unserem Kurs ist das besonders wichtig, weil wir bereits gelernt haben, die MVC-Schicht und integrierte Szenarien zu testen, und jetzt einfach eine weitere Achse hinzufügen: „Wer genau führt die Anfrage aus?“ spring-security-test hilft, diese Achse im Testcode explizit zu machen: entweder mit der Annotation @WithMockUser oder durch die Konfiguration einer bestimmten Anfrage über Request-Post-Processors (user(...), anonymous(...), httpBasic(...)).
Pragmatisch betrachtet spart Ihnen diese Abhängigkeit Stunden an Zeit: Statt zu rätseln, warum die Anfrage fehlgeschlagen ist und wie Sie „einen Benutzer unterschieben“, schreiben Sie direkt im Test einen verständlichen Satz: „Anfrage als admin ausführen“. Und dann wird der Test wieder das, was er sein soll: eine Dokumentation des Verhaltens.
Es gibt drei Vorbedingungen, um nicht in falsche Magie zu verfallen. Erstens: spring-security-test ist eine separate test-dependency org.springframework.security:spring-security-test, kein kostenloser Bonus, der automatisch im Projekt auftaucht. Zweitens: Alle Beispiele unten basieren auf der bereits vorhandenen Sicherheitskonfiguration von ContentHub: Rollen EDITOR/ADMIN, eine öffentliche Zone und eine funktionierende Authentifizierungsmethode in der Anwendung. Und drittens: @WithMockUser und user() leben im serverseitigen Spring-Test-Pfad: Sie sind sinnvoll in @WebMvcTest, in @SpringBootTest + @AutoConfigureMockMvc und in Method-Security-Tests für Spring-managed Beans. Wenn Sie einen echten Server starten und mit einem Client darauf zugreifen, brauchen Sie dort echte Anmeldedaten und einen normalen Authorization-Header.
2. Wo lebt der Benutzer?
Wenn Sie neu sind, gibt es eine Sache, die Sie akzeptieren sollten (dann wird das Leben leichter). Bei einer normalen Anfrage „springt“ der Benutzer nicht direkt in den Controller. Zuerst durchläuft die Anfrage die Security Filter Chain, und nur wenn die Kette der Meinung ist, dass alles in Ordnung ist, gelangt die Steuerung zum MVC und zu Ihrem @RestController. Aus Sicht von Spring Security ist das Hauptobjekt also nicht Ihr DTO und nicht einmal Ihr Controller, sondern das Authentication-Objekt im SecurityContext.
Innerhalb von Spring Security sieht das etwa so aus: Filter versuchen zu „verstehen“, wer Sie sind, erstellen ein Authentication-Objekt (mit username und einer Liste von Berechtigungen), legen es in den SecurityContext, und dann entscheidet der Autorisierungsmechanismus, ob Sie auf diesen Endpunkt oder diese Methode zugreifen dürfen. Wenn Sie nicht authentifiziert sind, ist der Kontext leer oder enthält eine „anonymous authentication“ (je nach Konfiguration). Wenn Sie eingeloggt sind, enthält der Kontext ein Authentication-Objekt mit Ihren Rollen/Authorities.
In Tests wollen wir dies ohne Hokuspokus steuern. Dabei gibt es zwei grundlegend unterschiedliche Ansätze:
1) Wir können einen bereits authentifizierten Benutzer simulieren, ohne zu prüfen, „wie genau er sich angemeldet hat“. Das ist der Weg von @WithMockUser und user(). Er ist gut, wenn uns die Autorisierung (Zugriff basierend auf Rollen/Berechtigungen) interessiert, nicht der Login-Mechanismus.
2) Wir können den realen Login-Mechanismus durchlaufen, z. B. über HTTP Basic, also der Anfrage einen Authorization: Basic ...-Header hinzufügen und die Filter alles selbst verarbeiten lassen. Das ist der Weg von httpBasic(). Er ist nützlich, wenn Sie sicherstellen möchten, dass Ihre Sicherheitskonfiguration tatsächlich wie vorgesehen funktioniert und nicht nur „in der Testfantasie“.
Dieses Modell ist der Hauptkompass für die Auswahl des Werkzeugs in einem Test. Wenn Sie es im Kopf behalten, wird alles Weitere ziemlich logisch und nicht „Magie der Annotationen“.
3. @WithMockUser: schneller Mock-Akteur
@WithMockUser ist im Grunde der offizielle „Cheat-Code“ für Tests. Aber nicht im schlechten Sinne, sondern im ingenieurtechnischen. Er ermöglicht es Ihnen, den Test so auszuführen, als ob im SecurityContext bereits ein authentifizierter Benutzer mit dem gewünschten username und den Rollen liegt. Dabei werden keine echten Passwörter überprüft, keine Benutzerdatenbank benötigt, und Ihr Test wird nicht zu einem Kurs „Wie funktioniert Login“.
In der Praxis eignet sich @WithMockUser hervorragend für MVC-Tests (sowohl Slice- als auch Full-Context), wenn Sie genau die Zugriffsregel testen möchten: Wer darf auf den Endpunkt zugreifen. Besonders gut passt er zu Szenarien, in denen der gesamte Test von einem einzigen Akteur ausgeführt wird: „Dieser Test ist als Editor“, „Dieser als Admin“. In diesem Modus liest sich der Code fast wie ein Satz auf Deutsch.
Minibeispiel: Anfrage an einen Editor-Endpunkt von einem Editor-Benutzer.
import org.junit.jupiter.api.Test;
import org.springframework.security.test.context.support.WithMockUser;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@Test
// Legt einen „Benutzer“ mit den erforderlichen Rechten in den SecurityContext, ohne einen echten Login
@WithMockUser(username = "alice", roles = "EDITOR")
void createDraft_asEditor_isOk() throws Exception {
// Wichtig: Die Anfrage selbst ist normal, die „Magie“ geschieht in der Security-Test-Infrastruktur
mockMvc.perform(post("/api/editor/articles"))
// Prüft, dass die Autorisierung einen solchen Benutzer durchlässt
.andExpect(status().is2xxSuccessful());
}
Beachten Sie zwei Punkte. Erstens: Der username ist hier nicht dekorativ. Wenn Sie irgendwo in den Zugriffsregeln eine „eigener/fremder Ressource“-Logik haben, können Sie ohne einen sinnvollen Namen die Szenarien nicht normal testen. Zweitens: EDITOR sieht fast wie ein Geschäftsbegriff unserer Domäne aus, und das ist sehr gut: Ein Test sollte nicht in der Sprache „ROLE_technische_Buchstaben_Innereien“ geschrieben sein.
4. roles und authorities: über ROLE_
Das Thema Rollen und Authorities scheint klein, bis Sie einen Test haben, der „eigentlich richtig“ ist, aber einen unerwarteten Status zurückgibt. Daher ist es besser, das jetzt zu klären, um später keine Ermittlung im Stil „Wer hat meinen Zugriff gestohlen?“ zu führen.
In Spring Security hat es sich historisch so ergeben: Eine Rolle ist ein Spezialfall einer Authority, einfach mit dem Präfix ROLE_. Das heißt, die Rolle ADMIN wird tatsächlich als Authority ROLE_ADMIN gespeichert. Genau deshalb erzeugt @WithMockUser(roles = "ADMIN") automatisch die Authority ROLE_ADMIN. Und genau deshalb ist es fast immer ein Fehler, roles = "ROLE_ADMIN" zu schreiben: Sie erhalten dann ROLE_ROLE_ADMIN, was wie „Chef der Chefs“ klingt, aber leider nicht funktioniert.
Wenn Sie hingegen Rechte feiner vergeben möchten (z. B. keine Rolle, sondern eine bestimmte Berechtigung), dann verwenden Sie authorities. Im Rahmen von ContentHub reichen uns grundsätzlich die Rollen EDITOR und ADMIN, aber es ist hilfreich, den Unterschied zumindest auf der Ebene „Ich weiß, warum es so ist“ zu verstehen.
Ein Minibeispiel mit authorities, nur um den Unterschied zu spüren:
import org.junit.jupiter.api.Test;
import org.springframework.security.test.context.support.WithMockUser;
@Test
// Hier wird ein „granulares“ Recht direkt gesetzt, ohne ROLE_-Präfix und ohne Rollen
@WithMockUser(username = "admin", authorities = "ARTICLE_APPROVE")
void approve_withAuthority_isOk() throws Exception {
// Ein solcher Test prüft genau die Zugriffsregel (Autorisierung) anhand der Authority
mockMvc.perform(post("/api/admin/articles/42/approve"))
.andExpect(status().is2xxSuccessful());
}
Diese Variante ist sinnvoll, wenn Ihr System nicht nur auf Rollen, sondern auf „granularen“ Rechten basiert. Wenn das Projekt jedoch auf Rollen beruht, ist es besser, die Ansätze nicht aus bloßer Abwechslung zu mischen: In Tests gibt es schon genug Stellen, an denen man versehentlich Chaos erzeugen kann.
5. Post-Processors für MockMvc
Die Annotation @WithMockUser ist praktisch, wirkt aber auf Ebene der Testmethode (oder -klasse). Was aber, wenn Sie einen Test schreiben, der zwei Anfragen stellt, und die zweite Anfrage soll von einem anderen Benutzer kommen? Oder Sie bauen eine kleine „Matrix“ von Prüfungen und möchten, dass der Akteur direkt in der Zeile mit der Anfrage sichtbar ist? Dann sind Request-Post-Processors viel praktischer.
Ein Request-Post-Processor ist ein Ding, das die Anfrage vor der Ausführung „nachkonfiguriert“. Für MockMvc stellt Spring Security Test eine Reihe von Methoden bereit, die normalerweise statisch importiert werden. Die wichtigsten für uns heute: anonymous(), user(...), httpBasic(...).
Beispiel: Explizit eine Anfrage als anonymous ausführen (das ist auch dann nützlich, wenn Ihre Tests „standardmäßig“ ohne Benutzer ausgeführt werden könnten – Sie machen die Absicht explizit).
import org.junit.jupiter.api.Test;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.anonymous;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@Test
void publicEndpoint_asAnonymous_isOk() throws Exception {
// Explizit einen anonymen Benutzer setzen (ohne Authentifizierung)
mockMvc.perform(get("/api/public/articles").with(anonymous()))
// Erwarten, dass die öffentliche Zone ohne Login zugänglich ist
.andExpect(status().is2xxSuccessful());
}
Beispiel: Eine Anfrage als bestimmten Editor-Benutzer ausführen, aber nicht über eine Annotation, sondern direkt in der Anfrage.
import org.junit.jupiter.api.Test;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@Test
void editorReadsOwnEndpoint_asUserPostProcessor() throws Exception {
// Der Benutzer wird auf Ebene der konkreten Anfrage gesetzt – praktisch, wenn es im Test mehrere Akteure gibt
mockMvc.perform(get("/api/editor/articles").with(user("alice").roles("EDITOR")))
// Auch hier wird die Autorisierung geprüft, nicht der reale Login-Mechanismus
.andExpect(status().is2xxSuccessful());
}
Und das dritte wichtige Werkzeug ist httpBasic(). Es unterscheidet sich in der Bedeutung: user() und @WithMockUser „unterschieben“ den Benutzer direkt in den Testkontext, während httpBasic() die Anfrage realistischer macht, weil es HTTP-Basic-Anmeldedaten zur Anfrage hinzufügt und die Filter den Login verarbeiten lässt.
import org.junit.jupiter.api.Test;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@Test
void adminApproves_asHttpBasic_isOk() throws Exception {
// Echte Anmeldedaten in die Anfrage einfügen (Authorization: Basic ...)
mockMvc.perform(post("/api/admin/articles/42/approve").with(httpBasic("admin", "password")))
// Wenn basic auth/Benutzer/Passwort konfiguriert sind, sollte die Anfrage durchgehen
.andExpect(status().is2xxSuccessful());
}
Wenn in Ihrer Anwendung tatsächlich In-Memory-Benutzer für den Kurs konfiguriert sind (und in ContentHub sollten sie das sein), beginnt ein solcher Test nicht nur zu prüfen, „ob die Rolle ADMIN erlaubt ist“, sondern auch, dass admin als Benutzer existiert, das Passwort gelesen wird und HTTP Basic aktiviert ist. Das ist eine andere Ebene der Sicherheit, und manchmal ist sie sehr nützlich.
6. Auswahl der Methode zur Benutzerangabe
Wenn man drei Werkzeuge hat, beginnt ein Neuling normalerweise nach dem Prinzip „welches ist kürzer“ auszuwählen. Aber besser ist es, nach dem Sinn auszuwählen, sonst ist der Test zwar grün, aber das Vertrauen wächst nicht (das ist der schlechteste Deal beim Testen: viel Code, wenig Nutzen).
Unten ist eine kleine Navigationstabelle. Sie ersetzt nicht das Nachdenken, hilft aber, nicht die Orientierung zu verlieren.
| Methode zur Benutzerangabe | Was Sie tatsächlich testen | Wann besonders praktisch | Hauptrisiko/-einschränkung |
|---|---|---|---|
| @WithMockUser | Autorisierung (Zugriffsregeln), ohne echten Login | Wenn der gesamte Test „unter einem Akteur“ läuft | Man vergisst leicht, dass Passwort/Login gar nicht geprüft werden |
| .with(user("...")) | Autorisierung auf Ebene der konkreten Anfrage | Wenn es in einem Test verschiedene Akteure gibt oder Sie den Akteur neben der URI sehen möchten | Prüft immer noch nicht den realen Authentifizierungsmechanismus |
| .with(httpBasic("...","...")) | Realistischerer Login über HTTP Basic | Wenn Sie näher an einer produktionsähnlichen Konfiguration sein möchten | Der Test wird etwas „teurer“ und hängt vom Benutzer-Setup der Anwendung ab |
Ein allgemeiner praktischer Rat für den ContentHub-Kurs: Wenn Sie eine reine Matrix „Akteur → Endpunkt → Ergebnis“ erstellen, reichen Ihnen oft @WithMockUser oder user(). Wenn Sie jedoch sicherstellen möchten, dass Ihr Integrationsszenario tatsächlich mit echten Anmeldedaten funktioniert (insbesondere in Full-Context-Tests), dann ist httpBasic() eine ausgezeichnete Wahl.
7. Beispiele für ContentHub
Da wir bereits sowohl @WebMvcTest als auch @SpringBootTest + @AutoConfigureMockMvc schreiben können, ist es wichtig zu verstehen, dass spring-security-test nicht an eine bestimmte Testart „gebunden“ ist. Es funktioniert überall dort, wo Spring Test überhaupt einen Kontext hochfährt und die Sicherheitsinfrastruktur einbindet.
Im MVC-Slice (@WebMvcTest) mocken Sie oft die Services des Controllers und konzentrieren sich auf die HTTP-Grenze. In einem solchen Test ist @WithMockUser normalerweise ideal: Sie setzen explizit eine Rolle, führen eine Anfrage aus und prüfen, ob der Controller zugänglich oder nicht zugänglich ist. Dabei verkomplizieren Sie den Test nicht durch die Prüfung des echten Logins – denn das Ziel eines Slice-Tests ist ein anderes: schnell und lokal die Web-Grenze zu schützen.
In einem Full-Context-Test (@SpringBootTest + @AutoConfigureMockMvc) sind Sie näher an der „echten Anwendung“ und haben eine Wahl. Wenn Sie einfach die Erreichbarkeit eines Endpunkts für eine Rolle testen möchten, verwenden Sie @WithMockUser. Wenn Sie jedoch sicherstellen möchten, dass Ihr Security-Setup tatsächlich „wie im echten Leben“ (wenn auch nur zu Übungszwecken) funktioniert, verwenden Sie httpBasic(). Das ist besonders nützlich, wenn sich die Security-Konfiguration ändert und Sie Regressionen wie „wir haben basic auth versehentlich ausgeschaltet“ oder „den Benutzer in der Konfiguration umbenannt“ befürchten.
Ein kleiner Ausschnitt aus einem Full-Context-Test mit httpBasic() im Stil „Zugriff mit echten Anmeldedaten prüfen“:
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@Test
void adminListing_asHttpBasic_isOk() throws Exception {
// In einem Full-Context-Test ist dies näher am realen Verhalten: die Filter verarbeiten den Login selbst
mockMvc.perform(get("/api/admin/articles").with(httpBasic("admin", "password")))
.andExpect(status().is2xxSuccessful());
}
Hier gibt es keine Geschäftslogik der Veröffentlichung. Und das ist gut. Es ist kein Regressionstest, sondern eine punktuelle Prüfung: „Zumindest kann der Admin überhaupt in den Admin-Bereich gelangen“.
8. Typische Fehler bei der Benutzerangabe in Security-Tests
Fehler Nr. 1: Erwarten, dass @WithMockUser in einem Test ohne Spring-Kontext funktioniert.
@WithMockUser ist keine Magie von JUnit an sich, sondern eine Integration mit Spring Test. Wenn Sie einen einfachen Unit-Test ohne @SpringBootTest, ohne @WebMvcTest und generell ohne Beteiligung des Spring TestContext schreiben, wird die Annotation nicht „greifen“, und Sie starren auf 401/403 wie auf ein Rätsel des Universums.
Fehler Nr. 2: roles = "ROLE_ADMIN" schreiben und sich dann wundern, dass der Zugriff nicht erscheint.
Für roles wird das Präfix ROLE_ automatisch hinzugefügt. Wenn Sie explizit ROLE_ADMIN angeben, erhalten Sie oft eine falsche Authority (ROLE_ROLE_ADMIN). Der Test sieht plausibel aus und ist dadurch besonders tückisch: Es scheint, als sei „alles wie in der Doku“, aber in Wirklichkeit ist es das nicht.
Fehler Nr. 3: @WithMockUser verwenden, wenn Sie eigentlich die reale Login-Methode testen wollten.
Manchmal denkt ein Team, es habe „basic auth funktioniert“ getestet, aber hat einen Test mit @WithMockUser geschrieben. Ein solcher Test hat nur die Autorisierungsregeln geprüft, den Authentifizierungsmechanismus aber umgangen. Wenn es Ihnen wichtig ist, sicherzustellen, dass Anmeldedaten tatsächlich verarbeitet werden, verwenden Sie httpBasic() (oder in Live-Server-Tests einen echten Authorization-Header).
Fehler Nr. 4: Den Akteur zu tief in einem Helfer verstecken und die Lesbarkeit verlieren.
Es ist praktisch, eine Methode asEditor() zu erstellen und zu vergessen, wer darin steckt. Aber wenn die Security-Logik an den username gebunden ist (und besitzerbasierter Zugriff ist fast immer so aufgebaut), werden „namenlose“ Helfer den Test in ein Rätsel verwandeln. Der Benutzer in einem Security-Test sollte sichtbar sein, sonst hört der Test auf, eine Dokumentation zu sein.
Fehler Nr. 5: 403 erhalten und automatisch denken „also ist die Rolle falsch“.
403 bedeutet einfach „verboten“, aber die Ursachen können vielfältig sein: unzureichende Rolle, fehlendes CSRF-Token (falls aktiviert), fehlende benötigte Authority, oder sogar, dass die Anfrage in einen anderen Security-Matcher gelandet ist. Bevor Sie den Test reparieren, ist es hilfreich sicherzustellen, dass Sie den Benutzer genau mit der Methode angeben, die Ihrer Konfiguration entspricht (Mock-Akteur vs. echtes Basic Auth).
GO TO FULL VERSION