Cum se face autentificarea JWT cu un Angular 6 SPA

Publicat: 2022-03-11

Astăzi vom arunca o privire asupra cât de ușor este să integrați autentificarea JSON web token (JWT) în aplicația dvs. cu o singură pagină (SPA) Angular 6 (sau mai recent). Să începem cu un pic de fundal.

Ce sunt jetoanele web JSON și de ce să le folosiți?

Cel mai simplu și mai concis răspuns aici este că sunt convenabile, compacte și sigure. Să ne uităm la aceste afirmații în detaliu:

  1. Convenabil : Utilizarea unui JWT pentru autentificare la back-end odată conectat necesită setarea unui antet HTTP, o sarcină care poate fi automatizată cu ușurință printr-o funcție sau subclasare, așa cum vom vedea mai târziu.
  2. Compact : un token este pur și simplu un șir codificat în base64, care conține câteva câmpuri de antet și o sarcină utilă, dacă este necesar. JWT total este de obicei mai mic de 200 de octeți, chiar dacă este semnat.
  3. Securizat : Deși nu este necesar, o caracteristică excelentă de securitate a JWT este că token-urile pot fi semnate folosind fie criptarea perechilor de chei publice/private RSA, fie criptarea HMAC folosind un secret partajat. Acest lucru asigură originea și valabilitatea unui simbol.

Totul se rezumă la faptul că aveți o modalitate sigură și eficientă de a autentifica utilizatorii și apoi de a verifica apelurile către punctele finale API fără a fi nevoie să analizați structuri de date sau să vă implementați propria criptare.

Teoria aplicației

Flux de date tipic pentru autentificarea JWT și utilizarea între sistemele front-end și back-end

Așadar, cu puține cunoștințe, acum ne putem scufunda în modul în care ar funcționa acest lucru într-o aplicație reală. Pentru acest exemplu, voi presupune că avem un server Node.js care găzduiește API-ul nostru și dezvoltăm o listă de lucruri SPA folosind Angular 6. Să lucrăm și cu această structură API:

  • /authPOST (postați numele de utilizator și parola pentru a vă autentifica și a primi înapoi un JWT)
  • /todosGET (returnează o listă de articole din lista de tot pentru utilizator)
  • /todos/{id}GET (returnează un anumit articol din lista de lucruri)
  • /usersGET (returnează o listă de utilizatori)

Vom trece prin crearea acestei aplicații simple în scurt timp, dar deocamdată să ne concentrăm asupra interacțiunii în teorie. Avem o pagină simplă de autentificare, unde utilizatorul își poate introduce numele de utilizator și parola. Când formularul este trimis, acesta trimite acele informații către punctul final /auth . Serverul Node poate apoi autentifica utilizatorul în orice mod este adecvat (căutare în baze de date, interogare la alt serviciu web etc.), dar în cele din urmă punctul final trebuie să returneze un JWT.

JWT pentru acest exemplu va conține câteva revendicări rezervate și câteva revendicări private . Revendicările rezervate sunt pur și simplu perechi cheie-valoare recomandate de JWT utilizate în mod obișnuit pentru autentificare, în timp ce revendicările private sunt perechi cheie-valoare aplicabile numai aplicației noastre:

Revendicări rezervate

  • iss : emitentul acestui token. De obicei, FQDN-ul serverului, dar poate fi orice, atâta timp cât aplicația client știe să se aștepte.
  • exp : data și ora de expirare a acestui token. Acesta este în secunde de la miezul nopții 01 ianuarie 1970 GMT (ora Unix).
  • nbf : Nu este valabil înainte de marcajul de timp. Nu este folosit des, dar oferă o limită inferioară pentru fereastra de valabilitate. Același format ca exp .

Revendicări private

  • uid : ID de utilizator al utilizatorului conectat.
  • role : Rol atribuit utilizatorului conectat.

Informațiile noastre vor fi codificate în base64 și semnate folosind HMAC cu cheia partajată todo-app-super-shared-secret . Mai jos este un exemplu despre cum arată JWT:

 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ0b2RvYXBpIiwibmJmIjoxNDk4MTE3NjQyLCJleHAiOjE0OTgxMjEyNDIsInVpZCI6MSwicm9sZSI6ImFkbWluIn0.ZDz_1vcIlnZz64nSM28yA1s-4c_iw3Z2ZtP-SgcYRPQ

