Spring Boot - Microservice & Rest API mit JWT & Spring Security absichern

In diesem Tutorial zeige ich dir wie du mit Spring Boot, Spring Security & JWT dein Micrsoservice oder Rest API gegen Unbefugte absichern kannst.
spring boot security jwt tutorial

Spring Boot Security – Sichere Microservices & RESTful API’s entwicklen mit JWT

Einleitung

Jeder Programmierer oder Softwareentwickler, der einen Microserive entwickelt, stellt sich irgendwann die Frage: „Wie sichere ich mein Microserivce gegen Unbefugte?“ oder „Wie erreiche ich, dass meine Microservices nur von angemeldeten Usern genutzt werden darf?“. In diesem Tutorial werde ich euch zeigen, wie ihr eure Microservices oder Rest API gegen unbefugte Nutzung sperren könnt. Hierfür werden wir Spring Boot, Spring Security und JWT verwenden. Wir werden zwei Microservices erstellen, an denen ich euch zeigen werde, wie ihr einen JWT Token erstellt und wie ihr diesen JWT Token nutzen könnt, um eure Microservices und Rest APIs abzusichern.

Voraussetzung

Ein Microservice oder RESTful API, der nicht abgesichert ist!

Als Microservice Beispiel haben wir einen InfoService, der einen InfoResponse Object zurück gibt, in dem die aktuelle Serverzeit enthalten ist. Hierfür haben wir einen RestController, der über die URL http://localhost:8080/info das InfoObject zurückgibt. Der Service ist absichtlich simpel gehalten, da es in dem Tutorial hauptsächlich darum geht, einen Microservice abzusichern. Wie man umfangreichere RestController erstellt, könnt ihr hier bei meinem Tutorial „Spring Boot – RestController Klassen erstellen“ nachlesen.

Folgender Abschnitt zeigt einen kompletten RestController, der über den GET Aufruf von http://localhost:8080/info das InfoObject zurück gibt.

Mit dem Tool Postman können wir nun unseren Rest API aufrufen und sehen dann folgendes Ergebnis.

Was ist ein JWT Token?

Wir haben es jetzt geschafft, dass wir unsere Rest API konsumieren können. Das Problem hierbei ist aber, dass jeder diese API konsumieren kann, wenn der Microservice im Internet verfügbar wäre. Genau das wollen wir mit diesem Tutorial verhindern. Wir müssen es schaffen, dass unser Microservice bzw unsere Rest API erkennt, von wem die Anfrage kommt. Hierfür können wir JWT Token nutzen.

JWT steht für JSON Web Token. Im Grunde ist es wie ein Ticket bzw. Zugangskarte, welches der Konsumierer eines Microservices erhält, mit dem er sich beim Konsumieren eines Microservices ausweisen muss. Ein JWT Token wird stets serverseitig erstellt, signiert und dem Konsumenten zur Verfügung gestellt.

Wie das geht, werden wir weiter unten erfahren. Wichtig bei der Nutzung von einem JWT Token ist es, dass der Token bei jeder Anfrage an das Microservice mit versendet wird. Der Microservice, in unserem Fall unser InfoService, validiert dann den JWT Token und entscheidet, ob er die HTTP Anfrage zulässt oder nicht. Man kann das Ganze mit der Zugangskarte vergleichen. Natürlich ist eine Zugangskarte nicht für die Ewigkeit gültig. Spätestens, wenn man das Unternehmen verlässt, wird einem Mitarbeiter die Zugangsakarte entzogen. Dies gilt auch für unseren JWT Token. Unser Token wird einen „Ablaufdatum“ erhalten. Dieses Datum definiert, wann unser JWT Token nicht mehr gültig ist. Wichtig hier ist es, dass abgelaufene Token vom InfoService mit dem HTTP Status 401 beantwortet werden.

Wie ein JWT Token aufgebaut ist, könnt ihr auf der folgenden Wikipedia Seite nachschauen.

AuthenticationService

Der AuthenticationService – Die Grundlage für die Sicherheit unserer Microservices

