|
一個實際範例
介紹了有關基於令牌的身份驗證的一些基本資訊後,我們現在可以繼續討論一個實際範例。看看下面的架構,然後我們將更詳細地分析它:
#
- 多個用戶端(例如網路應用程式或行動用戶端)出於特定目的向 API 發出請求。
- 要求是向
https://api.yourexampleapp.com
等服務發出的。如果很多人使用該應用程序,則可能需要多個伺服器來提供請求的操作。
- 這裡,負載平衡器用於平衡請求,以最適合後端的應用程式伺服器。當您向
https://api.yourexampleapp.com
發出請求時,負載平衡器會先處理請求,然後會將客戶端重新導向到特定伺服器。
- 有一個應用程序,並且該應用程式部署到多台伺服器(server-1、server-2、...、server-n)。每當向
https://api.yourexampleapp.com
發出請求時,後端應用程式都會攔截請求標頭並從授權標頭中提取令牌資訊。將使用此令牌進行資料庫查詢。如果此令牌有效且具有存取所請求端點所需的權限,則它將繼續。如果沒有,它將傳回 403 回應代碼(表示禁止狀態)。
優點
基於令牌的身份驗證具有解決嚴重問題的多個優點。以下是其中的一些:
獨立於客戶端的服務
在基於令牌的身份驗證中,令牌透過請求標頭傳輸,而不是將身份驗證資訊保留在會話或 cookie 中。這意味著沒有狀態。您可以從任何類型的可以發出 HTTP 請求的客戶端向伺服器發送請求。
內容傳遞網路 (CDN)
在目前的大多數 Web 應用程式中,視圖在後端呈現,HTML 內容返回瀏覽器。前端邏輯依賴後端程式碼。
沒有必要建立這樣的依賴關係。這帶來了幾個問題。例如,如果您正在與實作前端 HTML、CSS 和 JavaScript 的設計機構合作,您需要將該前端程式碼遷移到後端程式碼中,以便進行一些渲染或填滿操作。一段時間後,您呈現的 HTML 內容將與程式碼機構實現的內容有很大不同。
在基於令牌的身份驗證中,您可以與後端程式碼分開開發前端專案。您的後端程式碼將傳回 JSON 回應,而不是渲染的 HTML,而且您可以將前端程式碼的縮小、gzip 版本放入 CDN 中。當您造訪網頁時,HTML 內容將從 CDN 提供,並且頁面內容將由 API 服務使用授權標頭中的令牌填充。
無 Cookie 會話(或無 CSRF)
CSRF 是現代網路安全的一個主要問題,因為它不會檢查請求來源是否可信。為了解決這個問題,使用令牌池在每個表單貼文上發送該令牌。在基於令牌的身份驗證中,令牌用於授權標頭,而 CSRF 不包含該資訊。
持久令牌儲存
當應用程式中進行會話讀取、寫入或刪除操作時,它會在作業系統的 temp
資料夾中進行檔案操作,至少第一次是這樣。假設您有多個伺服器,並且在第一台伺服器上建立了一個會話。當您發出另一個請求並且您的請求落入另一台伺服器時,會話資訊將不存在並且將得到「未經授權」的回應。我知道,你可以透過黏性會話來解決這個問題。然而,在基於令牌的認證中,這種情況自然就解決了。不存在黏性會話問題,因為請求令牌在任何伺服器上的每個請求上都會被攔截。
這些是基於令牌的身份驗證和通訊的最常見優點。關於基於令牌的身份驗證的理論和架構討論就到此結束。是時候看一個實際例子了。
範例應用程式
您將看到兩個應用程式來演示基於令牌的身份驗證:
- 基於令牌的身份驗證後端
- 基於令牌的身份驗證前端
在後端專案中,會有服務的實現,服務結果將是JSON格式。服務中沒有返回視圖。在前端專案中,將有一個用於前端 HTML 的 Angular 項目,然後前端應用程式將由 Angular 服務填充,以向後端服務發出請求。
基於令牌的身份驗證後端
在後端專案中,主要有三個檔案:
-
package.json 用於依賴管理。
-
models/User.js 包含一個使用者模型,用於對使用者進行資料庫操作。
-
server.js 用於專案引導和請求處理。
就是這樣!這個項目非常簡單,因此您無需深入研究即可輕鬆理解主要概念。
{
"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 服務很有用。我們還將在另一節中討論主題。
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const UserSchema = new Schema({
email: String,
password: String,
token: String
});
module.exports = mongoose.model('User', UserSchema);
我們說過我們將使用使用者模型有效負載產生令牌。這個模型幫助我們對MongoDB進行使用者操作。在User.js中,定義了使用者模式並使用貓鼬模型建立了使用者模型。該模型已準備好進行資料庫操作。
我們的依賴關係已經定義,我們的使用者模型也已經定義,所以現在讓我們將所有這些組合起來建構一個用於處理特定請求的服務。
// 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
在專案中包含模組。首先,我們需要將必要的模組導入到專案中:
const port = process.env.PORT || 3001;
const User = require('./models/User');
// Connect to DB
mongoose.connect(process.env.MONGO_URL);
我們的服務將透過特定連接埠提供服務。如果系統環境變數中定義了任何連接埠變量,則可以使用它,或者我們定義了連接埠 3001
。之後,包含了User模型,並建立了資料庫連接,以進行一些使用者操作。不要忘記為資料庫連接 URL 定義一個環境變數 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 進行了一些配置來模擬 Node 中的 HTTP 請求處理。我們允許來自不同網域的請求,以便開發獨立於客戶端的系統。如果您不允許這樣做,您將在網頁瀏覽器中觸發 CORS(跨來源請求共用)錯誤。
-
Access-Control-Allow-Origin
允许所有域。
- 您可以向此服务发送
POST
和 GET
请求。
-
X-Requested-With
和 content-type
标头是允许的。
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
});
}
});
我们已经导入了所有必需的模块并定义了我们的配置,所以现在是时候定义请求处理程序了。在上面的代码中,每当你使用用户名和密码向 /authenticate
发出 POST
请求时,你都会得到一个 JWT
令牌。首先,使用用户名和密码处理数据库查询。如果用户存在,则用户数据将与其令牌一起返回。但是如果没有与用户名和/或密码匹配的用户怎么办?
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
});
}
});
当您使用用户名和密码向 /signin
发出 POST
请求时,将使用发布的用户信息创建一个新用户。在 14th
行,您可以看到使用 jsonwebtoken
模块生成了一个新的 JSON 令牌,该令牌已分配给 jwt
变量。认证部分没问题。如果我们尝试访问受限端点怎么办?我们如何设法访问该端点?
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
});
}
});
当您向 /me
发出 GET
请求时,您将获得当前用户信息,但为了继续请求的端点,确保Authorized
函数将被执行。
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);
}
}
在该函数中,拦截请求头,并提取authorization
头。如果此标头中存在承载令牌,则该令牌将分配给 req.token
以便在整个请求中使用,并且可以使用 next( )
。如果令牌不存在,您将收到 403(禁止)响应。让我们回到处理程序 /me
,并使用 req.token
使用此令牌获取用户数据。每当您创建新用户时,都会生成一个令牌并将其保存在数据库的用户模型中。这些令牌是独一无二的。
对于这个简单的项目,我们只有三个处理程序。之后,您将看到:
process.on('uncaughtException', function(err) {
console.log(err);
});
如果发生错误,Node.js 应用程序可能会崩溃。使用上面的代码,可以防止崩溃,并在控制台中打印错误日志。最后,我们可以使用以下代码片段启动服务器。
// Start Server
app.listen(port, function () {
console.log( "Express server listening on port " + port);
});
总结一下:
- 模块已导入。
- 配置已完成。
- 已定义请求处理程序。
- 定义中间件是为了拦截受限端点。
- 服务器已启动。
我们已经完成了后端服务。为了让多个客户端可以使用它,您可以将这个简单的服务器应用程序部署到您的服务器上,或者也可以部署在 Heroku 中。项目根文件夹中有一个名为 Procfile
的文件。让我们在 Heroku 中部署我们的服务。
Heroku 部署
您可以从此 GitHub 存储库克隆后端项目。
我不会讨论如何在 Heroku 中创建应用程序;如果您之前没有创建过 Heroku 应用程序,可以参考这篇文章来创建 Heroku 应用程序。创建 Heroku 应用程序后,您可以使用以下命令将目标添加到当前项目:
git remote add heroku <your_heroku_git_url>
现在您已经克隆了一个项目并添加了一个目标。在 git add
和 git commit
之后,您可以通过执行 git push heroku master
将代码推送到 Heroku。当您成功推送项目时,Heroku 将执行 npm install
命令将依赖项下载到 Heroku 上的 temp
文件夹中。之后,它将启动您的应用程序,您可以使用 HTTP 协议访问您的服务。
基于令牌的-auth-frontend
在前端项目中,您将看到一个 Angular 项目。在这里,我只提及前端项目中的主要部分,因为 Angular 不是一个教程可以涵盖的内容。
您可以从此 GitHub 存储库克隆该项目。在此项目中,您将看到以下文件夹结构:
我们拥有三个组件——注册、配置文件和登录——以及一个身份验证服务。
您的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 调用来处理身份验证,以登录、验证 Node.js 应用程序的 API 端点。
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 发送 POST 请求并对用户进行身份验证。从响应中,我们提取令牌并将其存储在服务的 this.token
属性和浏览器的 localStorage
中,然后将响应作为 Observable
返回。
在 profile()
方法中,我们通过在 Authorization 标头中包含令牌来发出 GET 请求以获取用户详细信息。
createHeaders()
方法在发出经过身份验证的 API 请求时创建包含身份验证令牌的 HTTP 标头。当用户拥有有效令牌时,它会添加一个授权标头。该令牌允许后端 API 对用户进行身份验证。
如果身份验证成功,用户令牌将存储在本地存储中以供后续请求使用。该令牌也可供所有组件使用。如果身份验证失败,我们会显示一条错误消息。
不要忘记将服务 URL 放入上面代码中的 baseUrl
中。当您将服务部署到 Heroku 时,您将获得类似 appname.herokuapp.com
的服务 URL。在上面的代码中,您将设置 var baseUrl = "appname.herokuapp.com"
。
注销功能从本地存储中删除令牌。
在 signup.component.ts
文件中,我们实现了 signup ()
方法,该方法获取用户提交的电子邮件和密码并创建一个新用户。
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);
}
);
}
}
配置文件组件使用用户令牌来获取用户的详细信息。每当您向后端的服务发出请求时,都需要将此令牌放入标头中。 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');
}
);
}
在上面的代码中,每个请求都会被拦截,并在标头中放入授权标头和值。然后,我们将用户详细信息传递到 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 { }
从上面的代码中您可以很容易地理解,当您转到/时,将呈现app.component.html页面。另一个例子:如果您转到/signup,则会呈现signup.component.html。这个渲染操作将在浏览器中完成,而不是在服务器端。
结论
基于令牌的身份验证系统可帮助您在开发独立于客户端的服务时构建身份验证/授权系统。通过使用这项技术,您将只需专注于您的服务(或 API)。
身份验证/授权部分将由基于令牌的身份验证系统作为服务前面的一层进行处理。您可以从任何客户端(例如网络浏览器、Android、iOS 或桌面客户端)访问和使用服务。