Acest șir este tot ce avem nevoie pentru a ne asigura că avem o autentificare validă, pentru a ști ce utilizator este conectat și pentru a ști chiar și ce rol(e) are utilizatorul.

Cele mai multe biblioteci și aplicații continuă să stocheze acest JWT în localStorage sau sessionStorage pentru o recuperare ușoară, dar aceasta este doar o practică obișnuită. Ceea ce faceți cu simbolul depinde de dvs., atâta timp cât îl puteți furniza pentru viitoarele apeluri API.

Acum, ori de câte ori SPA dorește să efectueze un apel către oricare dintre punctele finale API protejate, trebuie pur și simplu să trimită împreună cu simbolul din antetul HTTP de Authorization .

 Authorization: Bearer {JWT Token}

Notă : Din nou, aceasta este pur și simplu o practică obișnuită. JWT nu prescrie nicio metodă specială pentru a se trimite pe server. De asemenea, îl puteți atașa la adresa URL sau îl puteți trimite într-un cookie.

Odată ce serverul primește JWT, îl poate decoda, poate asigura coerența utilizând secretul partajat HMAC și poate verifica expirarea utilizând câmpurile exp și nbf . De asemenea, ar putea folosi câmpul iss pentru a se asigura că este partea emitentă inițială a acestui JWT.

Odată ce serverul este mulțumit de validitatea token-ului, informațiile stocate în JWT pot fi folosite. De exemplu, uid -ul pe care l-am inclus ne oferă ID-ul utilizatorului care face solicitarea. Pentru acest exemplu special, am inclus și câmpul de role , care ne permite să luăm decizii dacă utilizatorul ar trebui să poată accesa un anumit punct final sau nu. (Dacă aveți încredere în aceste informații sau, mai degrabă, doriți să faceți o căutare în baza de date depinde de nivelul de securitate necesar.)

 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); }

Să construim o aplicație simplă

Pentru a urma, va trebui să aveți o versiune recentă a Node.js (6.x sau mai recent), npm (3.x sau mai recent) și angular-cli instalate. Dacă trebuie să instalați Node.js, care include npm, vă rugăm să urmați instrucțiunile de aici. După aceea angular-cli poate fi instalat folosind npm (sau yarn , dacă l-ați instalat):

 # installation using npm npm install -g @angular/cli # installation using yarn yarn global add @angular/cli

Nu voi intra în detaliu despre placa Angular 6 pe care o vom folosi aici, dar pentru următorul pas, am creat un depozit Github pentru a deține o mică aplicație todo pentru a ilustra simplitatea adăugării autentificării JWT la aplicația dvs. Pur și simplu clonează-l folosind următoarele:

 git clone https://github.com/sschocke/angular-jwt-todo.git cd angular-jwt-todo git checkout pre-jwt

Comanda git checkout pre-jwt comută la o versiune numită în care JWT nu a fost implementat.

Ar trebui să existe două foldere în interior numite server și client . Serverul este un server API Node care va găzdui API-ul nostru de bază. Clientul este aplicația noastră Angular 6.

Serverul API Node

Pentru a începe, instalați dependențele și porniți serverul API.

 cd server # installation using npm npm install # or installation using yarn yarn node app.js

Ar trebui să puteți urma aceste link-uri și să obțineți o reprezentare JSON a datelor. Doar pentru moment, până când avem autentificare, am codificat hardpoint-ul /todos pentru a returna sarcinile pentru userID=1 :

  • http://localhost:4000: Pagina de testare pentru a vedea dacă serverul Node rulează
  • http://localhost:4000/api/users: returnează lista utilizatorilor din sistem
  • http://localhost:4000/api/todos: returnează lista de sarcini pentru userID=1

Aplicația Angular

Pentru a începe cu aplicația client, trebuie, de asemenea, să instalăm dependențele și să pornim serverul de dezvoltare.

 cd client # using npm npm install npm start # using yarn yarn yarn start

Notă : În funcție de viteza liniei, poate dura ceva timp pentru a descărca toate dependențele.

Dacă totul merge bine, acum ar trebui să vedeți ceva de genul acesta când navigați la http://localhost:4200:

Versiunea care nu este activată pentru JWT a aplicației noastre Angular Todo List.

Adăugarea de autentificare prin JWT

Pentru a adăuga suport pentru autentificarea JWT, vom folosi câteva biblioteci standard disponibile care o simplifică. Puteți, desigur, să renunțați la aceste facilități și să implementați totul singur, dar acest lucru depășește domeniul nostru de aplicare aici.

Mai întâi, să instalăm o bibliotecă pe partea clientului. Este dezvoltat și întreținut de Auth0, care este o bibliotecă care vă permite să adăugați autentificare bazată pe cloud la un site web. Utilizarea bibliotecii în sine nu necesită utilizarea serviciilor lor.

 cd client # installation using npm npm install @auth0/angular-jwt # installation using yarn yarn add @auth0/angular-jwt

Vom ajunge la cod într-o secundă, dar în timp ce suntem la el, să setăm și partea serverului. Vom folosi bibliotecile body-parser , jsonwebtoken și express-jwt pentru a face Node să înțeleagă corpurile JSON POST și JWT-urile.

 cd server # installation using npm npm install body-parser jsonwebtoken express-jwt # installation using yarn yarn add body-parser jsonwebtoken express-jwt

Punct final API pentru autentificare

În primul rând, avem nevoie de o modalitate de a autentifica utilizatorii înainte de a le oferi un simbol. Pentru demonstrația noastră simplă, vom configura doar un punct final de autentificare fix cu un nume de utilizator și o parolă codificate. Acest lucru poate fi la fel de simplu sau la fel de complex pe cât cere aplicația dvs. Important este să trimiți înapoi un JWT.

În server/app.js adăugați o intrare sub cealaltă linii de require , după cum urmează:

 const bodyParser = require('body-parser'); const jwt = require('jsonwebtoken'); const expressJwt = require('express-jwt');

Precum și următoarele:

 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}); });

Acesta este în mare parte cod JavaScript de bază. Obținem corpul JSON care a fost transmis punctului final /auth , găsim un utilizator care se potrivește cu acel nume de utilizator, verificăm dacă avem un utilizator și parola se potrivesc și returnăm o eroare HTTP 401 Unauthorized dacă nu.

Partea importantă este generarea token-ului și o vom descompune după cei trei parametri ai săi. Sintaxa pentru sign este următoarea: jwt.sign(payload, secretOrPrivateKey, [options, callback]) , unde:

  • payload este un obiect literal al perechilor cheie-valoare pe care doriți să le codificați în simbolul dvs. Aceste informații pot fi apoi decodificate din token de către oricine are cheia de decriptare. În exemplul nostru, codificăm user.id , astfel încât atunci când primim din nou jetonul pe back-end pentru autentificare, să știm cu ce utilizator avem de-a face.
  • secretOrPrivateKey este fie o cheie secretă partajată de criptare HMAC - aceasta este ceea ce am folosit în aplicația noastră, pentru simplitate - sau o cheie privată de criptare RSA/ECDSA.
  • options reprezintă o varietate de opțiuni care pot fi transmise codificatorului sub formă de perechi cheie-valoare. De obicei, specificăm cel puțin expiresIn (devine exp rezervat revendicare) și issuer ( iss rezervat revendicare) astfel încât un token să nu fie valabil pentru totdeauna, iar serverul poate verifica dacă de fapt a emis jetonul inițial.
  • callback este o funcție de apelat după ce se termină codificarea, dacă cineva dorește să se ocupe de codificarea jetonului în mod asincron.

(De asemenea, puteți citi mai multe detalii despre options și despre cum să utilizați criptografia cu cheie publică în loc de o cheie secretă partajată.)

Integrare Angular 6 JWT

Pentru a face ca Angular 6 să funcționeze cu JWT-ul nostru este destul de simplu folosind angular-jwt . Pur și simplu adăugați următoarele la 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'] } }) ], // ... }