Nun wissen wir, dass wir einen JWT Token brauchen. Die Frage ist nun: „Wie erzeugen wir uns einen JWT Token?“, den wir dem Client senden können?

Hierfür werden wir einen neuen Authentication Microservice erstellen. Dieser hat die Aufgabe, über einen HTTP Post Aufruf für den Client einen JWT Token zu erstellen. Die URL für den Aufruf des AuthenticationService sieht dann wie folgt aus: http://localhost:8888/login. Wie ihr sehen könnt, wird dieser Microservice mit dem Port 8888 gestartet. Würde man hier den Default Port (8080) von Spring Boot verwenden, dann würde der Microservice, den man als zweites starten möchte, nicht gestartet werden, weil der Port schon belegt ist.

Wie oben erwähnt, werden wir einen HTTP Post Aufruf an unseren AuthenticationService senden. Der Grund, wieso hier explizit ein POST Aufruf gesendet wird und nicht ein GET ist der, dass GET Anfragen von vielen HTTP Servern, wie Apache Webserver oder nginx in der access.log geloggt werden. Somit würde man das Passwort des Users der einen JWT Token haben möchte, in die Access Logs schreiben, welches ein unnötiges Sicherheitsrisiko ist.

Folgender Code Abschnitt zeigt den kompletten RestController für den AuthenticationService:

Mit einem RestController ist unser AuthenticationService noch nicht fertig implementiert. Wie ihr an dem Code Beispiel sehen könnt, wird die Anfrage an die Methode getJWTToken() der Klasse AuthenticationService.java weiter geleitet. In dieser Methode wird unser JWT Token erstellt und signiert.

Folgender Code Abschnitt zeigt den kompletten AuthenticationService Code

An der Implementrierung der Methode erkennen wir, dass zuerst über das AccountRepository genutzt wird um ein Account Objekt in der Datenbank zu suchen. Wie Spring Boot Repository Interfaces funktionieren, könnt ihr bei meinem Tutorial „Spring Boot Repository“ nachlesen.

Die Methode findOneByUsername gibt als Rückgabewert ein Optional Object zurück, welches entweder leer sein kann oder ein Account Objekt beinhalten kann. Auf das Optional Objekt können wir jetzt die Filter Methode aufrufen. Hier prüfen wir nun, ob das verschlüsselte Passwort auch das ist, welches übertragen wurde. Sollte die Filter Methode ein true zurück liefern, dann können wir das Ergebnis an den JwtTokenService weitergeben, der uns den JWTTokenResponse erzeugt.

Beachtet, dass die map Methode hier nur aufgerufen wird, wenn die Filter Methode ein true zurück geliefert hat. Sollte die Filter Methode ein false liefern, also die Passwörter nicht identisch sind, dann wird eine EntityNotFoundException geworfen, welches beim übergeordneten AuthenticationController über den ExceptionHandler abgefangen wird. Die orElseThrow Methode wird auch aufgerufen, wenn das Optional Objekt ein empty beinhaltet. Also kein Account Objekt zu dem Usernamen gefunden werden konnte.

Nachdem wir uns die Implementierung in der AuthenticationService Klasse angeschaut haben, schauen wir uns nun die Klasse JwtTokenService.java an. Hier konzentrieren wir uns auf die Methode String generateToken(String username), welches uns den Token erstellt. Ich werde hier nicht detailliert auf die Implementierung eingehen. Die Implementierung ist selbsterklärend.

Folgender Code Abschnitt zeigt den kompletten JwtTokenService Code:

AuthenticationService testen

Nun sind wir mit der Implementierung des AuthenticationServices fertig! Jetzt werden wir unser Rest Interface testen ob dieser genau das macht, was wir implementiert haben.

Bevor ihr die Spring Boot Applikation startet, solltet ihr wissen, dass ihr eine laufende mongoDb Installation oder einen MongoDB Container auf Port 27017 benötigt. Die Spring Boot Applikation ist so implementiert, dass beim Start der Anwendung ein Demo Account mit den Werten Username: DemoAccount und Passwort: DemoPassword angelegt wird. Die Implementierung hierfür findet ihr in der Klasse ApplicationStartup.java.

