学习Hapi验证、Bcrypt Hashing和JWT的作用

110 阅读8分钟

Hapi验证、Bcrypt Hashing和JWT的实际应用

一个网站的安全始于数据库中的干净数据。良好的做法是确保提交给数据库的信息是准确的,并对其进行过滤,以避免用冗余或不现实的记录来蒙蔽它。

这篇文章将带你了解如何使用Hapi来清理表单中提交的数据,并确保在提交数据到数据库之前进行正确的验证。

此外,我们将学习如何使用Bcrypt对密码进行散列,以避免向数据库提交明文密码字段。

这两个组件与Json Web Tokens ,以实现一个认证API,确保安全和干净的数据输入。

前提条件

这个项目的重点是后端,所以我们将使用Insomnia将请求发布到服务器上。此外,我们将建立一个后端API,以便于注册和验证用户的输入。

要跟上进度,你需要具备以下条件。

  • 在你的机器中安装[Insomnia]。
  • 在你的电脑上安装[Node.js]。
  • 一个合适的代码编辑器,最好是[VS Code]。

项目设置

首先,使用命令在所需的文件夹中设置一个开放的应用程序。

npm init -y

-y 标志会自动完成设置新项目时需要的其他依赖性。

我们将在这个项目中采用MVC架构。使用这种架构可以确保有组织的文件夹结构和容易调试的代码。

设置项目的文件夹结构如下所示。

Folder organization

安装依赖项

我们需要以下依赖项来使这个项目工作。

  • Express作为后端管理器。
  • JSON Web Tokens用于生成认证令牌。
  • Hapi Validation,在提交给数据库之前验证输入。
  • Bcryptjs用于散列和密码比较。
  • Dotenv用于配置环境变量。
  • Mongoose用于连接项目与MongoDB。
  • Body-parser,用于解析从请求中发送的请求体。

运行下面的命令来安装所有的依赖项。

npm install express mongoose jsonwebtokens @hapi/Joi bcryptjs dotenv body-parser

依赖性导入

为了导入已安装的依赖项,我们需要在应用程序的入口处添加以下片段;即index.js 文件。

// importing express
const express = require("express");
const app = express();

// import body parser
const bodyParser = require("body-parser");

// import dotenv
const dotenv = require("dotenv");

// import the databse connection object
const connectDB = require("./config/database");
dotenv.config({ path: "./config/config.env" });

// auth route
const authRoute = require("./routes/auth");

// posts route
const postRoute = require("./routes/posts");

// calling database connection function
connectDB();

// port
const PORT = process.env.PORT || 5000;
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use("/api/user", authRoute);
app.use("/api/posts", postRoute);

app.listen(process.env.PORT, () => {
	console.log("Running");
});

创建模型

我们需要为用户建立一个模型。用户模型有一个名字、电子邮件和密码。我们还包括创建用户时的日期。

models 文件夹中,创建一个名为User.js 的新文件,添加下面的片段。

const mongoose = require("mongoose");

const userSchema = new mongoose.Schema({
	name: {
		type: String, // type string
		required: true,
		min: 6, // min length
	},
	email: {
		type: String, // type string
		required: true,
		min: 5, // min length
		max: 255, // max length
	},
	password: {
		type: String, // type string
		required: true,
		max: 1024, // max password length
	},
	date: {
		type: Date,
		default: Date.now,
	},
});

module.exports = mongoose.model("User", userSchema);

连接到数据库

我们将使用MongoDB Atlas来保存我们的记录。

config 文件夹中,创建一个新文件并命名为config.env 。在这个文件中,我们将为项目定义我们的全局变量。

PORT = 5000 // environmental port where the project runs
MONGO_URI = 'YOUR MONGOBD CONNECTION URL' // mongodb connection url
AUTH_TOKEN_SECRET = any random string

AUTH_TOKEN_SECRET 是一个秘密,我们将在教程的后面与JSON Web Token一起使用。

