1. Problem: Dieselbe URL – verschiedene Aktionen
Wenn man REST API aus der Vogelperspektive betrachtet, kann man leicht der Illusion verfallen: der Pfad beginnt mit "/api/admin", also „das ist der Adminbereich“, der Pfad beginnt mit "/api/drafts", also „das sind Entwürfe“. Aber der Teufel steckt, wie üblich, nicht nur im Detail – er wohnt dort fest und zahlt Nebenkosten.
Eine grobe Zonenkarte haben wir bereits: public, authenticated, editor/admin. Aber sobald innerhalb einer Zone Lesen, Ändern und Veröffentlichen nebeneinander liegen, reichen allein die Pfadpräfixe nicht mehr aus. Jetzt brauchen wir eine präzisere Sprache: method + path pattern + Anforderung.
Ein und dieselbe Pfadzone enthält fast immer verschiedene Operationen. GET – normalerweise lesen, POST – erstellen, PATCH – ändern, DELETE – löschen. Wenn wir allen diesen Methoden dieselbe allgemeine Berechtigung geben, drehen wir entweder die Schrauben zu fest an und machen die API unbequem, oder wir lassen die Schrauben locker und lassen plötzlich „die falsche Person an die falsche Stelle“.
Schauen wir uns ein konkretes Beispiel an.
Stellen Sie sich den Adminbereich eines Projekts vor:
- GET /api/admin/users – Benutzerliste anzeigen (Recht „lesen“).
- PATCH /api/admin/users/{id}/roles – Rollen ändern (Recht „verwalten“).
Wenn wir sagen: „alles /api/admin/** erfordert user:manage“, dann wird auch das Lesen zur „Verwaltung“. Das scheint sicher, aber Sie schaffen sich selbst unnötige Einschränkungen: die Rolle/das Recht zum Lesen (user:read) wird nutzlos. Und wenn wir umgekehrt an /api/admin/** nur user:read hängen, kann plötzlich jeder „Leser“ Rollen ändern. Und das ist nicht nur „oh“, sondern „oh‑oh‑oh“.
Also brauchen wir eine Sprache, die nicht nur sagt, „wo man hinkommt“, sondern auch, „was man tun will“.
2. Mentales Modell: Method + Path + Anforderung
Um die Security-Konfiguration nicht in eine Sammlung von Zaubersprüchen aus Tutorials zu verwandeln, behalten wir ein einfaches Bild: für Spring Security ist eine eingehende Anfrage mindestens method + path (und noch viele Details, aber heute reicht uns das). Auf der Ebene der Request‑Level‑Authorization bauen wir Regeln, die entscheiden: „Lassen wir eine solche Anfrage überhaupt in die Anwendung weiter?“
Man kann sich das wie eine „Maske“ am Gebäudeeingang vorstellen. Der Pfad sagt, zu welcher Tür man gekommen ist. Die Methode sagt, was die Person tut: das Schild lesen, versuchen die Tür zu öffnen, das Schloss auswechseln oder die Tür mitsamt Getriebe heraustragen. Es ist ungefähr dasselbe „ins Gebäude gekommen“, aber die Aktionen sind unterschiedlich, und der Sicherheitsdienst reagiert anders.
Praktisch lässt sich das fast als Formel aufschreiben:
# Regel der Request‑Level‑Authorization (das ist genau der „Drehkreuz“ am Eingang)
Request‑level rule = (Method, PathPattern) -> permit | authenticated | hasAuthority | hasRole | deny
# Wichtig: hier prüfen wir nur die Form der Anfrage (Methode + Pfad),
# nicht den fachlichen Sinn wie „das ist definitiv dein Entwurf“.
Und noch ein wichtiger Gedanke, der viele Nerven spart: Request‑Level‑Authorization muss nicht perfekt sein und die Fachlogik „verstehen“. Ihre Aufgabe ist es, offensichtlichen Unsinn abzuwehren und sofort Öffentliches von Privatem und Privilegiertem zu trennen. Fachliche Einschränkungen (wie „das ist dein oder fremder Entwurf“) passen hier normalerweise nicht ohne Schmerzen und seltsame Krücken hinein.
Um es anschaulicher zu machen, hier eine kleine Tabelle zu einer Ressource:
| Ressource | Methode | Bedeutung der Operation (nach REST) | Beispielanforderung auf Request‑Level |
|---|---|---|---|
| /api/admin/users/** | GET | lesen | hasAuthority("user:read") |
| /api/admin/users/** | PATCH | ändern | hasAuthority("user:manage") |
| /api/public/articles/** | GET | öffentliches Lesen | permitAll() |
| /api/public/articles/** | POST | Artikel erstellen (und das ist verdächtig) | denyAll() oder diesen Endpoint gar nicht haben |
Genau diese Verbindung lernen wir jetzt zu denken: ein Pfadpräfix – verschiedene Methoden – verschiedene Anforderungen.
3. Path Pattern: * und /**
Wenn Sie ein oder zwei Endpunkte haben, können Sie exakte Pfade schreiben. Aber sobald „Familien“ von URLs auftauchen (z. B. /api/editor/drafts/{id}/publish für beliebige id), merken Sie schnell, dass es eine Strafe für ganz bestimmte Sünden ist, Regeln für jeden möglichen Pfad zu kopieren.
Dafür gibt es Path Patterns – Pfadschablonen. In Spring Security trifft man meist den Ant‑Stil, wo die häufigsten Zeichen sind: * für ein Segment, /** für einen ganzen Teilbaum. Das ist kein Regex, also Denkweisen wie .* und \d+ sind hier nicht nötig (und besser nicht, das schont die Psyche).
Halten wir in einer Tabelle fest, was wirklich jeden Tag nützlich ist:
| Pattern | Bedeutung | Beispielübereinstimmung | Beispiel KEINE Übereinstimmung |
|---|---|---|---|
| /api/admin/** | alles innerhalb /api/admin | /api/admin/users/42/roles | /api/administrator/users |
| /api/editor/drafts/*/publish | ein Segment anstelle von * | /api/editor/drafts/12/publish | /api/editor/drafts/12/extra/publish |
| /api/public/articles/** | alles unter dem Artikelzweig | /api/public/articles/spring-security | /api/public/article/spring-security |
Beachten Sie eine Feinheit: * bedeutet nicht „irgendein beliebiger Zeichenfolgeabschnitt“, sondern „irgendein Abschnitt innerhalb eines Pfadsegments“. Segmente werden durch / getrennt. Das ist bequemer, als es scheint, weil es einer „Path Variable“ im Controller sehr ähnelt.
In der Praxis sieht das so aus: ein Matcher deckt eine ganze Klasse gleichartiger URLs ab, und Sie erhalten Regeln, die sich wie eine API‑Karte lesen, nicht wie eine Liste zufälliger Zeichenketten.
Ein kurzes Beispiel in der Konfiguration (vorerst ohne den gesamten Dateikontext, nur um das Muster zu sehen):
import org.springframework.http.HttpMethod;
// innerhalb von authorizeHttpRequests(auth -> auth ...)
// POST + konkrete Pfadschablone: das ist eine separate „Operation“ in der API (publish)
auth.requestMatchers(HttpMethod.POST, "/api/editor/drafts/*/publish")
.hasAuthority("draft:publish"); // Fordert ein konkretes Recht für die Veröffentlichung
Das Pattern * steht hier „an Stelle von {id}“. Wir prüfen nicht, ob dort eine Zahl steht. Auf Request‑Level ist uns das normalerweise egal: wenn jemand zu „publish eines bestimmten Entwurfs“ kommt, ist das bereits eine hinreichend erkennbare Operation.
4. Methodenspezifische requestMatchers: Lesen und Ändern trennen
Nachdem wir die Path Patterns beherrschen, ist der nächste „Präzisionsverstärker“ die Bindung an die HTTP‑Methode. Und hier zeigt sich plötzlich der wahre Nutzen des REST‑Stils: Wenn Sie in der API die Methoden tatsächlich zweckgemäß verwenden, wird die Security‑Konfiguration fast „selbstdokumentierend“.
Das häufigste Szenario: Sie haben ein Präfix, aber verschiedene Methoden. Und Sie wollen: Lesen – ein Recht, Ändern – ein anderes. Im Übungsprojekt sieht man das besonders schön im Adminbereich.
Zum Beispiel: Benutzer lesen (user:read) und Benutzer verwalten (user:manage) sind verschiedene Dinge. Drücken wir das direkt aus:
import org.springframework.http.HttpMethod;
// innerhalb von authorizeHttpRequests(auth -> auth ...)
// 1) Lesen von Benutzern – separates Recht
auth.requestMatchers(HttpMethod.GET, "/api/admin/users", "/api/admin/users/**")
.hasAuthority("user:read"); // Erlaube nur denen, die das Leserecht haben
// 2) Jegliche Änderungen an Benutzern (PATCH) – stärkeres Recht
auth.requestMatchers(HttpMethod.PATCH, "/api/admin/users/**")
.hasAuthority("user:manage"); // Das ist bereits „verwalten“, nicht „ansehen“
Hier stecken zwei wichtige Ideen.
Erstens: GET und PATCH sind unterschiedliche Operationen, und wir müssen nicht dasselbe Recht „mitziehen“.
Zweitens: Wir schreiben die Matcher hinreichend eng, damit sie nichts Überflüssiges einfangen. PATCH /api/admin/users/** deckt sowohl roles, als auch lock und enable ab – alles, was den Benutzer verändert. Dabei bleibt das Lesen separat.
Jetzt ein Beispiel für den öffentlichen Bereich. Wir erlauben GET auf veröffentlichte Artikel für alle, alles andere unter /api/** – mindestens Authentifizierung (das ist typisch „erst Ausnahmen, dann allgemeine Zone“):
import org.springframework.http.HttpMethod;
// innerhalb von authorizeHttpRequests(auth -> auth ...)
// Öffentliches Lesen: allen ohne Login erlauben
auth.requestMatchers(HttpMethod.GET, "/api/public/articles", "/api/public/articles/**")
.permitAll();
// Das gesamte übrige API (einschließlich privater Zonen) – nur nach Authentifizierung
auth.requestMatchers("/api/**")
.authenticated();
Das klingt fast wie ein menschlicher Vertrag: „Artikel lesen darf jeder, der Rest des API nur nach Login.“ Und hier haben wir den perfekten Anlass, an das wichtigste Gebot der Security‑Konfiguration zu erinnern: Die Reihenfolge der Regeln ist wichtig. Darum geht es im nächsten Abschnitt.
5. Reihenfolge der Regeln: Wie ein „breiter“ Matcher einen „engen“ frisst
In der Konfiguration von authorizeHttpRequests werden die Regeln nicht nur mit den Augen von oben nach unten gelesen – das System wendet sie auch so an. Der häufigste „unsichtbare Bug“ bei Anfängern: Er hat korrekt zwei Regeln geschrieben, aber in der falschen Reihenfolge, und eine davon wird sinnlos.
Das ist vergleichbar mit der Situation: „Ich habe zwei Schlösser an die Tür gehängt, aber eines davon öffnet sich mit dem Schlüssel der Gegensprechanlage, also ist das zweite egal.“ Formal sind die Schlösser da. Praktisch – naja, Sie verstehen.
Ein klassisches Beispiel:
import org.springframework.http.HttpMethod;
// Schlecht: zuerst breite Regel, dann Ausnahme
// Zuerst haben wir alles /api/** geschlossen – darunter kann die „Ausnahme“ dann nicht wie erwartet wirken
auth.requestMatchers("/api/**").authenticated();
auth.requestMatchers(HttpMethod.GET, "/api/public/articles/**").permitAll();
In authorizeHttpRequests gilt eine einfache Regel: Es wird die erste übereinstimmende Regel von oben nach unten angewendet. Daher macht der Matcher "/api/**", der weiter oben steht, die darunterliegende Ausnahme für öffentliches GET unerreichbar. Ergebnis: Der öffentliche GET ist „versehentlich“ geschlossen.
Richtiger ist es, mit den engen Ausnahmen zu beginnen:
import org.springframework.http.HttpMethod;
// Gut: zuerst Ausnahmen, dann allgemeine Zone
// Zuerst explizit die public-Regel setzen, dann den Rest abschließen
auth.requestMatchers(HttpMethod.GET, "/api/public/articles/**").permitAll();
auth.requestMatchers("/api/**").authenticated();
Die Idee ist einfach: Zuerst die Spezialfälle festlegen (public read, technische Ausnahmen, enge privilegierte Operationen), dann den „ganzen Rest“ mit breiteren Regeln abdecken.
Fast immer kann man die Konfiguration so organisieren, dass sie wie eine Karte lesbar ist: public → user‑Zone → privilegierte Zone → Rest abschließen. Und Sie werden überrascht sein, wie viel seltener dann „warum hier 403?!“ vorkommt.
6. Beispiele für die Secure Content Platform API
Jetzt festigen wir alles an unserer Domain, damit es nicht abstrakt bleibt. In der Secure Content Platform API kommen solche Paare „ein Präfix – verschiedene Methoden“ ständig vor: Admin‑Zone für Benutzerverwaltung, Entwürfe, Moderation.
Beginnen wir mit dem Adminbereich, denn dort schreit die Methodenspezifität geradezu: Lesen ist nicht gleich Ändern.
Hier ist eine minimale „Request‑Level‑Karte“ für Admin‑Users:
| Endpoint‑Familie | Methode | Anforderung |
|---|---|---|
| /api/admin/users/** | GET | user:read |
| /api/admin/users/** | PATCH | user:manage |
Im Config‑Snippet könnte das so aussehen (Ausschnitt, nicht der gesamte SecurityFilterChain):
import org.springframework.http.HttpMethod;
// innerhalb von authorizeHttpRequests(auth -> auth ...)
// GET auf Benutzer – das ist „ansehen“
auth.requestMatchers(HttpMethod.GET, "/api/admin/users", "/api/admin/users/**")
.hasAuthority("user:read"); // Minimal ausreichendes Recht
// PATCH auf Benutzer – das ist „ändern“
auth.requestMatchers(HttpMethod.PATCH, "/api/admin/users/**")
.hasAuthority("user:manage"); // Stärkeres Recht für Admin‑Aktionen
Schauen wir uns jetzt die Entwürfe an. Wir haben die Familie /api/drafts/**. Auf Request‑Level können wir zwei sinnvolle Schichten machen: Erstens, alle Entwürfe sind nicht öffentlich, also mindestens authenticated(). Zweitens, einige Operationen können wir enger über Authorities ausdrücken, damit der API‑Vertrag sofort widerspiegelt, dass „Entwurf erstellen“ ein separates Recht ist.
Aus Sicht des Kurses sieht das so aus:
| Endpoint‑Familie | Methode | Beispielbedeutung | Mögliche Anforderung auf Request‑Level |
|---|---|---|---|
| /api/drafts | GET | eigene Entwürfe ansehen | hasAuthority("draft:read:own") oder zumindest |
| /api/drafts | POST | Entwurf erstellen | hasAuthority("draft:create") |
| /api/drafts/* | PATCH | Entwurf ändern | hasAuthority("draft:update:own") |
| /api/drafts/* | DELETE | Entwurf löschen | hasAuthority("draft:delete:own") |
| /api/drafts/*/submit | POST | zur Moderation einreichen | hasAuthority("draft:submit:own") |
Der entscheidende Punkt: Auch wenn die Authority draft:update:own heißt, beweist der Request‑Level‑Matcher nicht, dass der Entwurf tatsächlich „eigen“ ist. Er sagt nur: „Der Benutzer hat grundsätzlich das Recht, seine eigenen Entwürfe zu aktualisieren.“ Das ist für die erste Schicht in Ordnung. Wichtig ist nur, nicht so zu tun, als hätten wir damit schon alles gelöst.
Code‑Fragment (wieder ein Ausschnitt):
import org.springframework.http.HttpMethod;
// innerhalb von authorizeHttpRequests(auth -> auth ...)
// Entwurf erstellen – separate Operation mit separatem Recht
auth.requestMatchers(HttpMethod.POST, "/api/drafts")
.hasAuthority("draft:create"); // „Erstellen“ != „Lesen“
// Aktualisieren eines bestimmten Entwurfs (ein Segment statt id)
auth.requestMatchers(HttpMethod.PATCH, "/api/drafts/*")
.hasAuthority("draft:update:own"); // Recht „grundsätzlich eigene aktualisieren“
// Löschen eines bestimmten Entwurfs
auth.requestMatchers(HttpMethod.DELETE, "/api/drafts/*")
.hasAuthority("draft:delete:own"); // Analog: das geht um die Möglichkeit, nicht um den Eigentumsnachweis
Und das dritte Beispiel – die Moderation. Auch hier lesen sich Methode + Pfad fast wie ein „Befehl“: publish und reject sind POST-Operationen auf einen bestimmten Draft. Das ideale Pfadmuster ist genau *:
import org.springframework.http.HttpMethod;
// innerhalb von authorizeHttpRequests(auth -> auth ...)
// Veröffentlichung – separater Befehl, der separates Recht erfordert
auth.requestMatchers(HttpMethod.POST, "/api/editor/drafts/*/publish")
.hasAuthority("draft:publish"); // Nicht jeder darf veröffentlichen
// Ablehnung – ein anderer Befehl (und oft ein anderes Recht)
auth.requestMatchers(HttpMethod.POST, "/api/editor/drafts/*/reject")
.hasAuthority("draft:review"); // Zum Beispiel Recht auf Review/Moderation
Beachten Sie, wie schön lesbar das ist: ein Blick genügt, um zu verstehen, welche Operationen es in der API überhaupt gibt und welche Rechte dafür nötig sind. Das ist einer der besten Effekte einer gut gemachten Request‑Level‑Authorization: sie wird zur Dokumentation, nicht zu „irgendwo im Kopf des Teamleads“.
7. Grenzen der Präzision bei Matchern
Wenn man zum ersten Mal spürt, wie schön Methode+Pfad den Zugriff ausdrücken, entsteht eine gefährliche Versuchung: „Lass uns doch noch eine Prüfung einbauen, dass der Draft genau meiner ist, und dass der Status DRAFT ist, und dass auch die Mondphase stimmt.“ Das ist der Moment, in dem man innehalten und sich erinnern sollte: Request‑Level‑Authorization ist Eingangskontrolle, kein vollständiges Sicherheitsmodell.
Erstens: Matcher sehen die Anfrage, aber nicht die Entität. Wenn PATCH /api/drafts/123 kommt, weiß die Request‑Level‑Konfiguration nicht, was 123 ist, wem es gehört und in welchem Status es sich befindet. Sie kann nur entscheiden: „Eine solche Anfrage kann grundsätzlich eintreten“ oder „überhaupt nicht“.
Zweitens: Übermäßig komplexe Path Patterns werden schnell zu einer Quest. Heute verstehen Sie alles. In einem Monat öffnen Sie die Konfiguration und fragen sich: „Wer hat /api/**/drafts/*/*/publish/** geschrieben und warum funktioniert das nur mittwochs?“ Security‑Code hat eine unangenehme Eigenschaft: Er kann „leicht falsch“ sein und trotzdem funktionieren – bis ein Vorfall passiert.
Drittens: Denken Sie an die „Lesbarkeit von oben nach unten“. Wenn Sie aus Gründen der Präzision Dutzende ähnlicher Matcher erzeugen, erhöhen Sie nicht die Sicherheit – Sie erschweren die Wartung. Eine gute Konfiguration besteht meist aus einigen großen Blöcken (public, authenticated, privileged), und präzise Method+Path‑Regeln werden dort eingesetzt, wo sie wirklich Sinn ergeben, nicht nur Rauschen erzeugen.
Und noch ein praktischer Hinweis: Wenn das Projekt einen Browser‑Client verwendet und Sie CORS einsetzen, kann der Browser OPTIONS‑Preflight senden. Das ist ein separates Thema, aber gerade methodenbewusste Regeln sind manchmal der Grund, warum „es im Browser nicht funktioniert, aber mit curl schon“. Reparieren Sie das nicht mit „Magie“; denken Sie daran, dass die Methode ebenfalls am Matching beteiligt ist.
8. Typische Fehler bei der Konfiguration von Matchern
Fehler Nr. 1: „Alle Methoden eines Pfades sind dasselbe.“
Ein Anfänger sieht /api/admin/users/** und hängt eine einzige Regel daran. Dadurch erhalten Lesen und Ändern denselben Zugriff, und das Authority‑Modell (user:read vs. user:manage) verliert seinen Sinn. Die Heilung ist einfach: Trennen Sie zuerst GET von PATCH/POST/DELETE, und überlegen Sie erst danach, wo Sie zusammenfassen können.
Fehler Nr. 2: Zu breites Path Pattern, das „Überflüssiges“ einfängt.
Das Muster /api/admin/** erscheint manchmal praktisch, weil „eine Zeile und fertig“. Aber es erfasst alles: Lesen, Verwalten und zukünftige Endpoints, an die Sie noch nicht gedacht haben. Sicherer ist es, enge Matcher für „Familien“ zu schreiben (/api/admin/users/**) und erst dann den Rest abzudecken, wenn das wirklich nötig und klar ist.
Fehler Nr. 3: Falsche Reihenfolge der Regeln.
Selbst perfekt ausgewählte Matcher werden zum Kürbis, wenn die „allgemeine“ Regel vor der „speziellen“ steht. Das häufigste Symptom: ein öffentlicher Endpoint verlangt plötzlich Authentifizierung, oder umgekehrt wird eine privilegierte Operation plötzlich „irgendwie seltsam“ zugänglich. Gewöhnen Sie sich an: zuerst enge Ausnahmen, dann breite Zonen, und immer am Ende eine verständliche abschließende Regel (üblich denyAll() für den Rest).
Fehler Nr. 4: Verwechslung von * und /**.
* – ein Segment, /** – Teilbaum. Wenn Sie versehentlich /** setzen, wo eine id hingehört, wird die Regel zu gefräßig: Sie beginnt, auf Pfade zuzutreffen, die nicht zur Operation gehören sollten. Wenn Sie * setzen, wo es tatsächlich mehrere Segmente gibt, „matcht“ die Regel nicht, und Sie erhalten ein unerwartetes 401/403, das aussieht, als ob „Spring Security schon wieder kaputt ist“. In Wirklichkeit ist es einfach Pattern‑Mathematik.
Fehler Nr. 5: Versuch, objektbezogene Einschränkungen nur mit Matchern zu lösen.
Wenn Sie anfangen, Patterns „für alle Lebenslagen“ zu erfinden, bedeutet das oft, dass Sie eine fachliche Einschränkung an einer Stelle ausdrücken wollen, die nur die URL sieht. Versuchen Sie, die Request‑Level‑Schicht als „ersten Drehkreuz“ zu behalten: Sie muss nicht alles über die Domäne wissen, sie soll Grenzen und Zonen schützen.
GO TO FULL VERSION