Nachdem wir nun unsere AuthenticationService Microservice gestartet haben, senden mir mit Postman einen einen HTTP Post Aufruf auf die URL: http://localhost:8888/login.

Als BodyRequest senden wir folgende JSON Daten:

An dem Bild open erkennen wir nun, dass wir einen JWT Token erhalten haben. Euren Token könnt ihr nun auf der Seite https://jwt.io/ prüfen, ob dieser gültig ist. Achtet hier bitte darauf, dass ihr euren secret braucht, um zu prüfen, ob die Signatur des Tokens stimmt. Euren secret, welches in der application.yml hinterlegt ist solltet ihr Niemandem geben!

Wir sollten auch testen, ob unser AuthenticationService auch funktioniert, wenn wir einen Username eingeben, welches nicht in der MongoDB enthalten ist. Hierfür senden wir eine erneute Anfrage und verändern den Usernamen in der HTTP Post Anfrage. Nun sollten wir ein HTTP Status 404 als Response Code bekommen. Zusätzlich kann man auch testen, indem man das Passwort verändert. Auch in diesem Fall sollten wir einen HTTP Status 404 als Response Code erhalten. Diesen Test überlasse ich euch.

Der sichere InfoService

Den AuthenticationService haben wir nun abgeschlossen. Jetzt ist es an der Reihe, den InfoService mit Spring Security abzusichern. Spring Security ist ein eigenständiges Spring Projekt, welches dem Entwickler erlaubt, auf einfachste Art & Weise seine Spring Boot Anwendung abzusichern.

Die pom.xml Datei erweitern

Der erste Schritt ist die Erweiterung der pom.xml Datei. Hier müssen wir die dependency für Spring Security einbinden. Nach dem Einbinden ist Spring Security direkt aktiv. Das passiert nur, weil Spring Boot Security per default so konfiguriert ist, dass über die AutoConfiguration von Spring Boot die entsprechenden Konfigurationen geladen werden.

Folgender Code Abschnitt zeigt die komplette pom.xml Datei:

Die Filter Klasse

Als nächstes brauchen wir einen ServletFilter, mit dem wir den JWT Token aus der HTTP Anfrage extrahieren können. In der Klasse JwtAuthenticationTokenFilter.java werden wir hierfür den JWT Token aus dem Request Object übernehmen und diesen an den SecurityContext von Spring Security übergeben. Die Übergabe an den SecurityContext sorgt dafür,  dass der Token von Spring Security ausgewertet wird.

Folgender Code Abschnitt zeigt die komplette JwtAuthenticationTokenFilter.java Datei

Die JwtAuthenticationEntryPoint Klasse

Als nächstes schauen wir uns die Klasse JwtAuthenticationEntryPoint.java an. Dieser dient dazu, um bei Anfragen, die unautorisiert sind, einen HTTP Status Code SC_UNAUTHORIZED zurück zu geben.

Folgender Code Abschnitt zeigt die komplette JwtAuthenticationEntryPoint.java Datei:

Die JwtAuthenticationProvider Klasse

Die eigentliche Überprüfung findet dann in der Methode public Authentication authenticate(Authentication authentication) der JwtAuthenticationProvider.java Klasse. Hier wird der Token, den wir vorher in der JwtAuthenticationTokenFilter Klasse extrahiert haben, ausgewertet. Zusätzlich wird der Username aus dem Token extrahiert und, falls der Token valide ist, als JwtAuthenticatedProfile zurück gegeben.

Somit steht dann das JwtAuthenticatedProfile Objekt dem Spring SecurityContext zur Verfügung und kann zu einem späteren Zeitpunkt, an diversen Stellen im Code, ausgewertet werden, wenn man es will.

Sollte der Token nicht valide sein, dann wird eine JwtAuthenticationException geworfen. Unter Umständen kann es vorkommen, dass eine HTTP Anfrage gesendet wurde, wo zwar ein Bearer Token im Header enthalten ist, aber dieser Token kein echter JWT Token ist und somit nicht ausgewertet werden kann, dann wird ebenfalls eine JwtAuthenticationException geworfen.

Folgender Code Abschnitt zeigt die komplette JwtAuthenticationProvider.java Datei:

Die WebService Security Config Klasse

Zum Schluss muss nur noch die Spring Security Config an die eigene Anwendung angepasst werden. Hierfür schreiben wir eine eigene Config Klasse mit dem Namen WebSecurityConfig.java. In dieser Konfigurationsklasse werden alle Security bezogenen Konfigurationen vorgenommen.

In unserem Beispiel haben wir keine URL, die wir nicht absichern wollen. Jede URL, die von Spring Boot aus angeboten wird, wird abgesichert.

Folgender Code Abschnitt zeigt die komplette WebSecurityConfig.java Datei:

InfoService RESTful API mit Spring Security testen

Nachdem nun alles implementiert ist und die Spring Security Config durch die Annotation @EnableWebSecurity aktiviert wurde, starten wir unsere beiden Microservices neu. Wie ihr gesehen habt, haben wir nichts an der Implementierung des InfoController geändert.

Jetzt ist unsere Rest API gegen die Benutzung von Unbefugten gesichert. Wir sollten aber trotzdem noch testen, ob Anfragen, die ein gültiges Token haben, auch ordentlich beantwortet werden von unserem Microservice. Hierfür setzen wir unserer HTTP GET Anfrage für den Bearer Authorization Header einen gültigen JWT Tokens ein. Jetzt starten wir die Anfrage erneut und sehen, dass die Anfrage diesmal einen HTTP Status Code 200 und einen InfoResponse Objekt zurück gibt.

Das Ergebnis

Wir haben in diesem Tutorial gelernt, wie wir einen AuthenticationServer implementieren können, der uns einen JWT Token zurück gibt, den wir dann für weitere Anfragen gegen unseren Abgesicherten Services nutzen können. Dank Spring Security haben wir unsere MicroServices so abgesichert, dass diese Rest API nur noch mit einem gültigen JWT Token aufgerufen werden kann.

Das Prinzip mit dem JWT Token ist aber in dieser Form nicht wirklich 100% sicher! Solltet ihr nun eure Anfragen mittels HTTP senden und nicht über eine verschlüsselte Verbindung wie HTTPS, dann ist es für einen Angreifer sehr einfach an den JWT Token dran zu kommen. Ihr solltet eure Client Anwendung so implementieren, dass der JWT Token auch sicher abgelegt wird.

Der JWT Token ist wie ein Schlüssel eines Hauses. Ist der Schlüssel mal verloren oder geklaut worden, dann muss man einen neuen Schlüssel anfertigen lassen oder das Schloss auswechseln. Dies gilt auch für unseren JWT Token. Sollte der mal in fremde Hände geraten, dann ist es zwingend erforderlich, dass der secret (application.yml) bei beiden Microservices ausgetauscht wird.

Eine andere Möglichkeit ist, das ExpireDate des JWT Token so gering wie möglich zu halten. Dies würde dafür sorgen, das das entwendete JWT Token in einer sehr kurzen Zeit ungültig werden.

Den kompletten Quellcode zu dem Tutorial findet ihr auf meiner GitLab Seite

Ich hoffe euch hat mein Tutorial gefallen. Solltet ihr Fragen, Kritik oder Anregungen haben, dann schreibt ein Kommentar weiter unten.

Verwandte Beiträge

Leave a comment

Diese Website verwendet Cookies. Durch die Nutzung unserer Services erklären Sie sich damit einverstanden, dass wir Cookies setzen. weitere Informationen

Diese Website verwendet Cookies. Durch die Nutzung unserer Services erklären Sie sich damit einverstanden, dass wir Cookies setzen.

Schließen