如何在 NodeJS 中使用 JWT 对用户进行身份验证和授权

理解身份验证与授权:使用 JWT 构建 Node.js API

在计算机安全领域,身份验证和授权是两个至关重要的概念。 身份验证,顾名思义,是验证用户身份的过程。 它通常涉及用户提供凭据,例如用户名和密码,以证明其身份。 一旦验证成功,用户便会被视为已注册,并可能获得额外的访问权限。

这种机制也适用于您使用 Facebook 或 Google 帐户登录各种在线服务的情况。 您可以使用这些第三方平台来验证您的身份,而无需在每个服务中单独创建账户。

在本文中,我们将深入探讨如何使用 JWT(JSON Web 令牌)构建一个带有身份验证功能的 Node.js API。 我们将使用以下工具来实现这一目标:

  • Express.js
  • MongoDB 数据库
  • Mongoose
  • Dotenv
  • Bcrypt.js
  • Jsonwebtoken

身份验证 vs. 授权

什么是身份验证?

身份验证是一个识别用户的过程,它通常通过获取用户的凭据来完成,例如电子邮件地址、密码或令牌。 这些提供的凭据会与数据库或本地系统中存储的注册用户的凭据进行比较。 如果提供的凭据与存储的数据匹配,则身份验证过程成功,用户将被授权访问受保护的资源。

什么是授权?

授权发生在身份验证之后。 也就是说,每次授权尝试之前都必须有身份验证过程。 授权是指允许已验证用户访问系统或网站上的特定资源。 在本教程中,我们将实现允许已登录用户访问用户数据的授权机制。 如果用户尚未登录,他们将无法访问这些数据。

社交媒体平台(例如 Facebook 和 Twitter)是授权的典型例子。 如果您没有帐户,则无法访问社交媒体上的内容。

另一个授权的例子是订阅式内容。 您可以通过登录网站进行身份验证,但在您订阅服务之前,您将无法访问该内容。

先决条件

在继续之前,我们假设您已经掌握了 Javascript 和 MongoDB 的基本知识,并且对 Node.js 有相当程度的理解。

请确保您的本地机器上已经安装了 Node.js 和 npm。 要检查是否已安装,请打开命令提示符或终端并输入 node -vnpm -v。 这应该会显示 Node.js 和 npm 的版本号。

您的版本可能与图中显示的不同。 NPM 会随 Node.js 一起自动下载。 如果您尚未安装,请从 Node.js 官方网站 下载。

您还需要一个 IDE(集成开发环境)来编写代码。 本教程中,我们使用 VS Code 编辑器,当然您也可以使用任何其他您喜欢的 IDE。 如果您尚未安装 IDE,可以从 Visual Studio 官方网站 下载。 根据您的本地系统下载适合的版本。

项目设置

在您本地计算机的任何位置创建一个名为 nodeapi 的文件夹,然后使用 VS Code 打开它。 在 VS Code 终端中,通过输入以下命令来初始化 Node 包管理器:

npm init -y

确保您位于 nodeapi 目录中。

此命令将创建一个 package.json 文件,其中包含了我们将在项目中使用的所有依赖项。

现在,我们将下载前面提到的所有包。 在终端中键入并输入以下命令:

npm install express dotenv jsonwebtoken mongoose bcryptjs

现在,您的项目目录应该包含如下所示的文件和文件夹:

创建服务器并连接数据库

现在,创建一个名为 index.js 的文件,以及一个名为 config 的文件夹。 在 config 文件夹中,创建两个文件:conn.js 用于连接数据库,和 config.env 用于声明环境变量。 将以下代码写入相应的文件中:

index.js

const express = require('express');
const dotenv = require('dotenv');

// 配置 dotenv 文件,确保在其他使用环境变量的文件之前加载
dotenv.config({path:'./config/config.env'}); 

// 从 express 创建一个 app 实例
const app = express();

// 使用 express.json 中间件来解析 JSON 请求
app.use(express.json());


// 监听服务器端口
app.listen(process.env.PORT,() => {
    console.log(`服务器正在监听端口 ${process.env.PORT}`);
})
  

如果您使用了 dotenv,请在 index.js 文件中配置它,确保在其他使用环境变量的文件之前加载。

conn.js

const mongoose = require('mongoose');

mongoose.connect(process.env.URI, 
    { useNewUrlParser: true,
     useUnifiedTopology: true })
    .then((data) => {
        console.log(`数据库已连接到 ${data.connection.host}`)
})
  

config.env

URI = 'mongodb+srv://ghulamrabbani883:[email protected]/?retryWrites=true&w=majority'
PORT = 5000

这里我们使用了 MongoDB Atlas URI,您也可以使用本地 MongoDB 实例。

创建模型和路由

模型是 MongoDB 数据库中数据结构的描述,数据将存储为 JSON 文档。我们将使用 Mongoose schema 来创建模型。

路由是指应用程序如何响应客户端请求。我们将使用 express 路由功能来创建路由。

路由方法通常有两个参数。 第一个是路由路径,第二个是回调函数,用于定义该路由在客户端请求时会执行什么。