在同一个文件夹中,创建一个名为database.js 的文件,然后添加下面的代码段来连接到数据库。

// import mongoose
const mongoose = require("mongoose");

// connect to database
const connectDatabase = async () => {
	try {
		const connection = await mongoose.connect(process.env.MONGO_URI, {
			useNewUrlParser: true,
			useUnifiedTopology: true,
			useFindAndModify: false,
		});
		console.log(`MongoDB Connected: ${conn.connection.host}`);
	} catch (err) {
		// log the error incase of any then exit execution
		console.error(err);
		process.exit(1);
	}
};

module.exports = connectDatabase;

验证输入

routes 文件夹中,创建一个新的文件用于验证。在该文件中,我们将有两个常数;用于注册和登录时的验证。

为验证过程添加下面的片段。

// import validation module
const Joi = require("@hapi/joi");

const userRegistrationValidation = (data) => {
	const schema = Joi.object({
		name: Joi.string().min(6).required(),
		email: Joi.string().min(6).required().email(),
		password: Joi.string().min(6).required(),
	});

	return schema.validate(data);
};

const userLoginValidation = (data) => {
	const schema = Joi.object({
		email: Joi.string().min(6).required().email(),
		password: Joi.string().min(6).required(),
	});

	return schema.validate(data);
};

module.exports.userRegistrationValidation = userRegistrationValidation;
module.exports.userLoginValidation = userLoginValidation;

传递的数据对象包含请求体中的信息。这些信息被用来对照预设的条件来执行验证。

验证完成后,这些函数被导出到前端使用,当表单数据被提交时,它们将被调用。

创建路由

这个项目将有三个路由。第一条路由用于将用户注册到数据库中,第二条路由将用于登录已注册的用户,最后一条路由将在用户登录后向其显示一个项目列表。

最后,posts 路由将是一个受保护的路由;只有由Json Web Tokens 产生的授权令牌识别的注册用户才能访问。

在路由文件夹中创建一个名为auth.js 的新文件,并添加以下片段。

const router = require("express").Router();
const User = require("../models/User");
const {
	userRegistrationValidation,
	userLoginValidation,
} = require("./validation");
const bcrypt = require("bcryptjs");
const jwt = require("jsonwebtoken");

// posting form data to login route
router.post("/login", async (req, res) => {
	const { error } = userLoginValidation(request.body);

	if (error) {
		return response.status(400).send(error.details[0].message);
	}

	// check user existence in the database
	const user = await User.findOne({ email: request.body.email });
	if (!user) {
		return response.status(400).send("Sorry email is not with our records");
	}

	// compare passwords
	const validUserPassword = await bcrypt.compare(
		request.body.password,
		user.password
	);
	if (!validUserPassword) {
		return response.status(400).send("Sorry the password is invalid");
	}

	// creating and assignikng token
	const token = jwt.sign({ _id: user._id }, process.env.AUTH_TOKEN_SECRET);
	response.header("authentication-token", token).send(token);
});

module.exports = router;

注册路由

这个路由将用户注册到数据库中。传递给该路由的数据将在userRegistrationValidation 中进行验证。

如果请求中存在任何错误;特别是来自验证的错误,服务器会将其发送给用户。

// posting data to register route
router.post("/register", async (request, response) => {
	const { error } = userRegistrationValidation(req.body);

	// send any error to the user incase of any
	if (error) {
		return response.status(400).send(error.details[0].message);
	}

	// check user existence in the database in the mongo db database
	const emailExists = await User.findOne({ email: request.body.email });
	if (emailExists) {
		return response.status(400).send("Email already in the database");
	}
});

如果没有错误,请求正文中的电子邮件将与所有数据库记录进行相似性检查。如果电子邮件是唯一的,我们就调用bcrypt ,对密码进行加密以保证安全。

// Hashing the passwords
const salt = bcrypt.genSaltSync(10);
const hashedPassword = bcrypt.hashSync(request.body.password, salt);

