如何使用 Angular 6 SPA 進行 JWT 身份驗證

已發表: 2022-03-11

今天我們將看看將 JSON Web 令牌 (JWT) 身份驗證集成到您的 Angular 6(或更高版本)單頁應用程序 (SPA) 中是多麼容易。 讓我們從一些背景開始。

什麼是 JSON Web 令牌,為什麼要使用它們?

這裡最簡單和最簡潔的答案是它們方便、緊湊和安全。 讓我們詳細看看這些說法:

  1. 方便:登錄後使用 JWT 對後端進行身份驗證需要設置一個 HTTP 標頭,這項任務可以通過函數或子類輕鬆自動化,我們稍後會看到。
  2. 緊湊:令牌只是一個 base64 編碼的字符串,包含一些標頭字段,如果需要,還有一個有效負載。 總 JWT 通常少於 200 字節,即使已簽名也是如此。
  3. 安全:雖然不是必需的,但 JWT 的一個重要安全功能是可以使用 RSA 公鑰/私鑰對加密或使用共享密鑰的 HMAC 加密對令牌進行簽名。 這確保了令牌的來源和有效性。

這一切歸結為您擁有一種安全有效的方式來驗證用戶身份,然後驗證對 API 端點的調用,而無需解析任何數據結構或實現自己的加密。

應用理論

前端和後端系統之間 JWT 認證和使用的典型數據流

因此,有了一些背景知識,我們現在可以深入了解這將如何在實際應用程序中工作。 對於這個例子,我假設我們有一個 Node.js 服務器來託管我們的 API,並且我們正在使用 Angular 6 開發一個 SPA 待辦事項列表。讓我們也使用這個 API 結構:

  • /authPOST (發布用戶名和密碼以進行身份驗證並接收 JWT)
  • /todosGET (為用戶返回待辦事項列表項的列表)
  • /todos/{id}GET (返回特定的待辦事項列表項)
  • /usersGET (返回用戶列表)

我們將很快完成這個簡單應用程序的創建,但現在,讓我們專注於理論上的交互。 我們有一個簡單的登錄頁面,用戶可以在其中輸入他們的用戶名和密碼。 提交表單後,它會將該信息發送到/auth端點。 然後,Node 服務器可以以任何合適的方式(數據庫查找、查詢另一個 Web 服務等)對用戶進行身份驗證,但最終端點需要返回 JWT。

此示例的 JWT 將包含一些保留聲明和一些私有聲明。 保留聲明只是 JWT 推薦的通常用於身份驗證的鍵值對,而私有聲明是僅適用於我們的應用程序的鍵值對:

保留索賠

  • iss :此令牌的發行者。 通常是服務器的 FQDN,但只要客戶端應用程序知道期望它,它就可以是任何東西。
  • exp :此令牌的到期日期和時間。 這是自格林威治標準時間 1970 年 1 月 1 日午夜(Unix 時間)以來的秒數。
  • nbf : 在時間戳之前無效。 不經常使用,但給出了有效性窗口的下限。 exp格式相同。

私人索賠

  • uid : 登錄用戶的用戶 ID。
  • role :分配給登錄用戶的角色。

我們的信息將使用 HMAC 和共享密鑰todo-app-super-shared-secret進行 base64 編碼和簽名。 以下是 JWT 的示例:

 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ0b2RvYXBpIiwibmJmIjoxNDk4MTE3NjQyLCJleHAiOjE0OTgxMjEyNDIsInVpZCI6MSwicm9sZSI6ImFkbWluIn0.ZDz_1vcIlnZz64nSM28yA1s-4c_iw3Z2ZtP-SgcYRPQ

我們只需要這個字符串來確保我們有一個有效的登錄名,知道哪個用戶連接了,甚至知道用戶有什麼角色。

大多數庫和應用程序繼續將此 JWT 存儲在localStoragesessionStorage中以便於檢索,但這只是常見的做法。 只要您可以為將來的 API 調用提供它,您對令牌的處理取決於您自己。