Asta este practic tot ceea ce este necesar. Desigur, mai avem ceva de adăugat pentru a face autentificarea inițială, dar biblioteca angular-jwt se ocupă de trimiterea jetonului împreună cu fiecare solicitare HTTP.

  • Funcția tokenGetter() face exact ceea ce spune, dar modul în care este implementată depinde în întregime de dvs. Am ales să returnăm simbolul pe care îl salvăm în localStorage . Desigur, sunteți liber să furnizați orice altă metodă pe care o doriți, atâta timp cât returnează șirul codificat cu simbolul web JSON .
  • Opțiunea whiteListedDomains există, astfel încât să puteți restricționa domeniile către care este trimis JWT, astfel încât API-urile publice să nu primească și JWT-ul dvs.
  • Opțiunea blackListedRoutes vă permite să specificați rute specifice care nu ar trebui să primească JWT, chiar dacă se află pe un domeniu pe lista albă. De exemplu, punctul final de autentificare nu trebuie să îl primească pentru că nu are rost: token-ul este de obicei nul atunci când este oricum apelat.

Făcând totul să funcționeze împreună

În acest moment, avem o modalitate de a genera un JWT pentru un anumit utilizator folosind punctul final /auth de pe API-ul nostru și avem instalațiile efectuate pe Angular pentru a trimite un JWT cu fiecare solicitare HTTP. Grozav, dar ați putea sublinia că absolut nimic nu s-a schimbat pentru utilizator. Și ai avea dreptate. Încă putem naviga la fiecare pagină din aplicația noastră și putem apela orice punct final API fără a trimite măcar un JWT. Nu e bun!

Trebuie să ne actualizăm aplicația client pentru a ne îngrijora cine este conectat și, de asemenea, să ne actualizăm API-ul pentru a solicita un JWT. Să începem.

Vom avea nevoie de o nouă componentă Angular pentru a vă conecta. De dragul conciziei, voi păstra acest lucru cât mai simplu posibil. De asemenea, vom avea nevoie de un serviciu care să se ocupe de toate cerințele noastre de autentificare și de un Angular Guard pentru a proteja rutele care nu ar trebui să fie accesibile înainte de a ne conecta. Vom face următoarele în contextul aplicației 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

Acest lucru ar fi trebuit să genereze patru fișiere noi în folderul client :

 src/app/login/login.component.html src/app/login/login.component.ts src/app/auth.service.ts src/app/auth.guard.ts

În continuare, trebuie să furnizăm serviciul de autentificare și să asigurăm aplicația noastră. Actualizați client/src/app/app.modules.ts :

 import { AuthService } from './auth.service'; import { AuthGuard } from './auth.guard'; // ... providers: [ TodoService, UserService, AuthService, AuthGuard ],

Și apoi actualizați rutarea în client/src/app/app-routing.modules.ts pentru a utiliza gardul de autentificare și a furniza o rută pentru componenta de conectare.

 // ... 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}, // ...

În cele din urmă, actualizați client/src/app/auth.guard.ts cu următorul conținut:

 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; } }

Pentru aplicația noastră demo, pur și simplu verificăm existența unui JWT în stocarea locală. În aplicațiile din lumea reală, ai decoda jetonul și ai verifica valabilitatea, expirarea, etc. De exemplu, ai putea folosi JwtHelperService pentru asta.

În acest moment, aplicația noastră Angular vă va redirecționa întotdeauna către pagina de conectare, deoarece nu avem cum să ne autentificăm. Să rectificăm asta, începând cu serviciul de autentificare din 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); } }

Serviciul nostru de autentificare are doar două funcții, login și logout :

  • login POST este numele de username și password furnizate pentru back-end-ul nostru și setează access_token -ul în localStorage dacă primește unul înapoi. De dragul simplității, nu există nicio gestionare a erorilor aici.
  • logout pur și simplu șterge access_token din localStorage , necesitând achiziționarea unui nou token înainte ca orice altceva să poată fi accesat din nou.
  • loggedIn este o proprietate booleană pe care o putem folosi rapid pentru a determina dacă utilizatorul este conectat sau nu.

Și în sfârșit, componenta de conectare. Acestea nu au nicio legătură cu lucrul efectiv cu JWT, așa că nu ezitați să copiați și să lipiți în 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>

Și client/src/app/login/login.components.ts va avea nevoie de:

 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' ); } }

Voila, exemplul nostru de conectare la Angular 6:

Ecranul de conectare al aplicației noastre exemplu Angular Todo List.

