Come eseguire l'autenticazione JWT con una SPA Angular 6
Pubblicato: 2022-03-11Oggi daremo un'occhiata a quanto sia facile integrare l'autenticazione del token web JSON (JWT) nella tua applicazione a pagina singola (SPA) Angular 6 (o successiva). Cominciamo con un po' di background.
Cosa sono i token Web JSON e perché usarli?
La risposta più semplice e concisa qui è che sono convenienti, compatti e sicuri. Diamo un'occhiata a queste affermazioni in dettaglio:
- Comodo : l'utilizzo di un JWT per l'autenticazione al back-end una volta effettuato l'accesso richiede l'impostazione di un'intestazione HTTP, un'attività che può essere facilmente automatizzata tramite una funzione o una sottoclasse, come vedremo in seguito.
- Compatto : un token è semplicemente una stringa con codifica base64, contenente alcuni campi di intestazione e un payload se necessario. Il JWT totale è solitamente inferiore a 200 byte, anche se firmato.
- Sicuro : sebbene non sia necessario, un'ottima funzionalità di sicurezza di JWT è che i token possono essere firmati utilizzando la crittografia RSA a coppie di chiavi pubbliche/private o la crittografia HMAC utilizzando un segreto condiviso. Ciò garantisce l'origine e la validità di un token.
Tutto ciò si riduce a un modo sicuro ed efficiente per autenticare gli utenti e quindi verificare le chiamate agli endpoint API senza dover analizzare le strutture dati né implementare la propria crittografia.
Teoria dell'applicazione
Quindi, con un po' di background, ora possiamo approfondire come funzionerebbe in un'applicazione reale. Per questo esempio, presumo che abbiamo un server Node.js che ospita la nostra API e stiamo sviluppando un elenco di cose da fare SPA utilizzando Angular 6. Lavoriamo anche con questa struttura API:
-
/auth
→POST
(inserire nome utente e password per autenticarsi e ricevere indietro un JWT) -
/todos
→GET
(restituisce un elenco di voci dell'elenco di cose da fare per l'utente) -
/todos/{id}
→GET
(restituisce un elemento specifico dell'elenco di cose da fare) -
/users
→GET
(restituisce un elenco di utenti)
A breve esamineremo la creazione di questa semplice applicazione, ma per ora concentriamoci sull'interazione in teoria. Abbiamo una semplice pagina di accesso, in cui l'utente può inserire il proprio nome utente e password. Quando il modulo viene inviato, invia tali informazioni all'endpoint /auth
. Il server Node può quindi autenticare l'utente in qualsiasi modo sia appropriato (ricerca nel database, query su un altro servizio Web, ecc.), ma alla fine l'endpoint deve restituire un JWT.
Il JWT per questo esempio conterrà alcune attestazioni riservate e alcune attestazioni private . Le attestazioni riservate sono semplicemente coppie chiave-valore consigliate da JWT comunemente utilizzate per l'autenticazione, mentre le attestazioni private sono coppie chiave-valore applicabili solo alla nostra app:
Reclami riservati
-
iss
: Emittente di questo token. In genere l'FQDN del server, ma può essere qualsiasi cosa purché l'applicazione client sappia che se lo aspetta. -
exp
: data e ora di scadenza di questo token. Questo è in secondi dalla mezzanotte del 01 gennaio 1970 GMT (ora Unix). -
nbf
: non valido prima del timestamp. Non viene utilizzato spesso, ma fornisce un limite inferiore per la finestra di validità. Stesso formatoexp
.
Reclami privati
-
uid
: ID utente dell'utente connesso. -
role
: ruolo assegnato all'utente connesso.
Le nostre informazioni saranno codificate in base64 e firmate utilizzando HMAC con la chiave condivisa todo-app-super-shared-secret
. Di seguito è riportato un esempio di come appare il JWT:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ0b2RvYXBpIiwibmJmIjoxNDk4MTE3NjQyLCJleHAiOjE0OTgxMjEyNDIsInVpZCI6MSwicm9sZSI6ImFkbWluIn0.ZDz_1vcIlnZz64nSM28yA1s-4c_iw3Z2ZtP-SgcYRPQ
Questa stringa è tutto ciò di cui abbiamo bisogno per assicurarci di avere un login valido, per sapere quale utente è connesso e anche per sapere quale(i) ruolo(i) ha l'utente.
La maggior parte delle librerie e delle applicazioni procede all'archiviazione di questo JWT in localStorage
o sessionStorage
per un facile recupero, ma questa è solo una pratica comune. Quello che fai con il token dipende da te, purché tu possa fornirlo per future chiamate API.
Ora, ogni volta che la SPA desidera effettuare una chiamata a uno qualsiasi degli endpoint API protetti, deve semplicemente inviare il token nell'intestazione HTTP di Authorization
.
Authorization: Bearer {JWT Token}
Nota : ancora una volta, questa è semplicemente una pratica comune. JWT non prescrive alcun metodo particolare per l'invio al server. Puoi anche aggiungerlo all'URL o inviarlo in un cookie.
Una volta che il server riceve il JWT, può decodificarlo, garantire la coerenza utilizzando il segreto condiviso HMAC e verificare la scadenza utilizzando i campi exp
e nbf
. Potrebbe anche utilizzare il campo iss
per assicurarsi di essere la parte emittente originale di questo JWT.
Una volta che il server è soddisfatto della validità del token, è possibile utilizzare le informazioni memorizzate all'interno del JWT. Ad esempio, l' uid
che abbiamo incluso ci fornisce l'ID dell'utente che effettua la richiesta. Per questo particolare esempio, abbiamo incluso anche il campo del role
, che ci consente di prendere decisioni sul fatto che l'utente debba essere in grado di accedere a un determinato endpoint o meno. (Se ti fidi di queste informazioni o se vuoi eseguire una ricerca nel database dipende dal livello di sicurezza richiesto.)
function getTodos(jwtString) { var token = JWTDecode(jwtstring); if( Date.now() < token.nbf*1000) { throw new Error('Token not yet valid'); } if( Date.now() > token.exp*1000) { throw new Error('Token has expired'); } if( token.iss != 'todoapi') { throw new Error('Token not issued here'); } var userID = token.uid; var todos = loadUserTodosFromDB(userID); return JSON.stringify(todos); }
Costruiamo una semplice app Todo
Per seguire, dovrai avere una versione recente di Node.js (6.x o successivo), npm (3.x o successivo) e angular-cli installato. Se devi installare Node.js, che include npm, segui le istruzioni qui. Successivamente angular angular-cli
può essere installato usando npm
(o yarn
, se lo hai installato):
# installation using npm npm install -g @angular/cli # installation using yarn yarn global add @angular/cli
Non entrerò nei dettagli sul boilerplate di Angular 6 che useremo qui, ma per il passaggio successivo ho creato un repository Github per contenere una piccola applicazione da fare per illustrare la semplicità dell'aggiunta dell'autenticazione JWT alla tua app. Basta clonarlo usando quanto segue:
git clone https://github.com/sschocke/angular-jwt-todo.git cd angular-jwt-todo git checkout pre-jwt
Il comando git checkout pre-jwt
passa a una versione denominata in cui JWT non è stato implementato.
Dovrebbero esserci due cartelle all'interno chiamate server
e client
. Il server è un server API Node che ospiterà la nostra API di base. Il cliente è la nostra app Angular 6.
Il server API del nodo
Per iniziare, installa le dipendenze e avvia il server API.
cd server # installation using npm npm install # or installation using yarn yarn node app.js
Dovresti essere in grado di seguire questi collegamenti e ottenere una rappresentazione JSON dei dati. Solo per ora, fino a quando non avremo l'autenticazione, abbiamo codificato l'endpoint /todos
per restituire le attività per userID=1
:
- http://localhost:4000: pagina di test per verificare se il server Node è in esecuzione
- http://localhost:4000/api/users: restituisce l'elenco degli utenti sul sistema
- http://localhost:4000/api/todos: restituisce l'elenco delle attività per
userID=1
L'app angolare
Per iniziare con l'app client, dobbiamo anche installare le dipendenze e avviare il server di sviluppo.
cd client # using npm npm install npm start # using yarn yarn yarn start
Nota : a seconda della velocità della tua linea, il download di tutte le dipendenze può richiedere del tempo.
Se tutto sta andando bene, ora dovresti vedere qualcosa di simile quando navighi su http://localhost:4200:
Aggiunta dell'autenticazione tramite JWT
Per aggiungere il supporto per l'autenticazione JWT, utilizzeremo alcune librerie standard disponibili che lo rendono più semplice. Ovviamente puoi rinunciare a queste comodità e implementare tutto da solo, ma questo va oltre il nostro scopo qui.
Innanzitutto, installiamo una libreria sul lato client. È sviluppato e gestito da Auth0, una libreria che consente di aggiungere l'autenticazione basata su cloud a un sito Web. L'utilizzo della libreria stessa non richiede l'utilizzo dei loro servizi.
cd client # installation using npm npm install @auth0/angular-jwt # installation using yarn yarn add @auth0/angular-jwt
Arriveremo al codice in un secondo, ma già che ci siamo, configuriamo anche il lato server. Utilizzeremo le librerie body-parser
, jsonwebtoken
ed express-jwt
per fare in modo che Node comprenda i corpi JSON POST e i JWT.
cd server # installation using npm npm install body-parser jsonwebtoken express-jwt # installation using yarn yarn add body-parser jsonwebtoken express-jwt
Endpoint API per l'autenticazione
Innanzitutto, abbiamo bisogno di un modo per autenticare gli utenti prima di fornire loro un token. Per la nostra semplice demo, configureremo semplicemente un endpoint di autenticazione fisso con un nome utente e una password hardcoded. Questo può essere semplice o complesso come richiesto dalla tua applicazione. L'importante è rispedire un JWT.
In server/app.js
aggiungi una voce sotto le altre righe di require
come segue:
const bodyParser = require('body-parser'); const jwt = require('jsonwebtoken'); const expressJwt = require('express-jwt');
Oltre a quanto segue:
app.use(bodyParser.json()); app.post('/api/auth', function(req, res) { const body = req.body; const user = USERS.find(user => user.username == body.username); if(!user || body.password != 'todo') return res.sendStatus(401); var token = jwt.sign({userID: user.id}, 'todo-app-super-shared-secret', {expiresIn: '2h'}); res.send({token}); });
Questo è principalmente codice JavaScript di base. Otteniamo il corpo JSON che è stato passato all'endpoint /auth
, troviamo un utente che corrisponde a quel nome utente, controlliamo di avere un utente e la password corrispondano e, in caso contrario, restituiamo un errore HTTP 401 Unauthorized
.
La parte importante è la generazione di token e la suddivideremo in base ai suoi tre parametri. La sintassi per sign
è la seguente: jwt.sign(payload, secretOrPrivateKey, [options, callback])
, dove:
-
payload
è un oggetto letterale di coppie chiave-valore che vorresti codificare all'interno del tuo token. Queste informazioni possono quindi essere decodificate dal token da chiunque abbia la chiave di decrittazione. Nel nostro esempio, codifichiamouser.id
in modo che quando riceviamo nuovamente il token sul back-end per l'autenticazione, sappiamo con quale utente abbiamo a che fare. -
secretOrPrivateKey
è una chiave segreta condivisa con crittografia HMAC (questo è ciò che abbiamo utilizzato nella nostra app, per semplicità) o una chiave privata con crittografia RSA/ECDSA. -
options
rappresenta una varietà di opzioni che possono essere passate al codificatore sotto forma di coppie chiave-valore. In genere, specifichiamo almenoexpiresIn
(diventa attestazione riservataexp
) edissuer
( attestazione riservataiss
) in modo che un token non sia valido per sempre e il server può verificare di aver effettivamente emesso il token originariamente. -
callback
è una funzione da chiamare dopo che la codifica è stata completata, se si desidera gestire la codifica del token in modo asincrono.
(Puoi anche leggere maggiori dettagli sulle options
e su come utilizzare la crittografia a chiave pubblica invece di una chiave segreta condivisa.)
Integrazione JWT angolare 6
Per far funzionare Angular 6 con il nostro JWT è abbastanza semplice usare angular-jwt
. Aggiungi semplicemente quanto segue a client/src/app/app.modules.ts
:
import { JwtModule } from '@auth0/angular-jwt'; // ... export function tokenGetter() { return localStorage.getItem('access_token'); } @NgModule({ // ... imports: [ BrowserModule, AppRoutingModule, HttpClientModule, FormsModule, // Add this import here JwtModule.forRoot({ config: { tokenGetter: tokenGetter, whitelistedDomains: ['localhost:4000'], blacklistedRoutes: ['localhost:4000/api/auth'] } }) ], // ... }
Questo è fondamentalmente tutto ciò che è necessario. Ovviamente abbiamo altro codice da aggiungere per eseguire l'autenticazione iniziale, ma la libreria angular-jwt
si occupa di inviare il token insieme ad ogni richiesta HTTP.

- La funzione
tokenGetter()
fa esattamente quello che dice, ma come viene implementata dipende interamente da te. Abbiamo scelto di restituire il token che salviamo inlocalStorage
. Ovviamente sei libero di fornire qualsiasi altro metodo che desideri, purché restituisca la stringa codificata del token web JSON . - L'opzione
whiteListedDomains
esiste in modo da poter limitare i domini a cui viene inviato il JWT, in modo che anche le API pubbliche non ricevano il tuo JWT. - L'opzione
blackListedRoutes
consente di specificare percorsi specifici che non dovrebbero ricevere il JWT anche se si trovano in un dominio autorizzato. Ad esempio, l'endpoint di autenticazione non ha bisogno di riceverlo perché non ha senso: il token è in genere nullo quando viene chiamato comunque.
Far funzionare tutto insieme
A questo punto, abbiamo un modo per generare un JWT per un determinato utente utilizzando l'endpoint /auth
sulla nostra API e abbiamo eseguito l'impianto idraulico su Angular per inviare un JWT con ogni richiesta HTTP. Ottimo, ma potresti sottolineare che non è cambiato assolutamente nulla per l'utente. E avresti ragione. Possiamo ancora navigare in ogni pagina della nostra app e possiamo chiamare qualsiasi endpoint API senza nemmeno inviare un JWT. Non bene!
Dobbiamo aggiornare la nostra app client per preoccuparci di chi ha effettuato l'accesso e anche aggiornare la nostra API per richiedere un JWT. Iniziamo.
Avremo bisogno di un nuovo componente Angular per l'accesso. Per brevità, lo manterrò il più semplice possibile. Avremo anche bisogno di un servizio che gestirà tutti i nostri requisiti di autenticazione e di una protezione angolare per proteggere i percorsi che non dovrebbero essere accessibili prima dell'accesso. Faremo quanto segue nel contesto dell'applicazione client.
cd client ng g component login --spec=false --inline-style ng g service auth --flat --spec=false ng g guard auth --flat --spec=false
Questo dovrebbe aver generato quattro nuovi file nella cartella client
:
src/app/login/login.component.html src/app/login/login.component.ts src/app/auth.service.ts src/app/auth.guard.ts
Successivamente, dobbiamo fornire il servizio di autenticazione e proteggere la nostra app. Aggiorna client/src/app/app.modules.ts
:
import { AuthService } from './auth.service'; import { AuthGuard } from './auth.guard'; // ... providers: [ TodoService, UserService, AuthService, AuthGuard ],
E quindi aggiorna il routing in client/src/app/app-routing.modules.ts
per utilizzare la protezione dell'autenticazione e fornire un percorso per il componente di accesso.
// ... import { LoginComponent } from './login/login.component'; import { AuthGuard } from './auth.guard'; const routes: Routes = [ { path: 'todos', component: TodoListComponent, canActivate: [AuthGuard] }, { path: 'users', component: UserListComponent, canActivate: [AuthGuard] }, { path: 'login', component: LoginComponent}, // ...
Infine, aggiorna client/src/app/auth.guard.ts
con i seguenti contenuti:
import { Injectable } from '@angular/core'; import { Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; @Injectable() export class AuthGuard implements CanActivate { constructor(private router: Router) { } canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot) { if (localStorage.getItem('access_token')) { return true; } this.router.navigate(['login']); return false; } }
Per la nostra applicazione demo, stiamo semplicemente verificando l'esistenza di un JWT nella memoria locale. Nelle applicazioni del mondo reale, decodificheresti il token e ne verificherai la validità, la scadenza, ecc. Ad esempio, potresti utilizzare JwtHelperService per questo.
A questo punto, la nostra app Angular ora ti reindirizzerà sempre alla pagina di accesso poiché non abbiamo modo di accedere. Rettiviamolo, iniziando con il servizio di autenticazione in client/src/app/auth.service.ts
:
import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; @Injectable() export class AuthService { constructor(private http: HttpClient) { } login(username: string, password: string): Observable<boolean> { return this.http.post<{token: string}>('/api/auth', {username: username, password: password}) .pipe( map(result => { localStorage.setItem('access_token', result.token); return true; }) ); } logout() { localStorage.removeItem('access_token'); } public get loggedIn(): boolean { return (localStorage.getItem('access_token') !== null); } }
Il nostro servizio di autenticazione ha solo due funzioni, login
e logout
:
-
login
POST
s ilusername
e lapassword
forniti al nostro back-end e impostaaccess_token
inlocalStorage
se ne riceve uno indietro. Per motivi di semplicità, non c'è alcuna gestione degli errori qui. -
logout
cancella semplicementeaccess_token
dalocalStorage
, richiedendo l'acquisizione di un nuovo token prima che sia possibile accedere nuovamente a qualsiasi altra cosa. -
loggedIn
è una proprietà booleana che possiamo utilizzare rapidamente per determinare se l'utente ha effettuato l'accesso o meno.
E infine, il componente di accesso. Questi non hanno alcuna relazione con il lavoro effettivo con JWT, quindi sentiti libero di copiare e incollare in client/src/app/login/login.components.html
:
<h4 *ngIf="error">{{error}}</h4> <form (ngSubmit)="submit()"> <div class="form-group col-3"> <label for="username">Username</label> <input type="text" name="username" class="form-control" [(ngModel)]="username" /> </div> <div class="form-group col-3"> <label for="password">Password</label> <input type="password" name="password" class="form-control" [(ngModel)]="password" /> </div> <div class="form-group col-3"> <button class="btn btn-primary" type="submit">Login</button> </div> </form>
E client/src/app/login/login.components.ts
avrà bisogno di:
import { Component, OnInit } from '@angular/core'; import { AuthService } from '../auth.service'; import { Router } from '@angular/router'; import { first } from 'rxjs/operators'; @Component({ selector: 'app-login', templateUrl: './login.component.html' }) export class LoginComponent { public username: string; public password: string; public error: string; constructor(private auth: AuthService, private router: Router) { } public submit() { this.auth.login(this.username, this.password) .pipe(first()) .subscribe( result => this.router.navigate(['todos']), err => this.error = 'Could not authenticate' ); } }
Voilà, il nostro esempio di accesso ad Angular 6:
A questo punto, dovremmo essere in grado di accedere (usando jemma
, paul
o sebastian
con la password todo
) e vedere di nuovo tutte le schermate. Ma la nostra applicazione mostra le stesse intestazioni di navigazione e nessun modo per disconnettersi indipendentemente dallo stato corrente. Risolviamolo prima di passare alla correzione della nostra API.
In client/src/app/app.component.ts
, sostituisci l'intero file con il seguente:
import { Component } from '@angular/core'; import { Router } from '@angular/router'; import { AuthService } from './auth.service'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent { constructor(private auth: AuthService, private router: Router) { } logout() { this.auth.logout(); this.router.navigate(['login']); } }
E per client/src/app/app.component.html
sostituisci la sezione <nav>
con la seguente:
<nav class="nav nav-pills"> <a class="nav-link" routerLink="todos" routerLinkActive="active" *ngIf="auth.loggedIn">Todo List</a> <a class="nav-link" routerLink="users" routerLinkActive="active" *ngIf="auth.loggedIn">Users</a> <a class="nav-link" routerLink="login" routerLinkActive="active" *ngIf="!auth.loggedIn">Login</a> <a class="nav-link" (click)="logout()" href="#" *ngIf="auth.loggedIn">Logout</a> </nav>
Abbiamo reso la nostra navigazione consapevole del fatto che dovrebbe visualizzare solo determinati elementi a seconda che l'utente sia connesso o meno. auth.loggedIn
può, ovviamente, essere utilizzato ovunque sia possibile importare il servizio di autenticazione.
Protezione dell'API
Potresti pensare, è fantastico... tutto sembra funzionare meravigliosamente . Ma prova ad accedere con tutti e tre i diversi nomi utente e noterai qualcosa: restituiscono tutti lo stesso elenco di cose da fare. Se diamo un'occhiata al nostro server API, possiamo vedere che ogni utente, in effetti, ha il proprio elenco di elementi, quindi che succede?
Bene, ricorda quando abbiamo iniziato, abbiamo codificato il nostro endpoint API /todos
per restituire sempre l'elenco delle cose da fare per userID=1
. Questo perché non avevamo modo di sapere chi fosse l'utente attualmente connesso.
Ora lo facciamo, quindi vediamo quanto è facile proteggere i nostri endpoint e utilizzare le informazioni codificate nel JWT per fornire l'identità utente richiesta. Inizialmente, aggiungi questa riga al tuo file server/app.js
proprio sotto l'ultima chiamata app.use()
:
app.use(expressJwt({secret: 'todo-app-super-shared-secret'}).unless({path: ['/api/auth']}));
Usiamo il middleware express-jwt
, gli diciamo qual è il segreto condiviso e specifichiamo un array di percorsi per i quali non dovrebbe richiedere un JWT. E questo è tutto. Non c'è bisogno di toccare ogni singolo endpoint, creare istruzioni if
dappertutto o altro.
Internamente, il middleware sta facendo alcune ipotesi. Ad esempio, presuppone che l'intestazione HTTP di Authorization
segua il modello JWT comune di Bearer {token}
. (La libreria ha molte opzioni per personalizzare il suo funzionamento in caso contrario. Vedi l'utilizzo di express-jwt per maggiori dettagli.)
Il nostro secondo obiettivo è utilizzare le informazioni codificate JWT per scoprire chi sta effettuando la chiamata. Ancora una volta express-jwt
viene in soccorso. Come parte della lettura del token e della sua verifica, imposta il payload codificato che abbiamo inviato nel processo di firma alla variabile req.user
in Express. Possiamo quindi usarlo per accedere immediatamente a qualsiasi variabile che abbiamo memorizzato. Nel nostro caso, impostiamo userID
uguale all'ID dell'utente autenticato e come tale possiamo usarlo direttamente come req.user.userID
.
Aggiorna nuovamente server/app.js
e modifica l'endpoint /todos
in modo da leggere come segue:
res.send(getTodos(req.user.userID));
E questo è tutto. La nostra API è ora protetta contro l'accesso non autorizzato e possiamo determinare in sicurezza chi è il nostro utente autenticato in qualsiasi endpoint. La nostra applicazione client ha anche un semplice processo di autenticazione e tutti i servizi HTTP che scriviamo che chiamano il nostro endpoint API avranno automaticamente un token di autenticazione allegato.
Se hai clonato il repository Github e vuoi semplicemente vedere il risultato finale in azione, puoi controllare il codice nella sua forma finale usando:
git checkout with-jwt
Spero che tu abbia trovato questa procedura dettagliata utile per aggiungere l'autenticazione JWT alle tue app Angular. Grazie per aver letto!