現在,每當 SPA 想要調用任何受保護的 API 端點時,它只需要在Authorization HTTP 標頭中發送令牌即可。

 Authorization: Bearer {JWT Token}

注意:再一次,這只是常見的做法。 JWT 沒有規定任何將自身發送到服務器的特定方法。 您也可以將其附加到 URL,或將其發送到 cookie 中。

一旦服務器接收到 JWT,它就可以對其進行解碼,使用 HMAC 共享密鑰確保一致性,並使用expnbf字段檢查到期時間。 它還可以使用iss字段來確保它是這個 JWT 的原始發行方。

一旦服務器對令牌的有效性感到滿意,就可以使用存儲在 JWT 中的信息。 例如,我們包含的uid為我們提供了發出請求的用戶的 ID。 對於這個特定的示例,我們還包括了role字段,它讓我們可以決定用戶是否應該能夠訪問特定的端點。 (您是否信任此信息,或者更願意進行數據庫查找取決於所需的安全級別。)

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

讓我們構建一個簡單的 Todo 應用程序

要繼續,您需要安裝最新版本的 Node.js(6.x 或更高版本)、npm(3.x 或更高版本)和 angular-cli。 如果您需要安裝包含 npm 的 Node.js,請按照此處的說明進行操作。 之後可以使用npm (或yarn ,如果你已經安裝它)安裝angular-cli

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

我不會詳細介紹我們將在此處使用的 Angular 6 樣板,但對於下一步,我創建了一個 Github 存儲庫來保存一個小型 todo 應用程序,以說明將 JWT 身份驗證添加到您的應用程序的簡單性。 只需使用以下內容克隆它:

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

git checkout pre-jwt命令切換到未實現 JWT 的命名版本。

裡面應該有兩個文件夾,分別稱為serverclient 。 該服務器是一個 Node API 服務器,它將託管我們的基本 API。 客戶端是我們的 Angular 6 應用程序。

節點 API 服務器

首先,安裝依賴項並啟動 API 服務器。

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

您應該能夠訪問這些鏈接並獲得數據的 JSON 表示。 就目前而言,在我們進行身份驗證之前,我們已經硬編碼/todos端點以返回userID=1的任務:

  • http://localhost:4000:測試頁面查看Node服務器是否正在運行
  • http://localhost:4000/api/users:返回系統上的用戶列表
  • http://localhost:4000/api/todos:返回userID=1的任務列表

Angular 應用程序

要開始使用客戶端應用程序,我們還需要安裝依賴項並啟動開發服務器。

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

注意:根據您的線路速度,下載所有依賴項可能需要一段時間。

如果一切順利,您現在應該在導航到 http://localhost:4200 時看到如下內容:

我們的 Angular Todo List 應用程序的非 JWT 啟用版本。

通過 JWT 添加身份驗證

為了增加對 JWT 身份驗證的支持,我們將使用一些可用的標準庫來簡化它。 當然,您可以放棄這些便利,自己實現一切,但這超出了我們的範圍。

首先,讓我們在客戶端安裝一個庫。 它由 Auth0 開發和維護,這是一個允許您向網站添加基於雲的身份驗證的庫。 使用圖書館本身並不需要您使用他們的服務。

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

我們將在一秒鐘內獲得代碼,但是當我們在處理它時,讓我們也設置服務器端。 我們將使用body-parserjsonwebtokenexpress-jwt庫來讓 Node 理解 JSON POST 正文和 JWT。

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

用於身份驗證的 API 端點

首先,我們需要一種在給用戶令牌之前對用戶進行身份驗證的方法。 對於我們的簡單演示,我們將使用硬編碼的用戶名和密碼設置一個固定的身份驗證端點。 這可以像您的應用程序需要的那樣簡單或複雜。 重要的是發回 JWT。

server/app.js中,在其他require行下方添加一個條目,如下所示:

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

以及以下內容:

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

