Spring REST-API mit Spring Security absichern und mit Angular SPA authentifizieren
Update: Ich habe den Code nochmal überarbeitet und optimiert.
Bei meinem aktuellen Hobbyprojekt stand ich vor dem Problem einen Spring Boot REST-Service richtig abzusichern und mich mit meiner Single Page Application, einem Angular Frontend, dagegen zu authentifizieren. Ich hatte dabei große Probleme mit der Konfiguration von Spring Security und wie der Client damit interagieren soll. Spring Security ist sehr groß, mächtig und zunächst sehr undurchsichtig. Viele Tutorials empfand ich zudem als wenig hilfreich, sodass ich mich letztendlich das ganze Wochenende lang sowie gestern und heute immer wieder damit beschäftigt habe. Mittlerweile ist mein Bild von Spring Security deutlich besser geworden und ich habe viel verstehen können. Ich habe meinen Code in den letzten Tagen Dutzendfach überarbeitet und möchte nun gerne meine Erkenntnisse mit euch teilen. Für weitere Tipps bezüglich meiner Spring Security Konfiguration bin ich natürlich offen.
Hinweis: Ein grundsätzliches Verständnis von Spring Boot wird vorausgesetzt.
Mein Technologie-Stack
Ich habe eine Spring Boot-Anwendung, mit der ich einen REST-Service baue. Ich nutze Spring 2.1.0 dafür. Als Frontend habe ich eine Single Page Application mit Angular 6. Ich möchte die Anwendung mit einem Token absichern, damit ich nicht bei jedem Request den HTTP Basic-Auth-Header mitsenden muss. Das würde nämlich erfordern, dass das Passwort im UI gespeichert wird, z.B. im Local Storage, was nicht so cool ist. Ich brauche keine speziellen Berechtigungen an einem Token bzw. habe ich die Berechtigungen so am User, den ich über das Token raus kriege. Prinzipiell könnte ich jetzt aber denke ich auch einfach JWT einbauen und meine Token-Entität einfach ersetzen bzw. umbauen. Aber zunächst war mir erstmal wichtig zu verstehen wie es von Hand geht! Ich habe hierbei extrem viel über Spring Security gelernt und kann endlich meine REST-Services absichern!
Grundlegende Erkenntnisse über Spring Security
Zu Beginn möchte ich gleich einmal die wichtigsten Erkenntnisse mit euch teilen
- Spring Security geht bei jeder Anfrage an den Server intern eine Kette an
Filter
n durch - Jeder dieser
Filter
kann auf den HTTP-Request zugreifen und bspw. die Header-Daten lesen - ein Filter kann ein
Authentication
-Objekt dem Spring Security Context bekannt machen (wichtiger Punkt!) - man kann Spring eigene Implementierungen von
Authentication
anbieten oder vorhandene nutzen - setzt man in einem
Filter
beim Erstellen einerAuthentication
das Propertyauthenticated
nicht auftrue
, so wird von Spring dasAuthentication
-Objekt jedem Spring Security bekannt gemachtenAuthenticationProvider
durchgereicht und ein Provider, der den Typ desAuthentication
-Objekt unterstützt kann mit demAuthentication
-Objekt weitere Operationen durchführen (z.B. das Objekt anreichern) oder eineAuthenticationException
werfen - Gibt der
AuthenticationProvider
null zurück oder wirft eineAuthenticationException
gilt der Nutzer als nicht eingeloggt - Spring hat bereits eine Vielzahl vorgefertigter Filter und Authentication-Objekte (z.B. muss man keinen “HttpBasicAuthFilter” selbst schreiben)
- ist dem Spring Security Context ein
Authentication
-Objekt bekannt, das nicht null ist, gilt der Nutzer als eingeloggt. Das Propertyauthenticated
einerAuthentication
hat darauf keine Auswirkung! (siehe nochmal Punkt 5) - nochmal zu der Kette an Filtern, die Spring durchgeht: Der letzte Filter ist die Instanz, die schaut, ob der Nutzer authentifiziert ist und nun Zugriff auf die Ressource bekomm: Daher muss ein Filter zuvor ein
Authentication
-Objekt erzeugt haben
Mein Authentifizierungsablauf
Im Angular-Frontend habe ich ein Login-Formular, welches aus Nutzername und Passwort besteht. Beim Einloggen schickt es einen HTTP Basic-Auth Anfrage an den Endpunkt /api/auth/login
. Dort nimmt Spring Security entgegen, dass ich mich mit Benutzername und Passwort (via HTTP Basic-Auth) einloggen möchte. Die Anfrage geht zu meinem BasicAuthenticationProvider
. Der kriegt von Spring Security Benutzername und Passwort. Hier kann ich dann meinen UserService
fragen, ob er den Benutzernamen kennt. Ist das der Fall, wird das gehashte Passwort mit dem Hash in der Datenbank verglichen. Schlägt etwas fehl, wird null
(statt ein Objekt vom Typ Authentication
) zurück gegeben (man könnte bzw. sollte lieber eine AuthenticationException
werfen, das werde ich noch überarbeiten). Klappt alles, dann erstellt der Provider mir das Token. Das wird fortan das Auth-Token des Clients, das er sich speichern muss und bei allen Anfragen gegen gesicherte Endpunkte als X-Token: ...
-Header mitsenden. Ein Token ist bei mir eine Datenbank-Entität und der Nutzer kriegt eine UUID für diese Entität.
Dieser Token wird an den Nutzer in Form einer UUID zurück gegeben, wenn der Request an /api/auth/login
erfolgreich war. Bevor der Request aber fertig beantwortet ist, setzt der BasicAuthenticationProvider
das aktuelle Authentication
Objekt (mittels return in der .authenticate()
-Methode) auf eine Instanz meiner Klasse TokenAuthentication
. In diesem Authentification-Objekt steht dann fortan das Principal
, was ein Objekt meiner User-Repräsentation ist. Ich kann also fortan in Spring (bspw. in Controller-Methoden das Authentication
-Objekt injecten mittels DI) So kann ich im Controller dann folgenden Code schreiben (siehe die mit /login annotierte Methode):
Das Frontend weiß, dass bei einem HTTP 200 OK die Antwort das Token ist und speichert es sich. Kommt ein anderer Statuscode zurück weiß das Frontend, dass es einen Fehler gab und dem Nutzer das Loginformular erneut präsentiert werden muss.
Kommen wir nun zur Validierung des Tokens bzw. des X-Token
-Headers. In meiner Spring Security Config habe ich eine eigene Implementierung des Interfaces Filter
angegeben. Hier kommt meine Klasse AuthTokenFilter
zum Vorschein. Spring funktioniert so, dass es alle ihm bekannten Filter der Reihe nach durcharbeitet bis es am finalen Filter ankommt, der schaut ob für die aktuelle Ressource eine Authentifizierung gebraucht wird. Dann wird das aktuelle Authentication Objekt geholt, das nicht null
sein darf für eine erfolgreiche Authentifizierung.
Mein Filter überprüft bei jeder Anfrage den Request. Er schaut, ob der X-Token: ...
-Header enthalten ist. Ist das der Fall, so wird in der Datenbank geschaut, ob eine Token-Entität mit der UUID existiert und eine Objekt vom Typ TokenAuthentication
gebaut. Das wird Spring Security bekannt gemacht mittels SecurityContextHolder.getContext().setAuthentication(auth);
. Hier ein wichtiges Detail. Hat das Authentication
-Objekt hier noch den Wert isAuthenticated() == false
, so weiß Spring, dass es einen AuthenticationProvider
suchen muss, der den Typ der Authentication (TokenAuthentication .class
) unterstützt. Genau dafür habe ich einen TokenAuthenticationProvider
, der dafür zuständig ist. Der überprüft dann in der Datenbank, ob das Token überhaupt valide ist und nicht mittlerweile zum Beispiel expired. Ist das der Fall, geschieht nichts, andererseits wird eine AuthenticationException
geworfen. Ferner reichere ich hier daas Objekt mit dem User (Principal
) an.
SpringSecurityConfig
Schaut hier bitte auf configure(AuthenticationManagerBuilder auth)
sowie configure(HttpSecurity http)
ab Zeile 65. Das sind die wichtigen Stellen.
BasicAuthenticationProvider
Hier kommt in die authenticate(Authentication authentication)
ein von Spring erstelltes UsernamePasswordAuthenticationToken
rein. Siehe supports(Class> authentication) in Zeile 82
.
TokenFilter
Das ist der Filter, der schaut, ob ein “X-Token”-Header gesetzt ist. Ganz wichtig ist die Zeile SecurityContextHolder.getContext().setAuthentication(auth);
.
TokenAuthentication
So sieht Spring mein Authentifizierungs-Token. Das UserDto ist übrigens eine Abbildung meiner User-Entität aus der Datenbank. Man findet darin den Nutzername, die Rollen und den Namen.
TokenAuthenticationProvider
Das hier ist eine der wichtigsten Klassen für die Token-Authentifizierung. Es nimmt das TokenAuthentication
-Objekt, das der Filter dem Spring Security Context bekannt gemacht hat und validiert es. Das geschieht, wenn isAuthenticated()
des TokenAuthentication
-Objekts noch false
ist! Andernfalls würde der Provider nicht aufgerufen werden.
Angular / SPA
Hier arbeite ich mit HttpInterceptoren um den “X-Token”-Header in jeden Request hinzuzufügen. Das Token speichere ich in einem Angular-Service, der es zudem im Local Storage hält. Das ist im Internet bereits gut dokumentiert und darauf würde ich nur bei konkreten Fragen eingehen. Schaut doch einfach auf meinem Gitlab-Projekt vorbei und lernt vom Code!
Fazit
Natürlich besteht hier das Problem, dass jemand mit Zugriff zum Computer des Opfers mit den Developer Tools des jeweiligen Browsers ohne Probleme das Token aus dem Local Storage auslesen kann. Doch das ist in der Regel ja kein realistisches Szenario. Meine Lösung ist nicht geschützt gegen solche Attacken. Aber hier ging es ja auch primär darum zu verstehen wie Spring Security funktioniert und wie man einen Token-Mechanismus implementiert. Man könnte mein Token durch bspws. JWT austauschen, schauen ob das Token zur IP und dem User-Agent passt oder sonstige Dinge überprüfen. Für Vorschläge bin ich offen. Ich habe mit diesem Projekt jedenfalls sehr viel über Spring Security gelernt. Und vor allem kann ich mich endlich dagegen authentifizieren. Zuvor habe ich es nicht hinbekommen.
0 Comments