路由还可以将第三个参数作为中间件函数,例如在身份验证过程中。 当构建需要验证的 API 时,我们也将使用中间件函数来授权和验证用户。

现在,我们创建两个文件夹,分别命名为 routesmodels。 在 routes 内部,创建一个名为 userRoute.js 的文件。在 models 文件夹中,创建一个名为 userModel.js 的文件。 创建这些文件后,在各自的文件中写入以下代码:

userModel.js

const mongoose = require('mongoose');

// 使用 mongoose 创建 Schema
const userSchema = new mongoose.Schema({
    name: {
        type: String,
        required: true,
        minLength: [4, '姓名至少需要 4 个字符']
    },
    email: {
        type: String,
        required: true,
        unique: true,
    },
    password: {
        type: String,
        required: true,
        minLength: [8, '密码至少需要 8 个字符']
    },
    token: {
        type: String
    }
})

// 创建模型
const userModel = mongoose.model('user', userSchema);
module.exports = userModel;
  

userRoute.js

const express = require('express');
// 创建 express 路由实例
const route = express.Router();
// 导入 userModel
const userModel = require('../models/userModel');

// 创建注册路由
route.post('/register', (req, res) => {

})
// 创建登录路由
route.post('/login', (req, res) => {

})

// 创建用户路由以获取用户数据
route.get('/user', (req, res) => {

})

实现路由功能并创建 JWT 令牌

什么是 JWT?

JSON Web 令牌 (JWT) 是一个用于创建和验证令牌的 JavaScript 库。 它是一种开放标准,用于在客户端和服务器之间安全地传递信息。 我们将使用 JWT 的两个核心功能。 第一个函数是 sign 用于创建一个新的令牌,第二个函数是 verify 用于验证令牌。

什么是 Bcryptjs?

Bcryptjs 是由 Niels Provos 和 David Mazières 开发的哈希函数。 它使用哈希算法来对密码进行散列处理,使其不可逆。 我们将使用它的两个最常见的功能。 第一个 bcryptjs 函数是 hash 用于生成哈希值,第二个函数是 compare 函数用于比较密码。

实现路由功能

路由中的回调函数接受三个参数:req(请求)、res(响应) 和 next(下一个函数)。 next 参数是可选的,只有在需要使用中间件时才需要传递。 这些参数必须按照 req, resnext 的顺序传递。 现在,使用以下代码修改 userRoute.jsconfig.envindex.js 文件。

userRoute.js

// 导入所有必要的模块和库
const express = require('express');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');

// 创建 express 路由实例
const route = express.Router();
// 导入 userModel
const userModel = require('../models/userModel');

// 创建注册路由
route.post("/register", async (req, res) => {
    try {
        const { name, email, password } = req.body;
        // 检查请求数据是否为空
        if (!name || !email || !password) {
            return res.json({ message: '请填写所有详细信息' })
        }
        // 检查用户是否已存在
        const userExist = await userModel.findOne({ email: req.body.email });
        if (userExist) {
            return res.json({ message: '该电子邮件地址已被注册' })
        }
        // 对密码进行散列
        const salt = await bcrypt.genSalt(10);
        const hashPassword = await bcrypt.hash(req.body.password, salt);
        req.body.password = hashPassword;
        const user = new userModel(req.body);
        await user.save();
        const token = await jwt.sign({ id: user._id }, process.env.SECRET_KEY, {
            expiresIn: process.env.JWT_EXPIRE,
        });
        return res.cookie({ 'token': token }).json({ success: true, message: '用户注册成功', data: user })

    } catch (error) {
        return res.json({ error: error });
    }

})
// 创建登录路由
route.post('/login', async (req, res) => {
    try {
        const { email, password } = req.body;
        // 检查请求数据是否为空
        if (!email || !password) {
            return res.json({ message: '请填写所有详细信息' })
        }
        // 检查用户是否存在
        const userExist = await userModel.findOne({ email: req.body.email });
        if (!userExist) {
            return res.json({ message: '错误的凭据' })
        }
        // 检查密码是否匹配
        const isPasswordMatched = await bcrypt.compare(password, userExist.password);
        if (!isPasswordMatched) {
            return res.json({ message: '错误的密码' });
        }
        const token = await jwt.sign({ id: userExist._id }, process.env.SECRET_KEY, {
            expiresIn: process.env.JWT_EXPIRE,
        });
        return res.cookie({ "token": token }).json({ success: true, message: '登录成功' })

    } catch (error) {
        return res.json({ error: error });
    }

})

// 创建用户路由以获取用户数据
route.get('/user', async (req, res) => {
    try {
        const user = await userModel.find();
        if (!user) {
            return res.json({ message: '未找到用户' })
        }
        return res.json({ user: user })
    } catch (error) {
        return res.json({ error: error });
    }
})

module.exports = route;

如果您使用了 async 函数,请务必使用 try-catch 块,否则可能会抛出未处理的 Promise 拒绝错误。

config.env

