Pengesahan ialah salah satu bahagian terpenting dalam mana-mana aplikasi web. Tutorial ini membincangkan sistem pengesahan berasaskan token dan cara ia berbeza daripada sistem log masuk tradisional. Pada penghujung tutorial ini, anda akan melihat demo berfungsi sepenuhnya yang ditulis dalam Angular dan Node.js.
Sebelum beralih kepada sistem pengesahan berasaskan token, mari kita lihat sistem pengesahan tradisional.
Setakat ini bagus. Aplikasi web berfungsi dengan baik dan dapat mengesahkan pengguna supaya mereka boleh mengakses titik akhir terhad. Tetapi apakah yang berlaku apabila anda ingin membangunkan pelanggan lain untuk aplikasi anda (seperti pelanggan Android)? Adakah anda dapat mengesahkan pelanggan mudah alih dan menyediakan kandungan terhad menggunakan aplikasi semasa anda? Seperti yang dinyatakan, tidak. Terdapat dua sebab utama untuk ini:
Dalam kes ini, anda memerlukan aplikasi bebas pelanggan.
Dalam pengesahan berasaskan token, kuki dan sesi tidak digunakan. Token akan digunakan untuk mengesahkan pengguna untuk setiap permintaan yang dibuat kepada pelayan. Mari kita reka bentuk semula senario pertama menggunakan pengesahan berasaskan token.
Ia akan menggunakan aliran kawalan berikut:
Dalam kes ini, kami tidak memulangkan sesi atau kuki, mahupun sebarang kandungan HTML. Ini bermakna kita boleh menggunakan seni bina ini untuk sebarang aplikasi khusus pelanggan. Anda boleh lihat seni bina seni bina di bawah:
Jadi apakah JWT ini?
JWT bermaksud JSON Web Token dan merupakan format token yang digunakan dalam pengepala kebenaran. Token ini membantu anda mereka bentuk komunikasi antara kedua-dua sistem dengan cara yang selamat. Untuk tujuan tutorial ini, kami akan merumuskan semula JWT sebagai "Token Pembawa". Token pembawa terdiri daripada tiga bahagian: pengepala, muatan dan tandatangan.
Anda boleh lihat skema JWT dan contoh token di bawah:
Anda tidak perlu melaksanakan penjana token pembawa kerana anda boleh menemui pakej mantap untuk banyak bahasa. Anda boleh lihat sebahagian daripadanya di bawah:
Node.js | https://github.com/auth0/node-jsonwebtoken |
PHP | http://github.com/firebase/php-jwt |
Jawa | http://github.com/auth0/java-jwt |
红宝石 | https://github.com/jwt/ruby-jwt |
.BERSIH | https://github.com/auth0/java-jwt |
Python | http://github.com/progrium/pyjwt/ |
Setelah merangkumi beberapa maklumat asas tentang pengesahan berasaskan token, kini kita boleh beralih kepada contoh praktikal. Lihat seni bina di bawah, kemudian kami akan menganalisisnya dengan lebih terperinci:
https://api.yourexampleapp.com
. Jika ramai orang menggunakan aplikasi itu, berbilang pelayan mungkin diperlukan untuk menyediakan operasi yang diminta. https://api.yourexampleapp.com
等服务发出的。如果很多人使用该应用程序,则可能需要多个服务器来提供请求的操作。https://api.yourexampleapp.com
发出请求时,负载均衡器首先会处理请求,然后会将客户端重定向到特定服务器。https://api.yourexampleapp.com
https://api.yourexampleapp.com
, pengimbang beban mula-mula mengendalikan permintaan dan kemudian mengubah hala klien ke pelayan tertentu. https://api.yourexampleapp.com
, aplikasi bahagian belakang memintas pengepala permintaan dan mengekstrak maklumat token daripada pengepala Kebenaran. Token ini akan digunakan untuk pertanyaan pangkalan data. Jika token ini sah dan mempunyai kebenaran yang diperlukan untuk mengakses titik akhir yang diminta, ia akan diteruskan. Jika tidak, ia akan mengembalikan kod respons 403 (menunjukkan status terlarang). Kelebihan
Perkhidmatan bebas pelanggan
Rangkaian Penghantaran Kandungan (CDN)
Dalam kebanyakan aplikasi web semasa, paparan dipaparkan pada bahagian belakang dan kandungan HTML dikembalikan kepada penyemak imbas. Logik bahagian hadapan bergantung pada kod bahagian belakang.
Tidak perlu mencipta kebergantungan sedemikian. Ini menimbulkan beberapa persoalan. Contohnya, jika anda bekerja dengan agensi reka bentuk yang melaksanakan HTML, CSS dan JavaScript bahagian hadapan, anda perlu memindahkan kod bahagian hadapan itu ke kod hujung belakang supaya beberapa operasi pemaparan atau pengisian boleh berlaku. Selepas beberapa ketika, kandungan HTML yang anda berikan akan sangat berbeza daripada apa yang dilaksanakan oleh agensi kod.
Sesi tanpa kuki (atau tiada CSRF)🎜 🎜CSRF adalah masalah utama dalam keselamatan rangkaian moden kerana ia tidak menyemak sama ada sumber permintaan itu boleh dipercayai. Untuk menyelesaikan masalah ini, gunakan kumpulan token untuk menghantar token ini pada setiap siaran borang. Dalam pengesahan berasaskan token, token digunakan dalam pengepala kebenaran, dan CSRF tidak mengandungi maklumat ini. 🎜
Apabila operasi membaca, menulis atau memadam sesi berlaku dalam aplikasi, ia menjalankan operasi fail dalam folder temp
sistem pengendalian, sekurang-kurangnya untuk kali pertama. Katakan anda mempunyai berbilang pelayan dan anda membuat sesi pada pelayan pertama. Apabila anda membuat permintaan lain dan permintaan anda jatuh ke pelayan lain, maklumat sesi tidak akan berada di sana dan anda akan mendapat respons "Tidak Dibenarkan". Saya tahu, anda boleh menyelesaikan masalah ini dengan sesi melekit. Walau bagaimanapun, dalam pengesahan berasaskan token, keadaan ini diselesaikan secara semula jadi. Tiada isu sesi melekit kerana token permintaan dipintas pada setiap permintaan pada mana-mana pelayan. temp
文件夹中进行文件操作,至少第一次是这样。假设您有多个服务器,并且在第一台服务器上创建了一个会话。当您发出另一个请求并且您的请求落入另一台服务器时,会话信息将不存在并且将得到“未经授权”的响应。我知道,你可以通过粘性会话来解决这个问题。然而,在基于令牌的认证中,这种情况自然就解决了。不存在粘性会话问题,因为请求令牌在任何服务器上的每个请求上都会被拦截。
这些是基于令牌的身份验证和通信的最常见优点。关于基于令牌的身份验证的理论和架构讨论就到此结束。是时候看一个实际例子了。
您将看到两个应用程序来演示基于令牌的身份验证:
在后端项目中,会有服务的实现,服务结果将是JSON格式。服务中没有返回视图。在前端项目中,将有一个用于前端 HTML 的 Angular 项目,然后前端应用程序将由 Angular 服务填充,以向后端服务发出请求。
在后端项目中,主要有三个文件:
就是这样!这个项目非常简单,因此您无需深入研究即可轻松理解主要概念。
{ "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
untuk MVC, body-parser
< strong> digunakan untuk mensimulasikan pemprosesan permintaan pos dalam Node.js, morgan
digunakan untuk pengelogan permintaan, mongoose
digunakan untuk menyambungkan rangka kerja ORM kami kepada MongoDB dan jsonwebtoken
digunakan untuk mencipta token JWT menggunakan model pengguna kami. Terdapat juga atribut yang dipanggil engines
yang menunjukkan bahawa projek itu dibuat menggunakan versi Node.js >= 0.10.0. Ini berguna untuk perkhidmatan PaaS seperti Heroku. Kami juga akan membincangkan topik ini dalam bahagian lain. 🎜
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 }); } });
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 }); } });
require
untuk memasukkan modul dalam projek anda. Pertama, kita perlu mengimport modul yang diperlukan ke dalam projek: 🎜
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 }); } });
3001
. Selepas itu, model Pengguna disertakan dan sambungan pangkalan data diwujudkan untuk beberapa operasi pengguna. Jangan lupa untuk menentukan pembolehubah persekitaran MONGO_URL
untuk URL sambungan pangkalan data. 🎜
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); } }
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 中部署我们的服务。
您可以从此 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 协议访问您的服务。
在前端项目中,您将看到一个 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); } ); } }
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 或桌面客户端)访问和使用服务。
Atas ialah kandungan terperinci Pengesahan berasaskan token dengan Angular dan Node. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!