使用Angular和Node进行基于令牌的身份验证
身份验证是任何 Web 应用程序中最重要的部分之一。本教程讨论基于令牌的身份验证系统以及它们与传统登录系统的区别。在本教程结束时,您将看到一个用 Angular 和 Node.js 编写的完整工作演示。
传统身份验证系统
在继续基于令牌的身份验证系统之前,让我们先看一下传统的身份验证系统。
- 用户在登录表单中提供用户名和密码,然后点击登录。
- 发出请求后,通过查询数据库在后端验证用户。如果请求有效,则使用从数据库中获取的用户信息创建会话,然后在响应头中返回会话信息,以便将会话ID存储在浏览器中。
- 提供用于访问应用程序中受限端点的会话信息。
- 如果会话信息有效,则让用户访问指定端点,并使用呈现的 HTML 内容进行响应。
到目前为止一切都很好。 Web 应用程序运行良好,并且能够对用户进行身份验证,以便他们可以访问受限端点。但是,当您想为您的应用程序开发另一个客户端(例如 Android 客户端)时会发生什么情况?您是否能够使用当前的应用程序来验证移动客户端并提供受限制的内容?就目前情况而言,没有。造成这种情况的主要原因有两个:
- 会话和 Cookie 对于移动应用程序没有意义。您无法与移动客户端共享在服务器端创建的会话或 Cookie。
- 在当前应用程序中,返回呈现的 HTML。在移动客户端中,您需要包含 JSON 或 XML 等内容作为响应。
在这种情况下,您需要一个独立于客户端的应用程序。
基于令牌的身份验证
在基于令牌的身份验证中,不会使用 cookie 和会话。令牌将用于对向服务器发出的每个请求进行用户身份验证。让我们使用基于令牌的身份验证重新设计第一个场景。
它将使用以下控制流程:
- 用户在登录表单中提供用户名和密码,然后点击登录。
- 发出请求后,通过在数据库中查询来验证后端的用户。如果请求有效,则使用从数据库获取的用户信息创建令牌,然后在响应标头中返回该信息,以便我们可以将令牌浏览器存储在本地存储中。
- 在每个请求标头中提供令牌信息,以访问应用程序中的受限端点。
- 如果从请求标头信息中获取的令牌有效,则让用户访问指定端点,并使用 JSON 或 XML 进行响应。
在这种情况下,我们没有返回会话或cookie,也没有返回任何HTML内容。这意味着我们可以将此架构用于任何客户端的特定应用程序。您可以看到下面的架构架构:
那么这个 JWT 是什么?
JWT
JWT 代表 JSON Web 令牌,是授权标头中使用的令牌格式。该令牌可帮助您以安全的方式设计两个系统之间的通信。出于本教程的目的,我们将 JWT 重新表述为“不记名令牌”。不记名令牌由三部分组成:标头、负载和签名。
- 标头是令牌中保存令牌类型和加密方法的部分,也是使用 Base-64 进行加密的。
- 有效负载包含信息。您可以输入任何类型的数据,例如用户信息、产品信息等,所有这些数据都使用 Base-64 加密进行存储。
- 签名由标头、负载和密钥的组合组成。密钥必须安全地保存在服务器端。
您可以在下面看到 JWT 架构和示例令牌:
您不需要实现不记名令牌生成器,因为您可以找到多种语言的已建立包。您可以在下面看到其中一些:
Node.js | https://github.com/auth0/node-jsonwebtoken |
PHP | http://github.com/firebase/php-jwt |
Java | http://github.com/auth0/java-jwt |
红宝石 | https://github.com/jwt/ruby-jwt |
.NET | https://github.com/auth0/java-jwt |
Python | http://github.com/progrium/pyjwt/ |
一个实际示例
介绍了有关基于令牌的身份验证的一些基本信息后,我们现在可以继续讨论一个实际示例。看一下下面的架构,然后我们将更详细地分析它:
- 多个客户端(例如网络应用程序或移动客户端)出于特定目的向 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>
在主组件文件中,
在 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); } ); } }
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 或桌面客户端)访问和使用服务。
使用Angular和Node进行基于令牌的身份验证的详细内容,更多请关注红帽云邮其它相关文章!