SSO mit Spring Boot 2.0

Während unserer Arbeit mit einer Microservice-Architektur und Spring Boot taucht immer wieder ein Problem auf:
Jeder Nutzer sollte sich gegen alle unsere Schnittstellen authentifizieren, aber man möchte nicht für jede Schnittstelle einen eigenen Login schreiben und verwalten. Die Lösung ist ein so genannter Single Sign On (SSO). In diesem Blogartikel wird beschrieben, wie man einen SSO mit Spring Boot 2.0 mit einem Authentifizierungs- und einen Ressourcenserver über ein JWT-Token und OAuth2 realisiert.

Gradle-Datei(en)

Die Gradle-Build-Dateien unterscheiden sich für den Authentifizierungs- und den Ressourcenserver nicht. Für beide werden die Spring-Frameworks Spring-Security-OAuth2 und Spring-Security-Jwt benötigt. Daraus resultieren folgende Gradle-Build-Dateien:

Der Authentifizierungsserver

Der Authentifizierungsserver soll in unserem Testprojekt auf Port 9999 laufen. Da der Standardport Port 8080 ist, muss folgender Eintrag in die application.properties-Datei übernommen werden:

Zudem soll in unserem Projekt mit möglichst geringem Aufwand und ohne zusätzliche Abhängigkeiten eine Datenbank simuliert werden. Dies geschieht über eine weitere Properties-Datei, welche für jeden User das Passwort und die entsprechenden Rechte enthält. Die Datei muss im Projekt im Ressourcen-Ordner (neben der application.properties-Datei) angelegt werden und sollte folgenden Inhalt haben:

Die meist erzeugte Klasse, welche die main-Methode und die SpringBootApplication-Annotation enthält, muss von uns nicht angepasst werden.

Nutzerdaten auslesen

Nachdem nun das grundsätzliche Setup für den Authentifizierungsserver fertiggestellt ist, werden als Erstes die Nutzerdaten aus der soeben angelegten Datei ausgelesen. Dies geschieht mit Hilfe des UserService, welcher im Folgenden dargestellt ist:

Konfigurieren der Absicherung

Nun muss noch festgelegt werden, welche Anfragen an den Server selbst zugelassen sind. Dazu wird die Klasse WebSecurityConfig erzeugt. Diese hat grundsätzlich den selben Inhalt wie bei einer normalen abgesicherten Anwendung. Für das Beispielprojekt wurde ein NoOpPasswordEncoder verwendet. Dieser verwendet Passwörter, welche im Klartext hinterlegt sind. Für ein Beispiel ist dies ausreichend, produktiv sollte er allerdings ausgetauscht werden. Die komplette WebSecurityConfig für unser Testprojekt sieht wie folgt aus:

Als vorletzten Schritt zur Fertigstellung des Servers benötigen wir noch die Konfiguration für den Authentifizierungsserver selbst. Diese legt fest, für welche Clients welche Rechte existieren und wie die Token erzeugt werden. Diese Konfiguration heißt OAuth2Configuration und ist im Folgenden abgebildet:

JWT-Token Verwaltung

Um unsere Token zu verwalten, benötigen wir noch einen JWT-Token-Store. Dieser muss erstellt und ebenfalls im Ressourcenordner (neben der application.properties-Datei) abgelegt werden. Zusätzlich dazu wird noch ein Public Key erzeugt, mit dem sich später unsere Schnittstellen gegenüber unserem Authentifizierungsserver authentifizieren und die Token ohne den Server auswerten können. Dazu müssen folgende Befehle nacheinander ausgeführt werden (Ein Hinweis an alle Windows-Nutzer: Falls OpenSSL nicht vorhanden ist, in der Git Bash wird es mitgeliefert):

Danach wird mit dem Keystore-Kennwort „mySecretKey“ bestätigt und es sollte eine ähnliche Ausgabe folgen:

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwR84LFHwnK5GXErnwkmD
mPOJl4CSTtYXCqmCtlbF+5qVOosu0YsM2DsrC9O2gun6wVFKkWYiMoBSjsNMSI3Z
w5JYgh+ldHvA+MIex2QXfOZx920M1fPUiuUPgmnTFS+Z3lmK3/T6jJnmciUPY1pe
h4MXL6YzeI0q4W9xNBBeKT6FDGpduc0FC3OlXHfLbVOThKmAUpAWFDwf9/uUA//l
3PLchmV6VwTcUaaHp5W8Af/GU4lPGZbTAqOxzB9ukisPFuO1DikacPhrOQgdxtqk
LciRTa884uQnkFwSguOEUYf3ni8GNRJauIuW0rVXhMOs78pKvCKmo53M0tqeC6ul
+QIDAQAB
-----END PUBLIC KEY-----
-----BEGIN CERTIFICATE-----
MIIDGTCCAgGgAwIBAgIEOkszIDANBgkqhkiG9w0BAQsFADA9MQswCQYDVQQGEwJE
RTEPMA0GA1UECBMGQmVybGluMQ8wDQYDVQQHEwZCZXJsaW4xDDAKBgNVBAMTA2p3
dDAeFw0xNjAyMDExNzQwMTlaFw0xNjA1MDExNzQwMTlaMD0xCzAJBgNVBAYTAkRF
MQ8wDQYDVQQIEwZCZXJsaW4xDzANBgNVBAcTBkJlcmxpbjEMMAoGA1UEAxMDand0
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwR84LFHwnK5GXErnwkmD
mPOJl4CSTtYXCqmCtlbF+5qVOosu0YsM2DsrC9O2gun6wVFKkWYiMoBSjsNMSI3Z
w5JYgh+ldHvA+MIex2QXfOZx920M1fPUiuUPgmnTFS+Z3lmK3/T6jJnmciUPY1pe
h4MXL6YzeI0q4W9xNBBeKT6FDGpduc0FC3OlXHfLbVOThKmAUpAWFDwf9/uUA//l
3PLchmV6VwTcUaaHp5W8Af/GU4lPGZbTAqOxzB9ukisPFuO1DikacPhrOQgdxtqk
LciRTa884uQnkFwSguOEUYf3ni8GNRJauIuW0rVXhMOs78pKvCKmo53M0tqeC6ul
+QIDAQABoyEwHzAdBgNVHQ4EFgQUxebRVICNPg65T2RgrOe2J5qDMXMwDQYJKoZI
hvcNAQELBQADggEBAAHBF3JQ2ZsEjuYYwU5cp9BzBJoTQyChF37AA76EorrcSeqo
Rui1dUIfXbImIOZ5PBNk34IFWROTwpw80zBCZQ7NQ81ITzuhsxjbX7Wxj6iCq3u9
TDN+IxiaZMvJ2PDfeRqr93HOwMTMttxyW4KVa3geQ+yMMSZrxagEpqMA1Fviqa6T
5u8DNqfXQ8Hg+yG2bMNQs6GleAFkRprkHjR6yY7ehmIVMZ7iBkkXh8IO8fKy2WNK
uWa+DO2lXJj1W7HLXeaeT0twAqwyoNj2/pxMuv/JrTlNkhcUTmP+UBAJZih0KSGD
9TSKs5HlBGsIUpILuauNzZk1VS2RCyVtD1zf7vM=
-----END CERTIFICATE-----

