1. Aktueller Benutzer: Das Problem „Wer hat das gemacht?“
Wenn wir ehrlich sind, hat das Backend keine Ahnung, „wer gerade die Anfrage stellt“ – wie ein Büro ohne Ausweise: Türen gibt es zwar, aber entweder laufen alle, wohin sie wollen, oder der Wachmann (also Ihr Controller) muss die Leute jedes Mal erneut befragen. Vor formLogin konnten wir bereits /api/me/** und andere Bereiche mit der Regel authenticated() schützen, aber das gibt uns noch keine anwendungsbezogene Antwort auf die Frage: „Welcher Benutzer ist genau eingeloggt und wie kann ich ihn im Code sehen?“
Im Projekt Secure Content Platform API ist das besonders bei /api/me spürbar. Dieser Endpunkt sollte eigentlich ein maximal einfaches „Schaufenster“ sein: „hier ist, wer du bist“ (username), „hier sind deine Rollen/Rechte“ (roles/authorities). Und hier ist ein wichtiger Punkt: Wir wollen den MeController nicht in einen Mini-Detektiv verwandeln, der selbst den Login/Passwort aus der Anfrage extrahiert und manuell prüft (das wäre traurig, unsicher und schwer zu warten). Wir wollen, dass die Sicherheitsschicht die Authentifizierung durchführt, und der Controller einfach das Ergebnis abholt.
2. Authentication nach dem Login: der „Ausweis“
Wenn sich ein Benutzer erfolgreich über formLogin anmeldet, setzt Spring Security nicht einfach einen Haken „Zugriff erlauben“. Es erzeugt ein Objekt vom Typ Authentication – das ist der „Ausweis“, den das System als gültig anerkennt. Es enthält „wer du bist“ (principal), „was dir erlaubt ist“ (authorities) und einige technische Felder, die helfen, die Sicherheit vorhersagbar zu machen.
Das Nützlichste für Anfänger ist, aufzuhören, Authentication als „etwas für das Framework“ zu betrachten, und zu erkennen, dass es ein ganz normales Objekt ist, mit dem man sicher im Anwendungscode arbeiten kann. Genau dieses Objekt landet später im SecurityContext und wird somit Ihrer Anwendung zugänglich.
Eine kleine Tabelle zur Orientierung (nicht zum Auswendiglernen, sondern zum Verständnis):
| Teil von Authentication | Bedeutung auf menschlich | Was wir normalerweise in der Anwendung verwenden |
|---|---|---|
| getName() | Login/Identifikator des Benutzers | fast immer, besonders für /api/me |
| getAuthorities() | Rollen und/oder Berechtigungen, die das System anerkannt hat | oft, um zu zeigen, „welche Rechte du hast“ |
| getPrincipal() | Benutzerobjekt (oft UserDetails) | manchmal, wenn mehr Daten benötigt werden |
| getCredentials() | Credentials, mit denen man sich angemeldet hat | fast nie (und das ist gut so) |
| isAuthenticated() | „gilt als authentifiziert“ | selten, normalerweise prüfen wir Zugriff über Regeln, nicht mit if-Anweisungen |
Und hier ist der Schlüssel: Nach erfolgreichem Login gilt genau dieses Authentication als die „Wahrheit“ über den Benutzer für die aktuelle Anfrage.
3. SecurityContext: Wo der Login gespeichert wird
SecurityContext kann man sich wie die Tasche im Sakko von Spring Security vorstellen, in der der besagte Ausweis Authentication steckt. Die Tasche hat eine einfache Aufgabe: zu speichern, „wer der aktuelle Benutzer ist“, sodass die Anwendung an allen Stellen innerhalb der Anfrageverarbeitung darauf zugreifen kann, ohne Parameter manuell zwischen Methoden herumzureichen.
Hier brauchen wir eine Tatsache: Nach erfolgreichem Login landet Authentication im SecurityContext, und Spring MVC kann es dann an den Controller übergeben. Den Mechanismus, der diesen Zustand über mehrere Browseranfragen hinweg aufrechterhält, können wir vorerst ignorieren.
Als kleine Skizze sieht das etwa so aus:
sequenceDiagram
participant B as Browser
participant S as "Spring Security (Filters)"
participant C as Controller
participant SC as SecurityContext
B->>S: "POST /login (username+password)"
S->>S: "Prüfung der Credentials über AuthenticationManager/Provider"
S->>SC: "SecurityContext.authentication = (erfolgreiches Authentication)"
B->>S: GET /api/me
S->>C: Anfrage durchgelassen, Authentifizierung bereits bekannt
C->>SC: Liest authentication
C-->>B: Antwort "current user: maria"
Hier steckt keine Magie à la „dem Controller wird es irgendwie klar“. Es gibt einen konkreten Gedanken: Die Sicherheitsschicht hat einmal die schwere Arbeit erledigt (Passwort geprüft), das Ergebnis an einer standardisierten Stelle (SecurityContext) gespeichert, und jetzt kann die Anwendung es nutzen.
4. Aktueller Benutzer im Controller
Jetzt das Beste: Sie müssen nicht in Request-Headers, Cookies oder andere Low-Level-Dinge eintauchen, um herauszufinden, wer der Benutzer ist. In Spring MVC können Sie einfach Authentication als Methodenparameter des Controllers akzeptieren. Das sieht fast zu einfach aus, daher glauben Anfänger oft nicht daran und versuchen, es „richtig zu machen“. Spoiler: „Richtig“ ist genau das.
Beginnen wir mit dem minimalsten /api/me, das den Namen des aktuellen Benutzers zurückgibt. In unserem Projekt ist das ein idealer „erster Sensor“, um selbst zu sehen: Vor und nach dem Login sieht die Anwendung unterschiedliche Zustände.
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
class MeController {
@GetMapping("/api/me")
String me(Authentication authentication) {
// Spring fügt automatisch die aktuelle Authentifizierung aus dem SecurityContext ein.
// Hier lesen wir KEINE headers/cookies und prüfen KEIN Passwort – alles wurde bereits von der Sicherheitsschicht erledigt.
return "current user: " + authentication.getName(); // z.B.: "current user: maria"
}
}
Wenn Sie als maria eingeloggt sind, erhalten Sie current user: maria. Wenn Sie als admin eingeloggt sind, erhalten Sie current user: admin. Und das ist bereits ein sehr mächtiger Moment: Ihr Anwendungscode hat kein Passwort geprüft, wusste nichts über UserDetailsService, hat PasswordEncoder nicht angerührt, sondern einfach das Ergebnis erhalten.
Um den Unterschied zwischen „anonymer Benutzer“ und „eingeloggter Benutzer“ besser zu verstehen, können Sie einen kleinen Debug-Endpunkt im öffentlichen Bereich erstellen. Er ist nicht geschäftskritisch, aber für das Lernen ein nützliches Mikroskop.
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
class AuthDebugController {
@GetMapping("/api/public/auth-debug")
String authDebug(Authentication authentication) {
// In einem öffentlichen Endpunkt können Sie sehen, wie Spring sich „vor“ und „nach“ dem Login verhält.
// Vor dem Login ist es normalerweise ein AnonymousAuthenticationToken, nach dem Login ein UsernamePasswordAuthenticationToken.
return authentication.getClass().getSimpleName() + " -> " + authentication.getName();
// vor Login: "AnonymousAuthenticationToken -> anonymousUser"
// nach Login: "UsernamePasswordAuthenticationToken -> maria"
}
}
Beachten Sie, dass dies ein öffentlicher Endpunkt ist (/api/public/**), sonst würden Sie vor dem Login nicht darauf zugreifen und den Vergleich nicht sehen.
Manchmal möchte man statt Authentication „einfach nur den Benutzernamen“ akzeptieren. Dann können Sie java.security.Principal verwenden – das ist ein allgemeineres Interface aus der Java-Welt. Aber auf Lernniveau ist Authentication meist verständlicher, weil es sowohl den Namen als auch authorities enthält.
import java.security.Principal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
class WhoAmIController {
@GetMapping("/api/me/who")
String who(Principal principal) {
// Principal ist ein allgemeineres Interface: im Wesentlichen „wer ist das“.
// Wenn Sie Rechte (authorities) benötigen, ist es bequemer, Authentication zu verwenden.
return "principal name: " + principal.getName(); // z.B.: "principal name: maria"
}
}
Aus Sicht des Kurses ist es nützlich, sich eine einfache Regel zu merken: Wenn Sie den „aktuellen Benutzer“ in der Web-Schicht benötigen, ist das erste und transparenteste Werkzeug ein Methodenparameter des Controllers.
5. Authorities: Anzeigen ohne Leaks
Wenn wir zum ersten Mal Authentication sehen, wollen wir sofort „alles ausdrucken und ansehen“. Für Lernzwecke ist das ein normaler Impuls, aber in einer echten Anwendung sollten Sie schnell die Gewohnheit entwickeln: Nach außen geben wir nur das weiter, was der Client wirklich braucht, und „leaken“ nie unnötige Informationen.
Zum Beispiel ist es für /api/me sehr sinnvoll, nicht nur username, sondern auch die Liste der authorities zurückzugeben. In unserem Projekt hilft das zu erklären, warum ein Benutzer auf /api/editor/** zugreifen kann und ein anderer nicht.
Erstellen wir ein kleines DTO (in Java 25 ist record sehr praktisch für solche „Datenkästchen“) und geben JSON zurück.
import java.util.List;
// DTO für die Antwort von /api/me: wir geben genau das zurück, was der Client braucht, ohne technische Details von Spring Security.
record MeResponse(String username, List<String> authorities) { }
Jetzt der Controller:
import java.util.List;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
class MeController {
@GetMapping("/api/me")
MeResponse me(Authentication auth) {
// Authorities sind „was erlaubt ist“, nicht „wer du bist“. Wir holen sie aus dem SecurityContext über Authentication.
List<String> authorities = auth.getAuthorities().stream()
// Wandeln GrantedAuthority in Strings wie ROLE_ADMIN / ROLE_EDITOR usw. um.
.map(GrantedAuthority::getAuthority)
.toList();
// Wir geben einen minimalen, stabilen API-Vertrag zurück: username + authorities.
return new MeResponse(auth.getName(), authorities);
}
}
Hier sind mehrere Punkte wichtig.
Erstens: Wir arbeiten mit auth.getAuthorities(), nicht mit dem Versuch, Rollen aus username zu erraten. username ist „wer“, authorities ist „was erlaubt“.
Zweitens: Wir geben nach außen Strings wie ROLE_ADMIN oder ROLE_EDITOR zurück (je nachdem, wie Sie Rollen konfiguriert haben) – das ist für ein Lern-API in Ordnung. Später können Sie entscheiden, wie Sie sie dem Client genau präsentieren, aber jetzt ist es wichtiger zu sehen, dass die Sicherheitsschicht bereits alles enthält.
Drittens: Wir rühren bewusst auth.getCredentials() nicht an. Selbst wenn Sie denken, „da ist doch sowieso ein Hash/kein Hash/irgendwas Unverständliches“ – diese Gewohnheit ist gefährlich. Ein guter SecurityContext ist einer, aus dem die Anwendung Identität und Rechte bezieht, aber keine Geheimnisse speichert oder weitergibt.
6. Verknüpfung von formLogin und /api/me
Damit diese Vorlesung nicht nur über „abstrakte Klassen“ spricht, verknüpfen wir zwei Dinge zu einem beobachtbaren Verhalten: erfolgreicher Login → Weiterleitung zum geschützten Endpunkt → Endpunkt sieht den aktuellen Benutzer.
Dazu konfigurieren wir bequem defaultSuccessUrl("/api/me", true), sodass der Browser nach dem Login sofort zu einem Endpunkt gelangt, der den SecurityContext in Aktion zeigt. In der Konfiguration (etwa so, in Ihrem SecurityConfig) könnte das kompakt aussehen:
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(auth -> auth
// Öffentliche Endpunkte sind ohne Login erreichbar (einschließlich unseres auth-debug).
.requestMatchers("/api/public/**").permitAll()
// /api/me/** erfordert Authentifizierung: ohne Login kommt man hier nicht hin.
.requestMatchers("/api/me/**").authenticated()
// Im Demo-Projekt ist es sinnvoll, alles andere zu schließen, um nicht versehentlich etwas offen zu lassen.
.anyRequest().denyAll()
)
.formLogin(form -> form
// Nach erfolgreichem Login sofort zu /api/me weiterleiten, um den aktuellen Benutzer zu sehen.
.defaultSuccessUrl("/api/me", true)
// Wichtig: Das Login-Formular muss für alle erlaubt sein, sonst gibt es eine „ewige Weiterleitung“.
.permitAll()
)
.build();
}
Jetzt haben Sie eine sehr transparente „Demonstrationsschleife“:
1) Sie öffnen im Browser /api/me.
2) Wenn Sie nicht eingeloggt sind, leitet Spring Security Sie zum Login weiter.
3) Sie geben Login/Passwort ein.
4) Bei Erfolg landen Sie sofort auf /api/me, und der Controller zeigt Ihnen username (und, wenn Sie die JSON-Version gemacht haben, authorities).
Hier ist es nützlich, das Ergebnis vom Speichermechanismus zu trennen. Für /api/me brauchen wir jetzt nur das Ergebnis: Der Controller erhält ein bereits fertiges Authentication und setzt den Benutzer nicht neu zusammen. Hinter den Kulissen gibt es eine separate Verknüpfung von HttpSession und Browser-ID (JSESSIONID): Nur so kann eine neue Anfrage bereits als „nicht mehr völlig fremd“ ankommen. Ohne sie gäbe es weder das Gefühl „ich bin schon eingeloggt“ noch ein sinnvolles Logout.
In diesem Moment sollte es im Kopf klick machen: /api/me ist kein „besonderer Endpunkt“, er ist ganz normal. Die Sicherheitsschicht hat nur dafür gesorgt, dass, wenn der Controller aufgerufen wird, der aktuelle Benutzer bereits bekannt und als Authentication verfügbar ist.
Wenn Sie das Gefühl „derselbe Endpunkt – verschiedene Benutzer“ verstärken möchten, melden Sie sich mit drei verschiedenen Konten an (USER, EDITOR, ADMIN) und schauen Sie, was sich bei den authorities ändert. Das festigt hervorragend den Unterschied zwischen „wer du bist“ und „was erlaubt ist“: username ist unterschiedlich, authorities ebenfalls, aber der Controller-Code ist derselbe.
7. Typische Fehler im Umgang mit SecurityContext
Fehler Nr. 1: Versuchen, das Passwort im Controller zu überprüfen.
Manchmal versuchen Studenten, nachdem sie Authentication als Methodenargument gesehen haben, trotzdem Login und Passwort aus der Anfrage zu holen und sie „zur Sicherheit“ zu vergleichen. In der Praxis zerbricht das die Architektur: Sie duplizieren die Arbeit von DaoAuthenticationProvider, schaffen neue Datenlecks und verwandeln den Controller in eine Sicherheitsmaschine. Der Controller sollte geschäftliche Entscheidungen treffen, nicht die Identität bestätigen – das ist Aufgabe der Sicherheitsschicht.
Fehler Nr. 2: Erwarten, dass Authentication „vor dem Login“ null ist.
Intuitiv scheint es, dass vor dem Login „kein Benutzer da ist“, also authentication == null gilt. In einem typischen Spring-Security-Szenario ist das nicht der Fall: Für nicht authentifizierte Anfragen wird oft eine anonyme Authentifizierung erzeugt, und authentication.getName() kann "anonymousUser" sein. Schreiben Sie daher keine Geschäftslogik im Stil „wenn null, dann Gast“: Drücken Sie den Zugriff besser über permitAll()/authenticated() in der Konfiguration aus, und fordern Sie bei /api/me einfach authenticated().
Fehler Nr. 3: Unnötige Informationen aus Authentication nach außen geben.
Es ist verlockend, in /api/me das gesamte authentication.toString() zurückzugeben oder den principal „wie er ist“ zu serialisieren. Das führt schnell zu unerwarteten Problemen: Erstens könnten Sie versehentlich technische Felder preisgeben, zweitens binden Sie den API-Vertrag an interne Spring-Security-Klassen, was ihn zerbrechlich macht. Für eine öffentliche API ist es besser, ein minimales DTO zurückzugeben: username + authorities (und das bewusst).
Fehler Nr. 4: „username“ und „Rolle“ verwechseln.
Anfänger machen manchmal eine Prüfung wie „wenn username == admin, dann lassen wir ihn rein“. Das sieht lustig aus, bis es beängstigend wird. username ist die Identität, und Berechtigungen sollten durch roles/authorities und Zugriffsregeln im SecurityFilterChain beschrieben werden. Selbst wenn Sie in der Demo einen Benutzer admin haben, heißt das nicht, dass „der Name admin = Admin-Recht“ ist.
Fehler Nr. 5: /api/me schützen, aber Login/Logout vergessen zu öffnen.
In der Konfiguration kann man leicht versehentlich eine sehr breite Regel schreiben (z. B. anyRequest()authenticated()) und vergessen, dass der Einstiegspunkt ebenfalls erreichbar sein muss. Dann erhalten Sie eine „ewige Weiterleitung“: Der Benutzer kann nicht zur Login-Seite gelangen, weil er sich „erst einloggen“ müsste. Die Abhilfe ist einfach: Bei formLogin und logout sollte permitAll() stehen, und diese Punkte sollten in der Konfiguration sichtbar sein.
GO TO FULL VERSION