這主要是基本的 JavaScript 代碼。 我們獲取傳遞給/auth端點的 JSON 正文,找到與該用戶名匹配的用戶,檢查我們是否有用戶和密碼匹配,如果沒有,則返回401 Unauthorized HTTP 錯誤。

重要的部分是令牌生成,我們將通過它的三個參數對其進行分解。 sign的語法如下: jwt.sign(payload, secretOrPrivateKey, [options, callback]) ,其中:

  • payload是您希望在令牌中編碼的鍵值對的對象文字。 然後,任何擁有解密密鑰的人都可以從令牌中解碼此信息。 在我們的示例中,我們對user.id進行編碼,以便當我們在後端再次收到令牌進行身份驗證時,我們知道我們正在處理的是哪個用戶。
  • secretOrPrivateKey是 HMAC 加密共享密鑰(為簡單起見,這是我們在應用程序中使用的)或 RSA/ECDSA 加密私鑰。
  • options表示可以以鍵值對的形式傳遞給編碼器的各種選項。 通常,我們至少指定expiresIn (成為exp保留聲明)和issuer者( iss保留聲明),以便令牌不會永遠有效,並且服務器可以檢查它實際上是否最初發布了令牌。
  • callback是編碼完成後調用的函數,如果希望異步處理令牌編碼。

(您還可以閱讀有關options的更多詳細信息以及如何使用公鑰加密而不是共享密鑰。)

Angular 6 JWT 集成

使用angular-jwt讓 Angular 6 與我們的 JWT 一起工作非常簡單。 只需將以下內容添加到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'] } }) ], // ... }

這基本上就是所有需要的。 當然,我們需要添加更多代碼來進行初始身份驗證,但是angular-jwt庫會負責將令牌與每個 HTTP 請求一起發送。

  • tokenGetter()函數完全按照它所說的做,但它的實現方式完全取決於你。 我們選擇返回我們保存在localStorage中的令牌。 您當然可以自由地提供您想要的任何其他方法,只要它返回JSON Web 令牌編碼的字符串即可。
  • 存在whiteListedDomains選項,因此您可以限制將 JWT 發送到哪些域,因此公共 API 也不會接收您的 JWT。
  • blackListedRoutes選項允許您指定不應接收 JWT 的特定路由,即使它們位於白名單域中。 例如,身份驗證端點不需要接收它,因為沒有意義:無論如何調用令牌時,它通常為空。

讓一切協同工作

此時,我們有一種方法可以使用 API 上的/auth端點為給定用戶生成 JWT,並且我們在 Angular 上完成了為每個 HTTP 請求發送 JWT 的管道。 很好,但您可能會指出,對於用戶而言,絕對沒有任何改變。 你是對的。 我們仍然可以導航到應用程序中的每個頁面,並且我們可以調用任何 API 端點,甚至無需發送 JWT。 不好!

我們需要更新我們的客戶端應用程序以關注誰登錄,並更新我們的 API 以要求 JWT。 讓我們開始吧。

我們需要一個新的 Angular 組件來登錄。為了簡潔起見,我會盡可能簡單。 我們還需要一個服務來處理我們所有的身份驗證要求,以及一個 Angular Guard 來保護在登錄之前不應訪問的路由。我們將在客戶端應用程序上下文中執行以下操作。

 cd client ng g component login --spec=false --inline-style ng g service auth --flat --spec=false ng g guard auth --flat --spec=false

這應該在client文件夾中生成了四個新文件:

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

接下來,我們需要為我們的應用程序提供認證服務和守衛。 更新client/src/app/app.modules.ts

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

然後更新client/src/app/app-routing.modules.ts中的路由以利用身份驗證保護並為登錄組件提供路由。

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

最後,使用以下內容更新client/src/app/auth.guard.ts

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

對於我們的演示應用程序,我們只是檢查本地存儲中是否存在 JWT。 在實際應用程序中,您將解碼令牌並檢查其有效性、過期等。例如,您可以為此使用 JwtHelperService。

此時,我們的 Angular 應用程序現在將始終將您重定向到登錄頁面,因為我們無法登錄。讓我們糾正它,從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); } }

我們的身份驗證服務只有兩個功能, loginlogout

  • login POST將提供的usernamepassword發送到我們的後端,如果收到返回,則在localStorage中設置access_token為了簡單起見,這裡沒有錯誤處理。
  • logout只是從localStorage中清除access_token ,需要獲取一個新令牌,然後才能再次訪問任何進一步的內容。
  • loggedIn是一個布爾屬性,我們可以快速使用它來確定用戶是否登錄。

最後是登錄組件。 這些與實際使用 JWT 無關,因此請隨意複製並粘貼到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>

並且client/src/app/login/login.components.ts將需要:

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

瞧,我們的 Angular 6 登錄示例:

我們的示例 Angular Todo List 應用程序的登錄屏幕。

在這個階段,我們應該能夠登錄(使用jemmapaulsebastian並輸入密碼todo並再次看到所有屏幕。 但是我們的應用程序顯示相同的導航標題,並且無論當前狀態如何都無法註銷。 在我們繼續修復我們的 API 之前,讓我們修復它。

client/src/app/app.component.ts中,將整個文件替換為以下內容:

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

對於client/src/app/app.component.html ,將<nav>部分替換為以下內容:

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

我們已經讓我們的導航上下文感知,它應該只顯示某些項目,具體取決於用戶是否登錄。 auth.loggedIn當然,可以在任何可以導入身份驗證服務的地方使用。

保護 API

你可能會想,這太棒了……一切看起來都很好。 但是嘗試使用所有三個不同的用戶名登錄,您會注意到一些事情:它們都返回相同的待辦事項列表。 如果我們查看我們的 API 服務器,我們可以看到每個用戶實際上都有自己的項目列表,那麼發生了什麼?

好吧,請記住,當我們開始時,我們將/todos API 端點編碼為始終返回userID=1的待辦事項列表。 這是因為我們無法知道當前登錄的用戶是誰。

現在我們做到了,讓我們看看保護我們的端點和使用 JWT 中編碼的信息來提供所需的用戶身份是多麼容易。 最初,將這一行添加到您的server/app.js文件中,就在最後一個app.use()調用下方:

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

我們使用express-jwt中間件,告訴它共享密鑰是什麼,並指定一個不需要 JWT 的路徑數組。 就是這樣。 無需觸及每個端點,創建if語句,或任何事情。

在內部,中間件做了一些假設。 例如,它假設Authorization HTTP 標頭遵循Bearer {token}的常見 JWT 模式。 (如果不是這種情況,該庫有很多選項可以自定義它的工作方式。有關更多詳細信息,請參閱 express-jwt 用法。)

我們的第二個目標是使用 JWT 編碼的信息來找出誰在撥打電話。 express-jwt再次來救援。 作為讀取和驗證令牌的一部分,它將我們在簽名過程中發送的編碼有效負載設置為 Express 中的變量req.user 。 然後我們可以使用它來立即訪問我們存儲的任何變量。 在我們的例子中,我們將userID設置為經過身份驗證的用戶的 ID,因此我們可以直接將其用作req.user.userID

再次更新server/app.js ,並將/todos端點更改為如下所示:

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

我們的 Angular Todo List 應用程序利用 JWT 顯示登錄用戶的 todo 列表,而不是我們之前硬編碼的那個。

就是這樣。 我們的 API 現在可以防止未經授權的訪問,並且我們可以安全地確定我們在任何端點中的經過身份驗證的用戶是誰。 我們的客戶端應用程序也有一個簡單的身份驗證過程,我們編寫的任何調用 API 端點的 HTTP 服務都將自動附加一個身份驗證令牌。

如果您克隆了 Github 存儲庫,並且只是想查看最終結果,您可以使用以下命令查看最終形式的代碼:

 git checkout with-jwt

我希望您發現本演練對於將 JWT 身份驗證添加到您自己的 Angular 應用程序很有價值。 謝謝閱讀!

相關: JSON Web Token 教程:Laravel 和 AngularJS 中的示例