1. Unterschied zwischen 401 und 403 in REST-APIs
Wenn Sie anfangen, eine „echte“ JSON-API zu entwickeln, erkennen Sie recht schnell: Der Statuscode ist keine bloße Formalität, sondern ein Teil des Vertrags. Die Client-Anwendung, der mobile Client, die Postman-Sammlung und sogar Ihr zukünftiger automatischer Test lesen nicht nur den Antwortkörper. Sie lesen vor allem den Status und ziehen daraus Schlüsse: Muss ich mich anmelden, muss ein Bildschirm „Kein Zugriff“ angezeigt werden, soll die Anfrage wiederholt werden, oder soll der Server im Gegenteil nicht mehr mit Anfragen bombardiert werden?
Das Problem beginnt, wenn die API „irgendwie“ antwortet: mal 403 statt 401, mal umgekehrt. Für einen menschlichen Entwickler ist das zwar unangenehm, aber erträglich – er öffnet die Logs, schaut sich die Konfiguration an und versteht es. Für den Client führt das zu falschem Verhalten. Zum Beispiel denkt die Anwendung, der Benutzer habe „keine Berechtigung“, obwohl er sich einfach nicht angemeldet hat. Oder der Client beginnt, sich endlos „neu anzumelden“, obwohl die Anmeldung erfolgreich war – nur die Rolle passt nicht.
Um die Bedeutung zu verankern, können Sie eine kleine Tabelle im Kopf behalten (dies ist keine „heilig Wahrheit“, sondern eine praktische Gedächtnisstütze für das Design):
| Situation auf dem Server | Richtige Bedeutung | Typischer Code |
|---|---|---|
| Der Benutzer ist nicht authentifiziert (wir haben keine bestätigte Identität für die Anfrage) | „Stell dich erst einmal vor“ | 401 |
| Der Benutzer ist authentifiziert, aber die Zugriffsregel ist nicht erfüllt (Rolle/Berechtigung/andere Prüfung fehlgeschlagen) | „Wir haben dich erkannt, aber hier darfst du nicht rein“ | 403 |
Wichtig: Diese Codes betreffen nicht, wie schwerwiegend Ihr Vergehen ist. Es sind nicht „401 – eine kleine Strafe, 403 – permanent gesperrt“. Es sind unterschiedliche Arten von Fehlern, die unterschiedliche Fragen beantworten.
2. Mentales Modell: Identifikation und Zugriff
Man möchte 401 und 403 am liebsten als zwei magische Zahlen merken, aber Zahlen ohne Bedeutung bleiben schlecht im Kopf. Deshalb beginnt ein gutes mentales Modell immer mit einer menschlichen Geschichte. Stellen Sie sich vor, Ihre API ist ein Büro mit Sicherheitspersonal. Am Eingang werden zwei verschiedene Dinge geprüft.
Zuerst fragt der Wachmann: „Wer sind Sie?“ Wenn Sie keinen Ausweis zeigen, einen fremden Ausweis zeigen oder etwas zeigen, das nicht wie ein Ausweis aussieht (z. B. einen Studentenausweis von 2007 – auch Plastik, aber nicht geeignet), dann ist das Gespräch schnell beendet. Das ist 401: Das System hat keine Grundlage anzunehmen, dass die Anfrage von einem bestätigten Benutzer stammt.
Wenn Sie jedoch Ihren Ausweis gezeigt haben, der Wachmann Sie erkannt hat, Sie aber versuchen, in den Serverraum oder das Büro des Direktors zu gelangen, ist das eine andere Geschichte. Die Frage „Wer sind Sie?“ ist geklärt. Aber es stellt sich die Frage: „Was dürfen Sie?“ Und die Antwort kann „Nein“ lauten. Das ist 403: Die Identität ist klar, aber die Berechtigungen reichen nicht aus.
So kann man das als einfaches Schema der Anfrageverarbeitung darstellen (vereinfacht, aber nützlich für das Verständnis):
flowchart TD
A[HTTP-Anfrage] --> B{"Erfolgreiche Authentifizierung vorhanden?"}
B -- nein --> U[Antwort: 401]
B -- ja --> C{"Zugriffsregel erfüllt?"}
C -- nein --> F[Antwort: 403]
C -- ja --> D[Controller / Geschäftslogik]
Wenn Sie diese Verzweigung im Kopf behalten, werden Sie kaum noch verwirrt. Jede Diskussion „Ist das 401 oder 403?“ läuft auf eine einzige Prüfung hinaus: „Haben wir überhaupt einen bestätigten Benutzer im Kontext der Anfrage oder nicht?“
3. 401 Unauthorized: Wenn Sie „keinen Benutzer“ haben
401 ist die Situation, in der Sie auf eine geschützte Ressource zugreifen, aber für die aktuelle Anfrage keine erfolgreiche Authentifizierung erlangt werden konnte. Wichtig: „Konnte nicht erlangt werden“ bedeutet nicht nur „hat nichts gesendet“. Es bedeutet auch „hat etwas gesendet, aber falsch“, „hat etwas Abgelaufenes gesendet“, „hat Login/Passwort gesendet, aber sie stimmten nicht überein“, „das Konto ist gesperrt und der Authentifizierungsablauf schlägt fehl“.
Ja, der Name Unauthorized ist tatsächlich irreführend. Im Englischen ist das ein historisch verankerter Begriff, aber in unserem Kurs werden wir einfacher denken: 401 bedeutet fast immer Unauthenticated. Das heißt: „Das System betrachtet Sie nicht als angemeldet“.
Um dies in einem echten Projekt Secure Content Platform API zu sehen, nehmen wir den privaten Endpunkt /api/me. Nach unserem Zugriffsmodell sollte er nur für authentifizierte Benutzer funktionieren. Das bedeutet, dass ein Aufruf ohne Anmeldung zwingend 401 zurückgeben muss.
Der Controller könnte beispielhaft so aussehen (wir zeigen ihn nicht wegen der Logik, sondern zur Klarheit: Die Methode wird nur erreicht, wenn die Sicherheit sie durchlässt):
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@RestController
class MeController {
// Privater Endpunkt: Hier dürfen nur Anfragen nach erfolgreicher Authentifizierung hinein
@GetMapping("/api/me")
public Map<String, String> me() {
// Wenn Sie diese Nachricht sehen, hat die Sicherheitsschicht die Anfrage durchgelassen
return Map.of("message", "private zone");
}
}
Betrachten wir nun eine Anfrage ohne Authentifizierung (die ehrlichste Methode ist curl -i, um den Status zu sehen):
# -i wird benötigt, um den Statuscode und die Antwort-Header zu sehen
curl -i http://localhost:8080/api/me
# HTTP/1.1 401
Wenn Sie HTTP Basic verwenden (das haben wir bereits kennengelernt und es ist gerade für einen API-Client praktisch), führen auch falsche Anmeldedaten zu 401, da die Authentifizierung fehlgeschlagen ist:
# Falsche Anmeldedaten => Authentifizierung fehlgeschlagen => 401
curl -i -u anna:wrong http://localhost:8080/api/me
# HTTP/1.1 401
Beachten Sie einen feinen Punkt: Wenn es in Ihrem Projekt einen Benutzer anna gibt, das Passwort aber falsch ist, dann haben wir trotzdem „keine erfolgreiche Authentifizierung“. Das heißt, es ist nicht „403, weil Anna ja existiert“. Nein. Für die aktuelle Anfrage hat der Benutzer nicht bewiesen, dass er Anna ist.
Und noch eine wichtige Nuance aus den vorherigen Tagen: Wenn das Konto locked oder disabled ist, gilt der Login ebenfalls als nicht erfolgreich. Aus Sicht der Anfrage ist das wieder 401. Innerhalb von Spring Security mögen dort verschiedene Gründe (und verschiedene Ausnahmen) vorliegen, aber für den externen Client läuft es meist auf eines hinaus: „Die Authentifizierung ist fehlgeschlagen“.
4. 403 Forbidden: Wenn der Benutzer da ist, aber nicht darf
403 ist eine andere Art von Geschichte: „Wir haben dich erkannt, aber die Zugriffsregel wurde nicht erfüllt“. In Spring Security bedeutet das: Im SecurityContext existiert ein Authentication-Objekt, das als erfolgreich gilt (es ist also weder anonymous noch „Passwort falsch“), aber auf der Autorisierungsebene hat jemand „Nein“ gesagt.
Meistens ist der Grund für 403 in einem Schulungsprojekt sehr klar: Der Benutzer hat nicht die richtige Rolle oder Berechtigung. Beispielsweise versucht ein normaler USER, auf den Admin-Bereich zuzugreifen. Es ist jedoch nützlich zu wissen: 403 kann nicht nur wegen einer Rolle auftreten. Im session-basierten Zweig führt beispielsweise das Fehlen eines CSRF-Tokens bei einer state-changing-Anfrage oft ebenfalls zu 403. Wir gehen heute nicht näher auf CSRF ein, aber als Tatsache für die Fehlersuche sollten Sie das im Hinterkopf behalten, damit Sie nicht denken: „403 = genau die Rolle“.
Für unser Projekt ist der ideale Beispiel-Endpunkt /api/admin/users. Er sollte nur für die Rolle ADMIN zugänglich sein. Das bedeutet, dass ein normaler Benutzer nach erfolgreichem Login 403 erhalten muss.
Controller als Beispiel:
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@RestController
class AdminController {
// Admin-Endpunkt: Hier dürfen nur Benutzer mit der Rolle ADMIN hinein
@GetMapping("/api/admin/users")
public Map<String, String> users() {
// Wenn Sie diese Antwort sehen, ist die Rollenprüfung bereits bestanden
return Map.of("message", "admin zone");
}
}
Und nun die Anfrage. Angenommen, anna authentifiziert sich erfolgreich, hat aber die Rolle USER. Dann:
# Anmeldedaten korrekt => Authentifizierung vorhanden, aber Berechtigung fehlt => 403
curl -i -u anna:pass http://localhost:8080/api/admin/users
# HTTP/1.1 403
Das ist genau 403, weil die Frage „Wer bist du?“ bereits geklärt ist. Wir wissen, dass die Anfrage von anna kommt, nur ist sie kein Admin.
Wenn jedoch überhaupt keine Authentifizierung mitgeschickt wird, erhält man auf denselben Endpunkt 401, weil die Frage „Wer bist du?“ noch nicht einmal richtig begonnen hat:
# Keine Authentifizierung => 401 (nicht 403)
curl -i http://localhost:8080/api/admin/users
# HTTP/1.1 401
Das ist das entscheidende Muster: Dieselbe URL kann 401 oder 403 zurückgeben, je nachdem, ob der Client authentifiziert ist.
5. Ein Endpunkt: drei Ausgänge
Anfängern fällt es besonders schwer, sich daran zu gewöhnen, dass der Status nicht nur von der URL abhängt, sondern auch vom Zustand des Sicherheitskontexts. Daher ist es sehr nützlich, einmal die „drei Schicksale eines Endpunkts“ zu sehen. Nehmen wir /api/admin/users und betrachten, was in drei Zuständen passiert: anonymous, normaler Benutzer, Admin.
In Tabellenform sieht das so aus:
| Wer führt die Anfrage aus? | Was hat der Server? | Erwartetes Ergebnis |
|---|---|---|
| Anonymous (nichts gesendet) | Keine erfolgreiche Authentifizierung | 401 |
| USER (erfolgreich angemeldet) | Authentifizierter Benutzer vorhanden, aber keine ROLE_ADMIN | 403 |
| ADMIN (erfolgreich angemeldet) | Authentifizierter Benutzer + benötigte Rolle vorhanden | 200 |
Und mit curl kann man das buchstäblich in einer Minute durchspielen:
# 1) Anonymous => keine Authentifizierung => 401
curl -i http://localhost:8080/api/admin/users
# HTTP/1.1 401
# 2) USER => Authentifizierung vorhanden, aber Rolle reicht nicht => 403
curl -i -u anna:pass http://localhost:8080/api/admin/users
# HTTP/1.1 403
# 3) ADMIN => alles okay => 200
curl -i -u root:pass http://localhost:8080/api/admin/users
# HTTP/1.1 200
(Und ja, falls Sie jetzt denken: „Kann ich nicht immer 403 zurückgeben, um mir das Denken zu sparen?“ – das können Sie, aber dann muss der Client für Sie denken. Und er wird falsch denken.)
Dieses Beispiel untermauert noch einmal die Bedeutung: 401 bedeutet „nicht angemeldet“, 403 bedeutet „angemeldet, aber nicht erlaubt“.
6. Wie Spring Security die Entscheidung trifft: zwei „Barrieren“
Wenn Sie Spring Security als „einen großen Wachmann“ betrachten, werden Sie schneller verwirrt. Es ist hilfreicher, das System als zwei aufeinanderfolgende Barrieren zu sehen, die in den Filtern arbeiten, bevor der Controller erreicht wird. Die erste Barriere kümmert sich darum, eine erfolgreiche Authentifizierung zu erlangen und sie im SecurityContext zu speichern. Die zweite Barriere prüft die Zugriffsregeln für die konkrete Anfrage und entscheidet, ob sie durchgelassen wird.
In der Praxis lässt sich das sehr gut direkt aus der SecurityFilterChain ablesen. Schauen Sie sich eine typische Konfiguration für unsere Bereiche an:
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
// Regel Nr. 1: Alles unter /api/admin/** erfordert die Rolle ADMIN (Autorisierungsbarriere)
.requestMatchers("/api/admin/**").hasRole("ADMIN")
// Regel Nr. 2: Alle anderen Anfragen erfordern mindestens Authentifizierung (Authentifizierungsbarriere)
.anyRequest().authenticated()
)
// Spring Security entscheidet dann selbst, was zurückgegeben wird: 401 (keine Authentifizierung) oder 403 (Berechtigung fehlt)
.build();
}
Hier steckt die gesamte Logik der Statuscodes:
Wenn eine Anfrage an eine beliebige andere Stelle als /api/admin/** geht, ist authenticated() erforderlich. Fehlt die Authentifizierung, handelt es sich um die erste Barriere, und es ist sinnlos, weiter die Berechtigungen zu prüfen. Daher ist das logische Ergebnis 401.
Wenn die Anfrage an /api/admin/** geht, wird die Rolle ADMIN benötigt. Und hier ergibt sich eine Verzweigung: Fehlt die Authentifizierung, kommt wieder zuerst 401 (erste Barriere). Ist die Authentifizierung vorhanden, aber die Rolle fehlt, dann ist das die zweite Barriere, und sie endet mit 403.
Ein nützlicher Gedanke: Die Regeln authenticated(), hasRole("ADMIN"), hasAuthority("ADMIN") sind nicht „drei verschiedene Arten, einen Statuscode zurückzugeben“. Sie sind unterschiedliche Durchlassbedingungen. Und der Code (401 oder 403) wird danach ausgewählt, auf welcher Stufe Sie „gestolpert“ sind: beim Eintritt (kein Benutzer) oder bei der Zulassung (Benutzer vorhanden, aber nicht geeignet).
7. Wie ein falsch gewählter 401/403 den Client stört
Es scheint: „Ach, der Code unterscheidet sich nur um eine Ziffer.“ Aber in der Client-Welt sind diese Ziffern Befehle. Viele Clients verhalten sich so: Bei 401 muss der Anmeldebildschirm angezeigt oder die Anmeldedaten aktualisiert werden. Bei 403 hilft eine erneute Anmeldung nicht mehr; es muss eine Meldung „Kein Zugriff“ angezeigt werden, und es soll nicht versucht werden, die Anfrage durch Wiederholung zu „reparieren“.
Wenn Sie 403 zurückgeben, obwohl der Benutzer einfach nicht angemeldet ist, kann der Client eine sehr seltsame Benutzererfahrung erzeugen. Stellen Sie sich eine mobile App vor: Der Benutzer installiert die App, öffnet den Profilbereich und sieht sofort: „Zugriff verweigert“. Er hat noch nicht einmal den Anmeldebildschirm gesehen. Die Wahrscheinlichkeit, dass er denkt: „Oh, ich muss woanders auf den Anmeldebutton drücken“, ist etwa so hoch wie die Wahrscheinlichkeit, dass eine Katze selbst Ihre SecurityFilterChain konfiguriert (also nahe null).
Wenn Sie hingegen 401 zurückgeben, obwohl der Benutzer angemeldet ist, aber keine Berechtigung hat, könnte der Client in eine Endlosschleife aus „Neu anmelden und nochmal versuchen“ geraten. Manchmal äußert sich das als ständiges Zurückwerfen auf den Anmeldebildschirm nach jeder Anfrage an den Admin-Bereich, selbst wenn die Anmeldung korrekt ist. Der Benutzer gibt immer wieder das Passwort ein, beginnt Ihre Anwendung zu hassen, und Sie beginnen, das Leben zu hassen.
Daher ist die korrekte Semantik von 401/403 keine Ästhetik. Es geht um die tatsächliche Stabilität des Client-Server-Verhaltens.
8. Spickzettel für die Fehlersuche: Wie man 401 und 403 liest
Wenn Sie 401 sehen, ist es nützlich, nicht in Panik zu verfallen („Alles kaputt“), sondern sich zunächst eine einfache Frage zu stellen: „Sieht der Server überhaupt einen Benutzer?“ Und dann sehr konkret zu denken: War in der Anfrage ein Authorization-Header enthalten (bei Basic), war eine Session-Cookie vorhanden (im session-basierten Zweig), ist das Konto nicht gesperrt, haben wir uns bei Login/Passwort nicht vertan.
Wenn Sie 403 sehen, lautet die Frage bereits eine andere: „Der Benutzer ist da, aber welche Regel hat er nicht erfüllt?“ Dann beginnt die Lektüre der Zugriffsmatrix. Wenn die Konfiguration sauber geschrieben ist, finden Sie schnell den entsprechenden Matcher. Wenn Sie Rollen verwenden, vergleichen Sie die erwartete Rolle (ADMIN) mit den tatsächlichen Rollen des Benutzers. Wenn Sie bereits Berechtigungen (authorities) verwenden, prüfen Sie, ob in den GrantedAuthorities tatsächlich die benötigte Zeichenkette vorhanden ist (und ob Sie hasRole("ADMIN") nicht mit hasAuthority("ADMIN") verwechselt haben, denn das sind unterschiedliche Welten).
Auf der Denkebene kann man sich diese Reihenfolge merken: Zuerst beweisen, dass eine Authentifizierung existiert (sonst sind alle Gespräche über Rollen sinnlos), und erst dann die Berechtigungen analysieren. Das spart enorm Zeit, denn die meisten „403“ von Anfängern entpuppen sich als „Ich habe das Cookie / den Authorization-Header nicht übertragen“ und hätten 401 sein sollen, aber irgendwo hat die Browser-Semantik eingegriffen oder der Client hat ein anderes Verhalten erwartet.
Von dieser Verzweigung hängt auch die eigentliche Mechanik der Antwort direkt ab: Das Fehlen einer Authentifizierung sollte in einen bestimmten Security-Punkt münden, während ein Verbot für einen bereits bekannten Benutzer in einen anderen mündet. Wenn diese beiden Zweige korrekt getrennt sind, können sie auf einen einheitlichen JSON-Vertrag reduziert werden.
9. Typische Fehler bei der Unterscheidung von 401 und 403
Wenn Sie gerade anfangen, möchten Sie am liebsten eine „universelle Erklärung“ wie „401 ist, wenn das Passwort falsch ist, 403 ist, wenn die Rolle falsch ist“. Und das ist fast richtig, aber genau dieses „fast“ führt zu Kopfschmerzen. Die Fehler wiederholen sich von Projekt zu Projekt, daher ist es besser, sie jetzt kennenzulernen, solange wir ein Übungsprojekt und keinen brennenden Produktivbetrieb haben.
Fehler Nr. 1: 401 als „falsche Rolle“ interpretieren.
Dies ist die häufigste Verwechslung aufgrund des Wortes Unauthorized. In Wirklichkeit wird die Rolle erst geprüft, nachdem sich der Benutzer erfolgreich authentifiziert hat. Wenn Login/Passwort nicht stimmen oder gar nicht übermittelt wurden, ist das nicht „Rolle falsch“, sondern „Benutzer nicht vorhanden“. Daher lautet die grundlegende Prüfung: Bevor Sie über Rollen nachdenken, stellen Sie sicher, dass die Anfrage überhaupt authentifiziert wurde.
Fehler Nr. 2: Bei einer anonymous-Anfrage an einen privaten Endpunkt 403 zurückgeben.
Das passiert häufig, wenn der Entwickler die Statuscodes „von Hand“ im Controller zurückgibt oder wenn die Konfiguration so eingestellt ist, dass der Browser-Flow die wahre Bedeutung des Fehlers verschleiert. Für /api/me sollte ein anonymous-Benutzer 401 erhalten. Wenn er 403 bekommt, beginnt der Client zu denken, dass „der Zugriff für immer verweigert“ ist, obwohl man sich nur anmelden müsste.
Fehler Nr. 3: 401 zurückgeben, wenn der Benutzer bereits angemeldet ist, aber keine Berechtigung hat.
Zum Beispiel geht ein USER zu /api/admin/users, erhält 401, der Client denkt „Token/Session sind abgelaufen“ und schickt den Benutzer zur erneuten Anmeldung. Der Benutzer meldet sich erneut an und erhält wieder 401. Das sieht sehr schlecht aus und verwandelt das System in einen endlosen Korridor mit einer Tür, die immer verschlossen ist, aber jedes Mal wird gesagt: „Halten Sie Ihren Ausweis nochmal hin.“
Fehler Nr. 4: Versuchen, 401/403 durch ein if im Controller zu „reparieren“.
Der Controller ist ein viel zu später Punkt. Die korrekte Unterscheidung zwischen 401 und 403 erfolgt in der Sicherheitsschicht, bevor Spring MVC die Controllermethode auswählt. Wenn Sie anfangen, im Controller „wenn der Benutzer nicht der Richtige ist – gib den Status zurück“ zu schreiben, verdoppeln Sie die Sicherheitslogik, machen sie nicht zentralisiert und zerbrechlich. Außerdem riskieren Sie, versehentlich Zugriff zu gewähren, wo er nicht sein sollte, weil Sie einen der Eingänge übersehen haben.
Fehler Nr. 5: „Ein Text für alles“ – dieselbe Nachricht für 401 und 403.
Auch wenn das JSON-Format einheitlich sein kann (und das ist gut), sollte der Sinn der Nachrichten unterschiedlich sein. Für 401 sollte die Nachricht „Authentifizierung erforderlich“ lauten, für 403 „Authentifizierung vorhanden, aber Zugriff verweigert“. Sonst müssen Clients und Entwickler aus dem Kontext raten, was passiert ist – und Raten ist nicht die Fähigkeit, die wir in der Backend-Entwicklung fördern möchten.
GO TO FULL VERSION