În această etapă, ar trebui să ne putem autentifica (folosind jemma , paul sau sebastian cu parola todo ) și să vedem din nou toate ecranele. Dar aplicația noastră arată aceleași anteturi de navigare și nicio modalitate de a vă deconecta, indiferent de starea curentă. Să reparăm asta înainte de a trece la remedierea API-ului nostru.

În client/src/app/app.component.ts , înlocuiți întregul fișier cu următoarele:

 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']); } }

Și pentru client/src/app/app.component.html înlocuiți secțiunea <nav> cu următoarele:

 <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>

Am conștientizat contextul nostru de navigare că ar trebui să afișeze doar anumite elemente, în funcție de dacă utilizatorul este conectat sau nu. auth.loggedIn poate fi folosit, desigur, oriunde puteți importa serviciul de autentificare.

Securizarea API-ului

S-ar putea să vă gândiți, este grozav... totul pare să funcționeze minunat . Dar încercați să vă conectați cu toate cele trei nume de utilizator diferite și veți observa ceva: toate returnează aceeași listă de lucruri. Dacă ne uităm la serverul nostru API, putem vedea că fiecare utilizator are, de fapt, propria sa listă de articole, deci ce e?

Ei bine, amintiți-vă, când am început, am codificat punctul nostru final al API-ului /todos pentru a returna întotdeauna lista de lucruri pentru userID=1 . Acest lucru se datorează faptului că nu aveam nicio modalitate de a ști cine era utilizatorul conectat în prezent.

Acum o facem, așa că haideți să vedem cât de ușor este să ne securizăm punctele finale și să folosim informațiile codificate în JWT pentru a furniza identitatea utilizatorului necesară. Inițial, adăugați această linie la fișierul server/app.js chiar sub ultimul apel app.use() :

 app.use(expressJwt({secret: 'todo-app-super-shared-secret'}).unless({path: ['/api/auth']}));

Folosim middleware-ul express-jwt , îi spunem care este secretul partajat și specificăm o serie de căi pentru care nu ar trebui să necesite un JWT. Si asta e. Nu este nevoie să atingeți fiecare punct final, să creați declarații if peste tot sau orice altceva.

Pe plan intern, middleware-ul face câteva presupuneri. De exemplu, se presupune că antetul HTTP de Authorization urmează modelul JWT comun al Bearer {token} . (Biblioteca are totuși o mulțime de opțiuni pentru personalizarea modului în care funcționează dacă nu este cazul. Consultați Express-jwt Utilizare pentru mai multe detalii.)

Al doilea obiectiv al nostru este să folosim informațiile codificate JWT pentru a afla cine efectuează apelul. Încă o dată express-jwt vine în ajutor. Ca parte a citirii jetonului și a verificării acestuia, setează sarcina utilă codificată pe care am trimis-o în procesul de semnare variabilei req.user în Express. Apoi îl putem folosi pentru a accesa imediat oricare dintre variabilele stocate. În cazul nostru, setăm userID egal cu ID-ul utilizatorului autentificat și, ca atare, îl putem folosi direct ca req.user.userID .

Actualizați server/app.js din nou și modificați punctul final /todos astfel încât să citească după cum urmează:

 res.send(getTodos(req.user.userID)); 

Aplicația noastră Angular Todo List folosește JWT pentru a afișa lista de sarcini a utilizatorului conectat, mai degrabă decât cea pe care am codificat-o mai devreme.

Si asta e. API-ul nostru este acum securizat împotriva accesului neautorizat și putem determina în siguranță cine este utilizatorul nostru autentificat în orice punct final. Aplicația noastră client are, de asemenea, un proces simplu de autentificare, iar orice servicii HTTP pe care le scriem și care apelează punctul nostru final API va avea automat atașat un token de autentificare.

Dacă ați clonat depozitul Github și doriți pur și simplu să vedeți rezultatul final în acțiune, puteți verifica codul în forma sa finală folosind:

 git checkout with-jwt

Sper că ați găsit această explicație valoroasă pentru a adăuga autentificare JWT la propriile aplicații Angular. Multumesc pentru lectura!

Înrudit : Tutorial JSON Web Token: Un exemplu în Laravel și AngularJS