URI = 'mongodb+srv://ghulamrabbani883:[email protected]/?retryWrites=true&w=majority'
PORT = 5000
SECRET_KEY = KGGK>HKHVHJVKBKJKJBKBKHKBMKHB
JWT_EXPIRE = 2d

index.js

const express = require('express');
const dotenv = require('dotenv');

// 配置 dotenv 文件,确保在其他使用环境变量的文件之前加载
dotenv.config({ path: './config/config.env' });
require('./config/conn');
// 从 express 创建一个 app 实例
const app = express();
const route = require('./routes/userRoute');

// 使用 express.json 中间件来解析 JSON 请求
app.use(express.json());
// 使用路由
app.use('/api', route);

// 监听服务器端口
app.listen(process.env.PORT, () => {
    console.log(`服务器正在监听端口 ${process.env.PORT}`);
})

创建中间件以验证用户

什么是中间件?

中间件是一个在请求-响应周期中可以访问请求、响应对象和下一个函数的函数。 当函数执行完成时,会调用下一个函数。 正如我们上面提到的,当您需要在执行完当前操作后执行另一个回调函数或中间件函数时,请使用 next()

现在,创建一个名为 middleware 的文件夹,并在其中创建一个名为 auth.js 的文件,然后将以下代码写入该文件:

auth.js

const userModel = require('../models/userModel');
const jwt = require('jsonwebtoken');
const isAuthenticated = async (req, res, next) => {
    try {
        const { token } = req.cookies;
        if (!token) {
            return next('请先登录以访问数据');
        }
        const verify = await jwt.verify(token, process.env.SECRET_KEY);
        req.user = await userModel.findById(verify.id);
        next();
    } catch (error) {
        return next(error);
    }
}

module.exports = isAuthenticated;

现在,我们需要安装 cookie-parser 库,以便在您的应用程序中配置 cookie 解析器。 cookie-parser 帮助您访问存储在 cookie 中的令牌。 如果您没有在 Node.js 应用程序中配置 cookie-parser,您将无法从请求对象的标头访问 cookie。 现在,在终端中输入以下命令以下载 cookie-parser

npm i cookie-parser

现在,您已经安装了 cookie-parser。 通过修改 index.js 文件并将中间件添加到 /user/ 路由来配置您的应用程序。

index.js 文件

const cookieParser = require('cookie-parser');
const express = require('express');
const dotenv = require('dotenv');

// 配置 dotenv 文件,确保在其他使用环境变量的文件之前加载
dotenv.config({path:'./config/config.env'}); 
require('./config/conn');
// 从 express 创建一个 app 实例
const app = express();
const route = require('./routes/userRoute');

// 使用 express.json 中间件来解析 JSON 请求
app.use(express.json());
// 配置 cookie-parser
app.use(cookieParser()); 

// 使用路由
app.use('/api', route);

// 监听服务器端口
app.listen(process.env.PORT,() => {
    console.log(`服务器正在监听端口 ${process.env.PORT}`);
})

userRoute.js

// 导入所有必要的模块和库
const express = require('express');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const isAuthenticated = require('../middleware/auth');

// 创建 express 路由实例
const route = express.Router();
// 导入 userModel
const userModel = require('../models/userModel');


// 创建用户路由以获取用户数据
route.get('/user', isAuthenticated, async (req, res) => {
    try {
        const user = await userModel.find();
        if (!user) {
            return res.json({ message: '未找到用户' })
        }
        return res.json({ user: user })
    } catch (error) {
        return res.json({ error: error });
    }
})

module.exports = route;

只有在用户登录后才能访问 /user 路由。

在 Postman 上检查 API

在检查 API 之前,您需要修改 package.json 文件。 添加以下代码行:

  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node index.js",
    "dev": "nodemon index.js"
  },

您可以通过键入 npm start 来启动服务器,但它只会运行一次。 要在更改文件时保持服务器运行,您需要 nodemon。 在终端中输入以下命令以下载它:

npm install -g nodemon

-g 标志将在您的本地系统上全局下载 nodemon。 您不必为每个新项目重复下载它。

要运行服务器,请在终端中键入 npm run dev。您将看到类似如下的输出:

现在,您的代码已经完成,服务器正常运行,可以打开 Postman 进行测试了。

什么是 Postman?

Postman 是一款用于设计、构建、开发和测试 API 的软件工具。

如果您尚未在计算机上下载 Postman,请从 Postman 官方网站 下载。

现在,打开 Postman 并创建一个名为 nodeAPItest 的集合,并在其中创建三个请求:注册、登录和用户。 您应该有以下文件:

当您将 JSON 数据发送到 localhost:5000/api/register 时,您将获得以下结果:

由于我们在注册期间也创建了令牌并将其保存到 cookie 中,因此您可以在请求 localhost:5000/api/user 路由时获得用户详细信息。 您可以在 Postman 上检查其余请求。

如果您需要完整的代码,可以从我的 GitHub 账号 下载。

结论

在本教程中,我们学习了如何使用 JWT 令牌为 Node.js API 应用身份验证。 我们还授权用户访问用户数据。

祝您编程愉快!