JSON Web Tokens (JWT)¶
Mithilfe von JSON Web Tokens (JWT) verwalten wir den Zugriff auf Webseiten. Wir werden JWTs hier ganz einfach dafür verwenden, um zu unterscheiden, ob eine Nutzerin bereits eingeloggt ist oder nicht. Zunächst nochmal zur Wiederholung: HTTP ist ein zustandsloses Protokoll. Das bedeutet insbesondere, dass der Webserver vorherige Anfragen desselben Clients nirgendwo speichert und somit auch nicht kennt.
Prinzipiell ist es also gar nicht möglich, sich zunächst mit einer Anfrage beim Webserver einzuloggen und sich bei den folgenden Anfragen dann darauf zu beziehen. Eine Abhilfe dafür wird dadurch erreicht, dass der Webserver bei "Einloggen" ein sicheres Token generiert und dem Nutzer dieses Token übermittelt. Danach schickt der Nutzer dieses Token bei jeder Anfrage an den Webserver (im Header der Anfrage) mit. Der Webserver kann mithilfe dieses Tokens die Nutzerin identifizieren und eine eigene Historie über die Zugriffe anlegen, z.B. in einer Datenbank. Erst dadurch wird es möglich, dass man z.B. einen virtuellen Einkaufskorb befüllt.
Ein JSON Web Token besteht aus drei Teilen:
- einem Header, der den Typ des Tokens enthält (
JWT
) und den Verschlüsselungsalgorithmus, der zur Verschlüsselung der Informationen verwendet wird, z.B.SHA256
. - einem Paload, der die Informationen über die Ntzerin enthält und eventuell weitere Informationen, wie z.B. Rolle der Nutzerin, z.B.
admin
- einer Signatur, die mithilfe des angegebenen Verschlüsselungsalgorithmus die Inforationen des Headers und der Payload verschlüsselt. Mithilfe der Signatur kann überprüft werden, ob ide Informationen im Header oder im Payload geändert wurden oder nicht.
Die drei Teile werden jeweils durch einen Punkt getrennt, d.h. ein JWT hat die Form header.payload.signature
.
Mithilfe des Codierers/De-Codierers von JWT können Sie das Codieren bzw. Decodieren nachvollziehen. Angenommen, Ihr Header ist
{
"alg": "HS256",
"typ": "JWT"
}
, d.h. Sie verwenden SHA256
zur Verschlüsselung und der Typ ist JWT
. Dann ist der erste Teil des JWT
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
Angenommen, Ihr Payload ist
{
"data": [
{
"id": 1,
"username": "test",
"email": "test@test.de",
"password": "098f6bcd4621d373cade4e832627b4f6"
}
],
"iat": 1609061622
}
Die data
haben die gleiche Struktur, wie wir sie später auch verwenden werden. Das Passwort test
ist md5
-verschlüsselt. iat
steht für issued at und gibt die Zeit (Unix-Time) an, zu der das JWT erzeugt wurde (hier 27.12.2020). Dann ist der zweite Teil des JWT
eyJkYXRhIjpbeyJpZCI6MSwidXNlcm5hbWUiOiJ0ZXN0IiwiZW1haWwiOiJ0ZXN0QHRlc3QuZGUiLCJwYXNzd29yZCI6IjA5OGY2YmNkNDYyMWQzNzNjYWRlNGU4MzI2MjdiNGY2In1dLCJpYXQiOjE2MDkwNjE2MjJ9
Wenn Sie als "sichere" Passpharse secret
wählen und damit die Signatur erstellen, dann ist der dritet Teil des JWT
XBWOvX8OceGmx8u77YfsyQtupYYO9p9mUurvIqqwgdk
Sie werden diesen JWT (andere Zeit) später in Ihrer Anwendung wiederfinden. Ausführliche Informationen über JWT finden Sie hier.
Wir werden JWT hier in der einfachsten Form anwenden:
- wir können uns registrieren und einloggen
- das Backend erzeugt beim Registrieren ein JWT und schickt es an den Client
- im Client speichern wir das JWT ab und verwenden es dann beim Zugriff auf die Webseite
Login-Backend¶
Wir erstellen ein Backend mit dem folgenden Endpunkten:
Methode | URL | Bedeutung |
---|---|---|
POST |
/user/register |
Registrierung - fügt einen neuen Datensatz hinzu |
POST |
/user/login |
Login - prüft, ob ein Nutzer in der Datenbank enthalten ist |
Für das Registrieren wird ein JSON in der folgenden Form (Beispieldaten) an das Backend gesendet:
{
"username": "test",
"email": "test@test.de",
"password": "test"
}
Für das Login wird die E-Mail-Adresse weggelassen und nur username
und password
übermittelt:
{
"username": "test",
"password": "test"
}
Existiert der Nutzer test
mit dem Passwort test
in der Datenbank, so wird das folgende JSON vom Backend zum Frontend zurückgesendet:
{
"status": 1,
"data": [
{
"id": 1,
"username": "test",
"email": "test@test.de",
"password": "098f6bcd4621d373cade4e832627b4f6"
}
],
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjpbeyJpZCI6MSwidXNlcm5hbWUiOiJ0ZXN0IiwiZW1haWwiOiJ0ZXN0QHRlc3QuZGUiLCJwYXNzd29yZCI6IjA5OGY2YmNkNDYyMWQzNzNjYWRlNGU4MzI2MjdiNGY2In1dLCJpYXQiOjE2MDkwODIyMTF9.ou22aTCkagg_0hchSdO_dV5AHUDizPqp287dAUOpues"
}
Das Passwort wird nicht als Klarname in der Datenbank abgelegt, sondern verschlüsselt (als Hash) durch das md5
-Modul.
login
-Datenbank¶
Unter localhost/phpmyadmin
(oder auf dem studi.f4
-Server) eine Datenbank erstellen (z.B. login
). Darin eine Tabelle users
anlegen:
--
-- Tabellenstruktur für Tabelle `users`
--
CREATE TABLE `users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(255) NOT NULL,
`email` varchar(255) NOT NULL,
`password` varchar(255) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
--
-- Indizes für die Tabelle `users`
--
ALTER TABLE `users`
ADD PRIMARY KEY (`id`);
Backend erzeugen¶
Wir erzeugen uns ein Backend namens loginbackend
:
mkdir loginbackend
cd loginbackend
npm init
Nun installieren wir gleich alle node
-Module, die wir im Backend benötigen:
npm install express --save
npm install nodemon
npm install cors
npm install mysql
npm install jsonwebtoken
npm install md5
Als erstes erstellen wir eine neue Datei app.js
und geben ein:
1 2 3 4 5 6 7 8 9 10 |
|
In Zeile 2
wird eine controller/index.js
verwendet. Erzeugen Sie einen Ordner controller
und darin eine index.js
:
1 2 3 4 5 |
|
In Zeile 3
wird eine model/user.js
verwendet. Erzeugen Sie einen Ordner model
und darin eine user.js
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
|
Diese Datei ist recht umfangreich. Wir schauen uns die Details gleich näher an. Zunächst passen wir noch die package.json
an, um mithilfe von nodemon
unsere Anwendung zu starten (unter script
):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
Starten Sie die Anwendung mit
npm start
oder, noch besser, konfigurieren Sie das Ausführungskommando in Ihrer IDE, so dass Sie die Anwendung über den entsprechenden Button starten können: .
Wir verwenden dieses Mal nodemon
zum Start der Anwenung. Das hat den Vorteil, dass wir node
nicht jedes Mal aufrufen müssen, um das Backend zu starten. nodemon
ist noch nicht wirklich ausgetestet, funktioniert aber in den meisten Fällen schon recht gut, weshalb wir es hier ausprobieren. Es ist nur für die Entwicklung empfohlen. Das Backend sollte sich jetzt ausführen lassen.
Backend testen¶
Wir testen das Backend mithilfe von Postman. Starten Sie das Backend und öffnen Sie Postman. Geben Sie als POST
-Anfrage http://localhost:4000/user/register
ein und senden Sie im body
(als JSON
) die Daten
{
"username": "test",
"email": "test@test.de",
"password": "test"
}
mit:
Der User test
wird mit den entsprechenden Informationen in der Datenbank abgelegt. Das Passwort ist dabei verschlüsselt. Die Response des Servers ist etwas in der Art:
{
"status": 1,
"data": {
"fieldCount": 0,
"affectedRows": 1,
"insertId": 1,
"serverStatus": 2,
"warningCount": 0,
"message": "",
"protocol41": true,
"changedRows": 0
},
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjpbeyJpZCI6MSwidXNlcm5hbWUiOiJ0ZXN0IiwiZW1haWwiOiJ0ZXN0QHRlc3QuZGUiLCJwYXNzd29yZCI6IjA5OGY2YmNkNDYyMWQzNzNjYWRlNGU4MzI2MjdiNGY2In1dLCJpYXQiOjE2MDkwNjE2MjJ9.XBWOvX8OceGmx8u77YfsyQtupYYO9p9mUurvIqqwgdk"
}
In der Datenbank sehen Sie den Eintrag:
Nach dem Registrieren können Sie auch das EInloggen testen. Geben Sie als POST
-Anfrage http://localhost:4000/user/login
ein und senden Sie im body
(als JSON
) die Daten
{
"username": "test",
"password": "test"
}
mit:
Die Response hat die folgende Form:
{
"status": 1,
"data": [
{
"id": 1,
"username": "test",
"email": "test@test.de",
"password": "098f6bcd4621d373cade4e832627b4f6"
}
],
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjpbeyJpZCI6MSwidXNlcm5hbWUiOiJ0ZXN0IiwiZW1haWwiOiJ0ZXN0QHRlc3QuZGUiLCJwYXNzd29yZCI6IjA5OGY2YmNkNDYyMWQzNzNjYWRlNGU4MzI2MjdiNGY2In1dLCJpYXQiOjE2MDkwNjE2MjJ9.XBWOvX8OceGmx8u77YfsyQtupYYO9p9mUurvIqqwgdk"
}
Das Backend erzwugt den JWT bei jedem Login neu. Es gibt also ein Session-JWT. Eine andere Möglichkeit wäre, das JWT beim Registrieren zu erzeugen und eebenfalls in der Datenbank abzuspeichern und es dann immer mitzuschicken.
Wir schauen uns nun nochmal die user.js
genauer an:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
|
In den Zeilen 6-11
definieren wir den Zugang zur Datenbank. Achten Sie hier auf das richtige Passwort und den korrekten Namen der Datenbank. In den zeilen 12-35
wird die Registrierungs-Funktion definiert und in den zeilen 37-55
die Login-Funktion.
In der Registrierungsfunktion wird zunächst überprüft, ob der username
bereits in der Datenbank existiert (Zeilen 16-17
). Wenn nicht, wird der Nutzer in der Datenbank angelegt (Zeilen 19-20
) und das JWT wird erzeugt (Zeile 26
). Die Daten und das JWT werden vom Backend zusammen mit dem status: 1
als Response zurückgeschickt, wenn alles fehlerlos geklappt hat. Beachten Sie, dass beim Erstellen des JWT als "sichere" Passphrase das Wort secret
verwendet wird (Zeile 26
). Das Pasowrt wird mittels md5
gehasht - es wird also verschlüsselt abgelegt (Zeile 15
).
Die Login-Funktion ist der Registrierungs-Funktion sehr ähnlich. Es wird der passende username
in der Datenbank gesucht und das gehashte Passowrt verglichen. Auch hier wird ein JWT erzeugt.
Login-Frontend¶
Für das Frontend erstellen wir uns ein Formular zur Registrierung und ein Formular für das Login. Das JWT, das wir vom Webserver erhalten, speichern wir lokal ab. Dazu verwenden wir localStorage
. Wir nennen unser Frontend loginfrontend
:
ng new loginfrontend
Wir erzeugen uns zwei Komponenten, eine für die Registrierung und die andere für das Login:
cd loginfrontend
ng g c login
ng g c register
Wir binden beide Komponenten über Routing ein. In der app-routing.module.ts
definieren wir dazu:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
Damit die Formulare schicker aussehen, binden wir Bootstrap ein:
npm install --save bootstrap
npm install --save jquery
npm install --save popper.js
In der angular.json
Bootstrap hinzufügen:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Die Zeilen müssen zwei Mal eingefügt werden. Einmal unter "build"
und einmal unter "test"
. Achten Sie darauf, zunächst jQuery
einzubinden und erst dann bootstrap.js
.
Services im Frontend¶
Wir erstellen uns zwei Services. Der eine Service api
ist für die Kommunikation mit dem Backend zuständig. Der andere Service auth
legt den Token im localStorage
ab und holt ihn auch von dort.
ng g service services/auth
ng g service services/api
In der services/api.service.ts
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
In der services/auth.service.ts
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
Mit der getUserDetails()
-Funktion laden wir die userData
aus dem localStorage
, mit der setDataInLocalStorage()
-Funktion speichern wir die userData
im localStorage
und clearStorage()
löscht den gesamten localStorage
.
Registrierungs-Komponente¶
In der Registrierungskomponente erstellen wir das Formular zur Registrierung und rufen mit den eingebgebenen Daten das Backend zur Registrierung auf.
In der register.component.ts
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
|
Im Konstruktor (Zeilen 18-23
) werden die beiden Services, der FormBuilder
und das Router
-Modul per dependency injection eingebunden. In der ngOnInit()
-Funktion (Zeilen 28-35
) definieren wir das Formular bestehend aus den FormControls
username
, email
und password
. Das gesamte Formular ist in der Eigenschaft registerForm
gespeichert (Zeile 15
). Für den Zugriff auf die einzelnen FormControls
defineiren wir noch einen Getter f()
, der uns Schreibarbeit in der register.component.html
erspart (f()
gibt uns ein Objekt der drei FormControls
des Formulars zurück).
Die Funktion onSubmit()
wird aufgerufen, wenn das Formular abgeschickt wird. In Zeile 39
geben wir die Daten des Formulars auf die Konsole aus (nur für Debug-Zwecke). In den Zeilen 40-52
rufen wir die Funktion postTypeRequest
aus unserem api
-Service auf. Darin übergeben wir alle Daten aus dem Formular. Wenn alles klappt (status
der Response ist 1
), dann speichern wir alle Daten lokal unter userData
(Zeile 43
) und das JWT nochmal extra unter token
(Zeile 44
). Danach rufen wir die Route login
auf, wechseln also zur Login-Komponente.
Das Template der Registrierungs-Komponente sieht wie folgt aus:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
|
Die Direktive *ngIf = '!this.isLogin'
in Zeile 2
prüft den Status der Eigenschaft isLogin
. Diese ist am Angang false
(siehe register.component.ts
in Zeile 13
). Durch die isUserLogin()
-Funktion der register.component.ts
(Zeilen 55-59
) kann der Wert auf true
gesetzt werden, wenn der auth
-Service userData
gespeichert hat. Für den Fall. dass isLogin
den Wert true
hat, zeigt die Registrierungs-Komponente You are logged in
an (Zeilen 26-28
). Wenn nicht, erscheint das Registrierungsformular (Zeilen 2-25
). Beim Absenden des Formulars wird die onSubmit()
-Funktion aus register.component.ts
aufgerufen. Für die einzelnen Eingabefelder wird ein Atrributbinding definiert: Falls das Formular abgeschickt wurde (submitted
) und aber Fehler existieren (z.B. f.username.errors
), wird dem entsprechenden Eingabefeld die Bootstrap-Klasse is-invalid
zugeordnet und es erscheint die darunter definierte Fehlerausgabe. Hier findet auch die f()
-Funktion ihre Anwendung.
Login-Komponente¶
Die Login-Komponente hat starke Ähnlichkeit mit der Registrierungs-Komponente. Das Formular enthält jedoch keinen Eintrag für die E-Mail und es wird eine ander URL im Backend aufgerufen user/login
anstelle von user/rgister
. Außerdem stellt die Login-Komponente noch einen Button Log-out
zur Verfügung, der ein Ausloggen ermöglicht (logout()
-Funktion). Das Ausloggen wird hier dadurch simuliert, dass der localStorage
gelöscht wird. Ansonsten sehen sich swohl die ts
- als auch die html
-Datei sehr ähnlich:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
Die app.component.html
können Sie sich gestalten, wie Sie möchten. Eine Möglichkeit wäre so:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
Die entsprechenden Seiten sehen dann so aus:
Ausblick¶
Beachten Sie, dass das Beispiel bewusst einfach gehalten wurde. Es fehlen mehrere Sachen:
- wir reagieren im Frontend gar nicht auf Fehler des Backends, d.h. es fehlen z.B.
- Reaktionen beim Registrieren, falls der
username
bereits existiert, - Reaktionen beim Login, falls das Passwort nicht korrekt ist
- Reaktionen beim Registrieren, falls der
- wir haben keine Zugriffssteuerung definiert, d.h. es fehlt noch eine Seite (Komponente), auf die nur dann zugegriffen werden kann, wenn der Nutzer eingeloggt ist
Die Fehlerpunkte wären sicherlich eine gute Übung für Sie. Den letzten Punkt betrachten wir beim nächsten Mal. Allerdings kann man JWT auch noch so erweitern, dass bestimmte Rollen hinterlegt und abgefraget werden können. Das werden wir uns nicht anschauen. Aber vielleicht haben Sie ja bereits selbst Ideen, wie das geschehen kann. Die Nutzerdaten müssten ja nur im ein Attribut role
erweitert und abgefragt werden. Umfassende Informationen darüber finden Sie im JWT-Handbuch.
Dowmloads der aktuellen Versionen¶
Clonen von Github:
im Folgenden wird auf diesen Versionen aufgebaut.
Zugriff nur bei Login¶
UserService
ng g s services/user
UserComponent
ng g c user