Die erzeugte Datei jwt.jks ist der Keystore und muss in den Ressourcenordner verschoben werden. Der Teil der Ausgabe, welcher den Public Key darstellt, wird kopiert und zusammen mit der Anfangs- und Endzeile in eine neue Datei public.cert ausgelagert. Diese Datei muss in den Ressourcenordner des Ressourcenservers verschoben werden.

Der Ressourcenserver

Auch der Ressourcenserver erhält, wie der Authentifizierungsserver, seinen eigenen Port. Diesmal den Port 9090. Dazu muss wieder folgende Zeile in die application.properties übernommen werden.

Auch hier muss die selbst erzeugte Klasse mit der main-Methode zum Starten des Projekts nicht verändert werden.

Setzen der JWT-Token

Nachdem der Public Key des Authentifizierungsservers hinterlegt wurde, muss dieser ausgewertet werden. Dies geschieht mit der der Klasse JwtConfiguration. Diese enthält folgenden Quellcode, welcher den Schlüssel ausliest und für diese Anwendung setzt:

Konfiguration des Ressourcenservers

Nun folgt die eigentliche Konfiguration des Ressourcenservers. Es wird wieder festgelegt, welche Anfragen abgesichert werden, welche nicht und wie das Token weiter verwendet wird. Zudem kann schon hier festgelegt werden, welche Anfragen welche Rechte benötigen. Eine Konfiguration über Annotationen direkt im Contoller ist aber praktikabler. Der Quellcode der Klasse sieht dann folgendermaßen aus:

Um auch die Annotationen der Frameworks voll auszunutzen, wird die Klasse GlobalMethodSecurityConfiguration benötigt. Sie aktiviert eben jene Annotationen und hat folgenden Inhalt:

Als letztes müssen dem Ressourcenserver Schnittstellen mit auf den Weg gegeben werden. Dazu wird ein entsprechender Controller erstellt. In diesem Controller gibt es nichts weiter Außergewöhnliches, nur die Annotation @PreAuthorize ist neu. Diese Annotation legt fest, dass nur Nutzer mit bestimmten Rechten diese Methode ausführen und damit den Endpunkt ansprechen können. Weitere Annotationen, wie z.B. @PostAuthorize, und deren Verwendung können der Dokumentation des Frameworks entnommen werden. Unser Controller hat zwei Endpunkte mit folgendem Inhalt:

Ablauf der Authentifizierung und Testen der Implementierung

Um sich zu authentifizieren, muss sich jeder Nutzer zunächst bei unserem Authentifizierungsserver anmelden. Dort erhält er ein (oder mehrere) Token. Diese kann er danach für alle Requests nutzen.

Der Request auf den Authentifizierungsserver ist ein POST-Request und stellt sich für den Nutzer „reader“ folgendermaßen dar:

  • POST auf sfl_app:@localhost:9999/oauth/token mit folgenden form-data-Body-Elementen: grant_type=password , username=reader , password=reader

In der Antwort ist unter anderem ein „access_token“ enthalten. Dieses kann für weitere Authentifizierungen genutzt werden. So sieht der Zugriff auf den Endpunkt „api/read“ des Ressourcenservers folgendermaßen aus:

  • GET auf localhost:9090/api/read mit folgendem Header: Key = Authorization ; Value = Bearer + access_token

Wichtig ist hier, dass der Nutzer Reader nur auf die Ressource „api/read“ des Ressourcenservers zugreifen kann. Der Nutzer „writer“ hingegen, kann beide Endpunkte ( „api/read“ und „api/write“) nutzen.

Fertig!

Nach der Durchführung aller genannten Schritte sollte nun eine funktionierende, einfache Microservice-Architektur mit einem Authentifizierungs- und einem Ressourcenserver vorliegen. Diese kann noch beliebig um weitere Ressourcenserver erweitert werden. Zudem kann für die Verwaltung der Nutzer jegliche Datenbank über den UserService angebunden werden. Auch die Erzeugung der Token kann beliebig verändert werden. Zu beachten ist nur, dass für die Auswertung der JWT-Token keine Verbindung zum Authentifizierungsserver vorliegen muss. Dies kann bei anderen Vorgehensweisen anders sein.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.