之后,一个新的用户实例被创建并保存到数据库中。

// creating a new user object
const user = new User({
	name: request.body.name,
	email: request.body.email,
	password: hashedPassword,
});

try {
	// saving the newly created user
	const savedUser = await user.save();
	response.send({ savedUser: user._id });
} catch (err) {
	console.log(err);
	response.status(400).send(err);
}

登录路线

登录路线接收请求体中的数据,并通过userLoginValidation 进行验证。

然后Hapi检查数据是否有错误。如果没有,数据库就会被查询到一个带有所提供的电子邮件的记录。

router.post("/login", async (request, response) => {
	const { error } = userLoginValidation(request.body);

	if (error) {
		return response.status(400).send(error.details[0].message);
	}

	// check user existence in the database
	const user = await User.findOne({ email: req.body.email });
	if (!user) {
		return response.status(400).send("Sorry email is not with our records");
	}
});

在下一步,我们使用bcrypt ,将请求体中提供的密码与数据库中的等效哈希值进行比较。如果密码匹配,用户就会被登录,并给他的JSON web token secretuserID

该令牌允许他们访问受保护的路由,因为该令牌被附加到每个后续请求的请求头中。

// make a comparison between entered password and the database password
const validUserPassword = await bcrypt.compare(
	request.body.password,
	user.password
);
if (!validUserPassword) {
	return response.status(400).send("Sorry the password is invalid");
}

// creating and assigning token
const token = jwt.sign({ _id: user._id }, process.env.AUTH_TOKEN_SECRET);
response.header("authentication-token", token).send(token);

令牌验证

我们需要验证令牌是否被传递到了请求头中;这样,只有经过认证的用户才能访问受保护的路由。

我们检查请求中是否有authentication token ,如果请求中没有令牌,则拒绝其访问受保护的路由。

// importing the jwt module
const jwt = require("jsonwebtoken");

module.exports = function (request, response, next) {
	// fetch the token from the request header
	const token = req.header("authentication-token");
	if (!token) {
		return response.status(400).send("Access denied!");
	}
};

但是,如果请求头中有令牌,我们就把用户标记为已验证,并允许他访问受保护的路由。

// verify the user
try {
	const verifiedUser = jwt.verify(token, process.env.AUTH_TOKEN_SECRET);
	request.user = verifiedUser;
	next();
} catch (error) {
	response.status(400).send("Invalid token");
}

保护路由

为了保护一个给定的路由,我们需要在请求前添加verify 方法,如下图所示。

// extracting the router module from the express
const router = require("express").Router();

// verify
const verify = require("./verifyToken");

// method called in the request
router.get("/", verify, (request, response) => {
	response.json({
		posts: {
			title: "Very first post",
			body: "Random post you should not even see",
		},
	});
});

module.exports = router;

上面的代码段确保只有经过验证的用户才能访问这些帖子。

测试项目

我们需要运行nodemon index 命令来测试这个项目。然后启动开发服务器,试试Insomnia 中的端点。Postman也可以在这里工作。

测试验证

让我们尝试使用比userRegistrationValidation 中指定的长度更短的密码和或电子邮件,看看我们的验证是否有效。

我们将首先导航到register 路线。

Password check

如果我们使用错误的电子邮件,我们会得到一个验证错误,如下图所示。

Email check

然而,当所有的字段都填写正确并且验证通过后,用户就被添加到数据库中。

然后返回user-id ,如图所示。

User saved

测试受保护的路由访问

当我们尝试在没有登录的情况下访问posts 路由时,我们被拒绝访问。

Access denied

然而,当登录后,我们得到了一个认证令牌,我们将其添加到请求的标题中以访问受保护的路由。

Authentication token

View protected route

总结

本教程教会了我们如何使用Hapi 来验证用户输入,用bcrypt 来加密密码,以及JWT认证。我们使用这三者构建了一个认证API,并测试了该应用。

这个教程应该可以让你开始为你的网络项目进行数据清洗和保护数据。