Spring REST-API mit Spring Security absichern und mit Angular SPA authentifizieren

Published by Philipp Schuster on

Java Code (Symbolbild)

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 Filtern 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 einer Authentication das Property authenticated nicht auf true, so wird von Spring das Authentication-Objekt jedem Spring Security bekannt gemachten AuthenticationProvider durchgereicht und ein Provider, der den Typ des Authentication-Objekt unterstützt kann mit dem Authentication-Objekt weitere Operationen durchführen (z.B. das Objekt anreichern) oder eine AuthenticationException werfen
  • Gibt der AuthenticationProvider null zurück oder wirft eine AuthenticationException 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 Property authenticated einer Authentication 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.


Philipp Schuster

Hi, I'm Philipp and interested in Computer Science. I especially like low level development, making ugly things nice, and de-mystify "low level magic".

0 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *