|
一個實(shí)際範(fàn)例
介紹了有關(guān)基於令牌的身份驗(yàn)證的一些基本資訊後,我們現(xiàn)在可以繼續(xù)討論一個實(shí)際範(fàn)例??纯聪旅娴募軜?gòu),然後我們將更詳細(xì)地分析它:

#
- 多個用戶端(例如網(wǎng)路應(yīng)用程式或行動用戶端)出於特定目的向 API 發(fā)出請求。
- 要求是向
https://api.yourexampleapp.com
等服務(wù)發(fā)出的。如果很多人使用該應(yīng)用程序,則可能需要多個伺服器來提供請求的操作。
- 這裡,負(fù)載平衡器用於平衡請求,以最適合後端的應(yīng)用程式伺服器。當(dāng)您向
https://api.yourexampleapp.com
發(fā)出請求時(shí),負(fù)載平衡器會先處理請求,然後會將客戶端重新導(dǎo)向到特定伺服器。
- 有一個應(yīng)用程序,並且該應(yīng)用程式部署到多臺伺服器(server-1、server-2、...、server-n)。每當(dāng)向
https://api.yourexampleapp.com
發(fā)出請求時(shí),後端應(yīng)用程式都會攔截請求標(biāo)頭並從授權(quán)標(biāo)頭中提取令牌資訊。將使用此令牌進(jìn)行資料庫查詢。如果此令牌有效且具有存取所請求端點(diǎn)所需的權(quán)限,則它將繼續(xù)。如果沒有,它將傳回 403 回應(yīng)代碼(表示禁止?fàn)顟B(tài))。
優(yōu)點(diǎn)
基於令牌的身份驗(yàn)證具有解決嚴(yán)重問題的多個優(yōu)點(diǎn)。以下是其中的一些:
獨(dú)立於客戶端的服務(wù)
在基於令牌的身份驗(yàn)證中,令牌透過請求標(biāo)頭傳輸,而不是將身份驗(yàn)證資訊保留在會話或 cookie 中。這意味著沒有狀態(tài)。您可以從任何類型的可以發(fā)出 HTTP 請求的客戶端向伺服器發(fā)送請求。
內(nèi)容傳遞網(wǎng)路 (CDN)
在目前的大多數(shù) Web 應(yīng)用程式中,視圖在後端呈現(xiàn),HTML 內(nèi)容返回瀏覽器。前端邏輯依賴後端程式碼。
沒有必要建立這樣的依賴關(guān)係。這帶來了幾個問題。例如,如果您正在與實(shí)作前端 HTML、CSS 和 JavaScript 的設(shè)計(jì)機(jī)構(gòu)合作,您需要將該前端程式碼遷移到後端程式碼中,以便進(jìn)行一些渲染或填滿操作。一段時(shí)間後,您呈現(xiàn)的 HTML 內(nèi)容將與程式碼機(jī)構(gòu)實(shí)現(xiàn)的內(nèi)容有很大不同。
在基於令牌的身份驗(yàn)證中,您可以與後端程式碼分開開發(fā)前端專案。您的後端程式碼將傳回 JSON 回應(yīng),而不是渲染的 HTML,而且您可以將前端程式碼的縮小、gzip 版本放入 CDN 中。當(dāng)您造訪網(wǎng)頁時(shí),HTML 內(nèi)容將從 CDN 提供,並且頁面內(nèi)容將由 API 服務(wù)使用授權(quán)標(biāo)頭中的令牌填充。
無 Cookie 會話(或無 CSRF)
CSRF 是現(xiàn)代網(wǎng)路安全的一個主要問題,因?yàn)樗粫z查請求來源是否可信。為了解決這個問題,使用令牌池在每個表單貼文上發(fā)送該令牌。在基於令牌的身份驗(yàn)證中,令牌用於授權(quán)標(biāo)頭,而 CSRF 不包含該資訊。
持久令牌儲存
當(dāng)應(yīng)用程式中進(jìn)行會話讀取、寫入或刪除操作時(shí),它會在作業(yè)系統(tǒng)的 temp
資料夾中進(jìn)行檔案操作,至少第一次是這樣。假設(shè)您有多個伺服器,並且在第一臺伺服器上建立了一個會話。當(dāng)您發(fā)出另一個請求並且您的請求落入另一臺伺服器時(shí),會話資訊將不存在並且將得到「未經(jīng)授權(quán)」的回應(yīng)。我知道,你可以透過黏性會話來解決這個問題。然而,在基於令牌的認(rèn)證中,這種情況自然就解決了。不存在黏性會話問題,因?yàn)檎埱罅钆圃谌魏嗡欧魃系拿總€請求上都會被攔截。
這些是基於令牌的身份驗(yàn)證和通訊的最常見優(yōu)點(diǎn)。關(guān)於基於令牌的身份驗(yàn)證的理論和架構(gòu)討論就到此結(jié)束。是時(shí)候看一個實(shí)際例子了。
範(fàn)例應(yīng)用程式
您將看到兩個應(yīng)用程式來演示基於令牌的身份驗(yàn)證:
- 基於令牌的身份驗(yàn)證後端
- 基於令牌的身份驗(yàn)證前端
在後端專案中,會有服務(wù)的實(shí)現(xiàn),服務(wù)結(jié)果將是JSON格式。服務(wù)中沒有返回視圖。在前端專案中,將有一個用於前端 HTML 的 Angular 項(xiàng)目,然後前端應(yīng)用程式將由 Angular 服務(wù)填充,以向後端服務(wù)發(fā)出請求。
基於令牌的身份驗(yàn)證後端
在後端專案中,主要有三個檔案:
-
package.json 用於依賴管理。
-
models/User.js 包含一個使用者模型,用於對使用者進(jìn)行資料庫操作。
-
server.js 用於專案引導(dǎo)和請求處理。
就是這樣!這個項(xiàng)目非常簡單,因此您無需深入研究即可輕鬆理解主要概念。
{
"name": "angular-restful-auth",
"version": "0.0.1",
"dependencies": {
"body-parser": "^1.20.2",
"express": "4.x",
"express-jwt": "8.4.1",
"jsonwebtoken": "9.0.0",
"mongoose": "7.3.1",
"morgan": "latest"
},
"engines": {
"node": ">=0.10.0"
}
}
?
package.json 包含專案的依賴: express
用於MVC,body-parser
用於模擬post Node. js 中的請求處理,morgan
用於請求日誌記錄,mongoose
用於我們的ORM 框架連接到MongoDB,和jsonwebtoken
用於使用我們的使用者模型建立JWT 令牌。還有一個名為 engines
的屬性,表示該專案是使用 Node.js 版本 >= 0.10.0 製作的。這對於 Heroku 等 PaaS 服務(wù)很有用。我們還將在另一節(jié)中討論主題。
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const UserSchema = new Schema({
email: String,
password: String,
token: String
});
module.exports = mongoose.model('User', UserSchema);
?
我們說過我們將使用使用者模型有效負(fù)載產(chǎn)生令牌。這個模型幫助我們對MongoDB進(jìn)行使用者操作。在User.js中,定義了使用者模式並使用貓鼬模型建立了使用者模型。該模型已準(zhǔn)備好進(jìn)行資料庫操作。
我們的依賴關(guān)係已經(jīng)定義,我們的使用者模型也已經(jīng)定義,所以現(xiàn)在讓我們將所有這些組合起來建構(gòu)一個用於處理特定請求的服務(wù)。
// Required Modules
const express = require("express");
const morgan = require("morgan");
const bodyParser = require("body-parser");
const jwt = require("jsonwebtoken");
const mongoose = require("mongoose");
const app = express();
?
在 Node.js 中,您可以使用 require
在專案中包含模組。首先,我們需要將必要的模組導(dǎo)入到專案中:
const port = process.env.PORT || 3001;
const User = require('./models/User');
// Connect to DB
mongoose.connect(process.env.MONGO_URL);
?
我們的服務(wù)將透過特定連接埠提供服務(wù)。如果系統(tǒng)環(huán)境變數(shù)中定義了任何連接埠變量,則可以使用它,或者我們定義了連接埠 3001
。之後,包含了User模型,並建立了資料庫連接,以進(jìn)行一些使用者操作。不要忘記為資料庫連接 URL 定義一個環(huán)境變數(shù) MONGO_URL
。
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
app.use(morgan("dev"));
app.use(function(req, res, next) {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST');
res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,content-type, Authorization');
next();
});
?
在上面的部分中,我們使用 Express 進(jìn)行了一些配置來模擬 Node 中的 HTTP 請求處理。我們允許來自不同網(wǎng)域的請求,以便開發(fā)獨(dú)立於客戶端的系統(tǒng)。如果您不允許這樣做,您將在網(wǎng)頁瀏覽器中觸發(fā) CORS(跨來源請求共用)錯誤。
-
Access-Control-Allow-Origin
允許所有域。
- 您可以向此服務(wù)發(fā)送
POST
和 GET
請求。
-
X-Requested-With
和 content-type
標(biāo)頭是允許的。
app.post('/authenticate', async function(req, res) {
try {
const user = await User.findOne({ email: req.body.email, password: req.body.password }).exec();
if (user) {
res.json({
type: true,
data: user,
token: user.token
});
} else {
res.json({
type: false,
data: "Incorrect email/password"
});
}
} catch (err) {
res.json({
type: false,
data: "Error occurred: " + err
});
}
});
?
我們已經(jīng)導(dǎo)入了所有必需的模塊并定義了我們的配置,所以現(xiàn)在是時(shí)候定義請求處理程序了。在上面的代碼中,每當(dāng)你使用用戶名和密碼向 /authenticate
發(fā)出 POST
請求時(shí),你都會得到一個 JWT
令牌。首先,使用用戶名和密碼處理數(shù)據(jù)庫查詢。如果用戶存在,則用戶數(shù)據(jù)將與其令牌一起返回。但是如果沒有與用戶名和/或密碼匹配的用戶怎么辦?
app.post('/signin', async function(req, res) {
try {
const existingUser = await User.findOne({ email: req.body.email }).exec();
if (existingUser) {
res.json({
type: false,
data: "User already exists!"
});
} else {
const userModel = new User();
userModel.email = req.body.email;
userModel.password = req.body.password;
const savedUser = await userModel.save();
savedUser.token = jwt.sign(savedUser.toObject(), process.env.JWT_SECRET);
const updatedUser = await savedUser.save();
res.json({
type: true,
data: updatedUser,
token: updatedUser.token
});
}
} catch (err) {
res.json({
type: false,
data: "Error occurred: " + err
});
}
});
?
當(dāng)您使用用戶名和密碼向 /signin
發(fā)出 POST
請求時(shí),將使用發(fā)布的用戶信息創(chuàng)建一個新用戶。在 14th
行,您可以看到使用 jsonwebtoken
模塊生成了一個新的 JSON 令牌,該令牌已分配給 jwt
變量。認(rèn)證部分沒問題。如果我們嘗試訪問受限端點(diǎn)怎么辦?我們?nèi)绾卧O(shè)法訪問該端點(diǎn)?
app.get('/me', ensureAuthorized, async function(req, res) {
try {
const user = await User.findOne({ token: req.token }).exec();
res.json({
type: true,
data: user
});
} catch (err) {
res.json({
type: false,
data: "Error occurred: " + err
});
}
});
?
當(dāng)您向 /me
發(fā)出 GET
請求時(shí),您將獲得當(dāng)前用戶信息,但為了繼續(xù)請求的端點(diǎn),確保Authorized
函數(shù)將被執(zhí)行。
function ensureAuthorized(req, res, next) {
var bearerToken;
var bearerHeader = req.headers["authorization"];
if (typeof bearerHeader !== 'undefined') {
var bearer = bearerHeader.split(" ");
bearerToken = bearer[1];
req.token = bearerToken;
next();
} else {
res.send(403);
}
}
?
在該函數(shù)中,攔截請求頭,并提取authorization
頭。如果此標(biāo)頭中存在承載令牌,則該令牌將分配給 req.token
以便在整個請求中使用,并且可以使用 next( )
。如果令牌不存在,您將收到 403(禁止)響應(yīng)。讓我們回到處理程序 /me
,并使用 req.token
使用此令牌獲取用戶數(shù)據(jù)。每當(dāng)您創(chuàng)建新用戶時(shí),都會生成一個令牌并將其保存在數(shù)據(jù)庫的用戶模型中。這些令牌是獨(dú)一無二的。
對于這個簡單的項(xiàng)目,我們只有三個處理程序。之后,您將看到:
process.on('uncaughtException', function(err) {
console.log(err);
});
?
如果發(fā)生錯誤,Node.js 應(yīng)用程序可能會崩潰。使用上面的代碼,可以防止崩潰,并在控制臺中打印錯誤日志。最后,我們可以使用以下代碼片段啟動服務(wù)器。
// Start Server
app.listen(port, function () {
console.log( "Express server listening on port " + port);
});
?
總結(jié)一下:
- 模塊已導(dǎo)入。
- 配置已完成。
- 已定義請求處理程序。
- 定義中間件是為了攔截受限端點(diǎn)。
- 服務(wù)器已啟動。
我們已經(jīng)完成了后端服務(wù)。為了讓多個客戶端可以使用它,您可以將這個簡單的服務(wù)器應(yīng)用程序部署到您的服務(wù)器上,或者也可以部署在 Heroku 中。項(xiàng)目根文件夾中有一個名為 Procfile
的文件。讓我們在 Heroku 中部署我們的服務(wù)。
Heroku 部署
您可以從此 GitHub 存儲庫克隆后端項(xiàng)目。
我不會討論如何在 Heroku 中創(chuàng)建應(yīng)用程序;如果您之前沒有創(chuàng)建過 Heroku 應(yīng)用程序,可以參考這篇文章來創(chuàng)建 Heroku 應(yīng)用程序。創(chuàng)建 Heroku 應(yīng)用程序后,您可以使用以下命令將目標(biāo)添加到當(dāng)前項(xiàng)目:
git remote add heroku <your_heroku_git_url>
現(xiàn)在您已經(jīng)克隆了一個項(xiàng)目并添加了一個目標(biāo)。在 git add
和 git commit
之后,您可以通過執(zhí)行 git push heroku master
將代碼推送到 Heroku。當(dāng)您成功推送項(xiàng)目時(shí),Heroku 將執(zhí)行 npm install
命令將依賴項(xiàng)下載到 Heroku 上的 temp
文件夾中。之后,它將啟動您的應(yīng)用程序,您可以使用 HTTP 協(xié)議訪問您的服務(wù)。
基于令牌的-auth-frontend
在前端項(xiàng)目中,您將看到一個 Angular 項(xiàng)目。在這里,我只提及前端項(xiàng)目中的主要部分,因?yàn)?Angular 不是一個教程可以涵蓋的內(nèi)容。
您可以從此 GitHub 存儲庫克隆該項(xiàng)目。在此項(xiàng)目中,您將看到以下文件夾結(jié)構(gòu):

我們擁有三個組件——注冊、配置文件和登錄——以及一個身份驗(yàn)證服務(wù)。
您的app.component.html 如下所示:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Bootstrap demo</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">
</head>
<body>
<nav class="navbar navbar-expand-lg bg-body-tertiary">
<div class="container-fluid">
<a class="navbar-brand" href="#">Home</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item"><a class="nav-link" routerLink="/profile">Me</a></li>
<li class="nav-item"><a class="nav-link" routerLink="/login">Signin</a></li>
<li class="nav-item"><a class="nav-link" routerLink="/signup">Signup</a></li>
<li class="nav-item"><a class="nav-link" (click)="logout()">Logout</a></li>
</ul>
</div>
</div>
</nav>
<div class="container">
<router-outlet></router-outlet>
</div>
</body>
</html>
?
在主組件文件中,<router-outlet></router-outlet>
?定義各個組件的路由。
在 auth.service.ts 文件中,我們定義 AuthService
類,該類通過 API 調(diào)用來處理身份驗(yàn)證,以登錄、驗(yàn)證 Node.js 應(yīng)用程序的 API 端點(diǎn)。
import { Injectable } from '@angular/core';
import { HttpClient,HttpHeaders } from '@angular/common/http';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class AuthService {
private apiUrl = 'your_node_app_url';
public token: string ='';
constructor(private http: HttpClient) {
}
signin(username: string, password: string): Observable<any> {
const data = { username, password };
return this.http.post(`${this.apiUrl}/signin`, data);
}
authenticate(email: string, password: string): Observable<any> {
const data = { email, password };
console.log(data)
return this.http.post(`${this.apiUrl}/authenticate`, data)
.pipe(
tap((response:any) => {
this.token = response.data.token; // Store the received token
localStorage.setItem('token',this.token)
console.log(this.token)
})
);
}
profile(): Observable<any> {
const headers = this.createHeaders();
return this.http.get(`${this.apiUrl}/me`,{ headers });
}
private createHeaders(): HttpHeaders {
let headers = new HttpHeaders({
'Content-Type': 'application/json',
});
if (this.token) {
headers = headers.append('Authorization', `Bearer ${this.token}`);
}
return headers;
}
logout(): void {
localStorage.removeItem('token');
}
}
在 authenticate()
方法中,我們向 API 發(fā)送 POST 請求并對用戶進(jìn)行身份驗(yàn)證。從響應(yīng)中,我們提取令牌并將其存儲在服務(wù)的 this.token
屬性和瀏覽器的 localStorage
中,然后將響應(yīng)作為 Observable
返回。
在 profile()
方法中,我們通過在 Authorization 標(biāo)頭中包含令牌來發(fā)出 GET 請求以獲取用戶詳細(xì)信息。
createHeaders()
方法在發(fā)出經(jīng)過身份驗(yàn)證的 API 請求時(shí)創(chuàng)建包含身份驗(yàn)證令牌的 HTTP 標(biāo)頭。當(dāng)用戶擁有有效令牌時(shí),它會添加一個授權(quán)標(biāo)頭。該令牌允許后端 API 對用戶進(jìn)行身份驗(yàn)證。
如果身份驗(yàn)證成功,用戶令牌將存儲在本地存儲中以供后續(xù)請求使用。該令牌也可供所有組件使用。如果身份驗(yàn)證失敗,我們會顯示一條錯誤消息。
不要忘記將服務(wù) URL 放入上面代碼中的 baseUrl
中。當(dāng)您將服務(wù)部署到 Heroku 時(shí),您將獲得類似 appname.herokuapp.com
的服務(wù) URL。在上面的代碼中,您將設(shè)置 var baseUrl = "appname.herokuapp.com"
。
注銷功能從本地存儲中刪除令牌。
在 signup.component.ts
文件中,我們實(shí)現(xiàn)了 signup ()
方法,該方法獲取用戶提交的電子郵件和密碼并創(chuàng)建一個新用戶。
import { Component } from '@angular/core';
import { AuthService } from '../auth.service';
@Component({
selector: 'app-signup',
templateUrl: './signup.component.html',
styleUrls: ['./signup.component.css']
})
export class SignupComponent {
password: string = '';
email: string = '';
constructor(private authService:AuthService){}
signup(): void {
this.authService.signin(this.email, this.password).subscribe(
(response) => {
// success response
console.log('Authentication successful', response);
},
(error) => {
// error response
console.error('Authentication error', error);
}
);
}
}
?
login.component.ts 文件看起來與注冊組件類似。
?
import { Component } from '@angular/core';
import { AuthService } from '../auth.service';
@Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.css']
})
export class LoginComponent {
email: string = '';
password: string = '';
constructor(private authService: AuthService) {}
login(): void {
this.authService.authenticate(this.email, this.password).subscribe(
(response) => {
// success response
console.log('Signin successful', response);
},
(error) => {
// error response
console.error('Signin error', error);
}
);
}
}
配置文件組件使用用戶令牌來獲取用戶的詳細(xì)信息。每當(dāng)您向后端的服務(wù)發(fā)出請求時(shí),都需要將此令牌放入標(biāo)頭中。 profile.component.ts 如下所示:
import { Component } from '@angular/core';
import { AuthService } from '../auth.service';
@Component({
selector: 'app-profile',
templateUrl: './profile.component.html',
styleUrls: ['./profile.component.css']
})
export class ProfileComponent {
myDetails: any;
constructor(private authService: AuthService) { }
ngOnInit(): void {
this.getProfileData();
}
getProfileData(): void {
this.authService.me().subscribe(
(response: any) => {
this.myDetails = response;
console.log('User Data:', this.myDetails);
},
(error: any) => {
console.error('Error retrieving profile data');
}
);
}
?
在上面的代碼中,每個請求都會被攔截,并在標(biāo)頭中放入授權(quán)標(biāo)頭和值。然后,我們將用戶詳細(xì)信息傳遞到 profile.component.html 模板。
<h2>User profile </h2>
<div class="row">
<div class="col-lg-12">
<p>{{myDetails.data.id}}</p>
<p>{{myDetails.data.email}}</p>
</div>
</div>
最后,我們在 app.routing.module.ts 中定義路由。
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { LoginComponent } from './login/login.component';
import { ProfileComponent } from './profile/profile.component';
import { SignupComponent } from './signup/signup.component';
const routes: Routes = [
{path:'signup' , component:SignupComponent},
{path:'login' , component:LoginComponent},
{ path: 'profile', component: ProfileComponent },
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
?
從上面的代碼中您可以很容易地理解,當(dāng)您轉(zhuǎn)到/時(shí),將呈現(xiàn)app.component.html頁面。另一個例子:如果您轉(zhuǎn)到/signup,則會呈現(xiàn)signup.component.html。這個渲染操作將在瀏覽器中完成,而不是在服務(wù)器端。
結(jié)論
基于令牌的身份驗(yàn)證系統(tǒng)可幫助您在開發(fā)獨(dú)立于客戶端的服務(wù)時(shí)構(gòu)建身份驗(yàn)證/授權(quán)系統(tǒng)。通過使用這項(xiàng)技術(shù),您將只需專注于您的服務(wù)(或 API)。
身份驗(yàn)證/授權(quán)部分將由基于令牌的身份驗(yàn)證系統(tǒng)作為服務(wù)前面的一層進(jìn)行處理。您可以從任何客戶端(例如網(wǎng)絡(luò)瀏覽器、Android、iOS 或桌面客戶端)訪問和使用服務(wù)。