node.js jwt
先决条件 (Prerequisites)
In order to follow along this article, you need to have a basic understanding of express and node.js.
为了继续阅读本文,您需要对express和node.js有基本的了解。
智威汤逊简介 (Introduction to JWT)
JWT stands for JSON Web Token. It is a compact and secure way of transmitting JSON data. JWT can be easily verified because it is signed digitally. JWT can be used for authorization or exchanging information.
JWT代表JSON Web令牌。 这是一种传输JSON数据的紧凑且安全的方式。 JWT是经过数字签名的,因此很容易验证。 JWT可用于授权或交换信息。
In this article, we will be using JWT for authorization.
在本文中,我们将使用JWT进行授权。
让我们开始工作! (Let’s start working!)
Open up the terminal in main project directory and write the following command:
在主项目目录中打开终端,并输入以下命令:
npm init -y
The -y
flag here will help to set every value to default.
此处的-y
标志将有助于将每个值设置为默认值。
Let’s now install the packages we need. For that, let’s write the following command in the terminal of our main project directory:
现在安装所需的软件包。 为此,让我们在主项目目录的终端中编写以下命令:
npm i express jsonwebtoken express-validator mongoose bcrypt dotenv
With the above command, we have installed express
, jsonwebtoken
, express-validator
, mongoose
, bcrypt
, dotenv
. You obviously know what express
does. Here, jsonwebtoken
is used to generate and verify JWT. We will be using it in a while. The express-validator
package will help us to validate the data. Similarly, dotenv
will help us access .env file.
有了上面的命令,我们已经安装了express
, jsonwebtoken
, express-validator
, mongoose
, bcrypt
, dotenv
。 您显然知道express
是做什么的。 在这里, jsonwebtoken
用于生成和验证JWT。 我们将在一段时间内使用它。 express-validator
软件包将帮助我们验证数据。 同样, dotenv
将帮助我们访问.env文件。
Let’s now create a file called .env in our main project directory and write the following inside the file:
现在,在主项目目录中创建一个名为.env的文件,并将以下内容写入文件:
MONGO_URI= // add mongo URI here
SECRET= // add secret here
The MONGO_URI
should contain the URL of MongoDB database. The SECRET
should contain some secret value. The SECRET
can be any random set of strings.
MONGO_URI
应该包含MongoDB数据库的URL。 SECRET
应该包含一些秘密值。 SECRET
可以是任意随机的字符串集。
After entering the values, let’s create a file called index.js inside of the main project directory and write the following lines of code:
输入值之后,让我们在主项目目录中创建一个名为index.js的文件,并编写以下代码行:
require("dotenv").config();
const express = require("express");
const mongoose = require("mongoose");
const AuthRoute = require("./routes/Auth");
const app = express();
app.use(express.json());
app.use("/api/auth/", AuthRoute);
const PORT = process.env.PORT || 5000;
app.listen(PORT, () => console.log(`listening on ${PORT}`));
mongoose
.connect(process.env.MONGO_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
})
.then(() => console.log("connected to mongo database"))
.catch((e) => console.error(e));
In the above code, the first few lines are just imports. At line 1, we have imported and configured dotenv
. At line 3, we imported express
. Then, at line 4, we imported mongoose
. At line 6, we imported AuthRoute
. At line 8, we initialized our express app. Then, at line 10, we used express.json()
middleware. This middleware helps us recognize JSON data received from the client. At line 11, we created a route for api/auth and used AuthRoute
as the router for the route. At line 13, we created a constant called PORT
. The PORT
stores the value of PORT
from the environment variable, if it exists, else it stores 5000
. At line 14, we started our server at PORT
. From line 16 to 22, we connected to the MongoDB database. At line 17, we entered the value of MONGO_URI
to make the connection. After the connection is made, we console.log
it (at line 21). If there occurs any error, we log the error at line 22.
在上面的代码中,前几行只是导入。 在第1行,我们导入并配置了dotenv
。 在第3行,我们导入了express
。 然后,在第4行,我们进口了mongoose
。 在第6行,我们导入了AuthRoute
。 在第8行,我们初始化了Express应用程序。 然后,在第10行,我们使用express.json()
中间件。 该中间件有助于我们识别从客户端接收的JSON数据。 在第11行,我们为api / auth创建了一条路由,并使用AuthRoute
作为该路由的路由器。 在第13行,我们创建了一个名为PORT
的常量。 PORT
存储环境变量中PORT
的值(如果存在),否则存储5000
。 在第14行,我们在PORT
启动了服务器。 从第16到22行,我们连接到MongoDB数据库。 在第17行,我们输入MONGO_URI
的值进行连接。 建立连接后,我们进行console.log
(第21行)。 如果发生任何错误,我们将在第22行记录该错误。
Let’s now work on the routes. For that, let’s create a directory called routes inside of our main project directory. Then, create a file called Auth.js inside of the routes directory. Let’s now open the file Auth.js and write the following lines of code in the file:
现在开始在路线上工作。 为此,让我们在主项目目录中创建一个名为路由的目录。 然后,在路由目录中创建一个名为Auth.js的文件。 现在让我们打开文件Auth.js,并在文件中编写以下代码行:
const express = require("express");
const { check, validationResult } = require("express-validator");
const { login, signup, me } = require("../controller/AuthController");
const router = express.Router();
// /api/auth/signup
router.post(
"/signup",
[
check("name")
.isLength({ min: 3 })
.withMessage("the name must have minimum length of 3")
.trim(),
check("email")
.isEmail()
.withMessage("invalid email address")
.normalizeEmail(),
check("password")
.isLength({ min: 8, max: 15 })
.withMessage("your password should have min and max length between 8-15")
.matches(/\d/)
.withMessage("your password should have at least one number")
.matches(/[!@#$%^&*(),.?":{}|<>]/)
.withMessage("your password should have at least one sepcial character"),
check("confirmPassword").custom((value, { req }) => {
if (value !== req.body.password) {
throw new Error("confirm password does not match");
}
return true;
}),
],
(req, res, next) => {
const error = validationResult(req).formatWith(({ msg }) => msg);
if (!error.isEmpty()) return res.status(422).json({ error: error.array() });
next();
},
signup
);
// /api/auth/login
router.post(
"/login",
[
check("email").isEmail().withMessage("invalid message").normalizeEmail(),
check("password")
.isLength({ min: 8, max: 15 })
.withMessage("invalid password"),
],
(req, res, next) => {
let error = validationResult(req).formatWith(({ msg }) => msg);
if (!error.isEmpty())
return res.status(422).json({ errors: error.array() });
next();
},
login
);
// /api/auth/me
router.get("/me", me);
module.exports = router;
The first few lines are just imports. At line 1, we imported express
. At line 2, we imported check
and validationResult
from express-validator
. At line 4, we imported login
, signup
and me
controllers. At line 6, we created the router
constant and stored express.Router()
in it.
前几行只是导入。 在第1行,我们导入了express
。 在第2行,我们进口的check
,并validationResult
从express-validator
。 在第4行,我们导入了login
, signup
和me
控制器。 在第6行,我们创建了router
常量,并将express.Router()
存储在其中。
From line 9 to 45, we created /api/auth/signup route. From 11 to 43, we added validation with the help of express-validator
. If you don’t have a basic understanding of express-validator
, you might want to take a look at an article which I have written:
从第9行到第45行,我们创建了/ api / auth / signup路由。 从11到43,我们在express-validator
的帮助下添加了express-validator
。 如果您对express-validator
没有基本的了解,则可能需要看一下我写的一篇文章:
Let’s go through line 9 to 45 a bit fast.
让我们快速完成第9至45行。
From line 12 to 15, we have added the validation for the "name"
field so that it should have the minimum length of 3. We also specified what the validation error message should be with the help of .withMessage()
(at line 14). We then trimmed the data (at line 15).
从第12行到第15行,我们添加了对"name"
字段的验证,以便其最小长度为3。我们还借助.withMessage()
指定了验证错误消息的.withMessage()
第14行) )。 然后我们修剪数据(在第15行)。
From line 17 to 20, we added validation to the "email"
field. We checked if the "email"
field has an actual email address. We specified what the validation error message should be (at line 19) and then we serialized the email (at line 20).
从第17行到第20行,我们在"email"
字段中添加了验证。 我们检查了"email"
字段中是否有实际的电子邮件地址。 我们指定了验证错误消息应该是什么(在第19行),然后我们对电子邮件进行了序列化(在第20行)。
From line 22 to 28, we added validation for the "password"
field. We have specified that the password should have minimum length of 8 and maximum length of 15. We also specified that the password field should have at least one number and at least one special character. We also specified what the validation error message should be.
从第22行到第28行,我们为"password"
字段添加了验证。 我们指定了密码的最小长度为8,最大长度为15。我们还指定了密码字段应至少具有一个数字和至少一个特殊字符。 我们还指定了验证错误消息应该是什么。
Then from line 30 to 35, we checked if the "password"
and "confirmPassword"
match with the help of custom validation.
然后从第30行到第35行,借助于自定义验证,我们检查了"password"
和"confirmPassword"
匹配。
Let’s now take a look at line 37 to 43. At line 38, we stored the formatted error in error
constant. Then, we check if the error
exists at line 40. If the error
exists, we send the array of errors and a status code of 422, else we run next()
. After that, at line 44, we have defined that the controller should be the signup
.
现在,让我们看一下第37至43行。在第38行,我们将格式化后的错误存储在error
常量中。 然后,在第40行检查error
存在。如果存在error
,则发送错误数组和状态码422,否则运行next()
。 之后,在第44行,我们定义了控制器应该为signup
。
From line 48 to 66, we created a route for /api/auth/login. From line 50 to 64, we added validation using express-validator
. Let’s go through these lines a bit fast. From line 50 to 56, we added validation for "email"
and "password"
field. We checked if the "email"
field has a valid email address and defined what the validation error message should be at line 51. Then, from line 53 to 55, we defined the minimum and maximum length of "password"
field and the validation error message. At line 58, we formatted the errors and stored it in the error
variable. Then, at line 60, we checked if the error
exists. If the error
exists, we send the user an array of error messages and a status code of 422 (at line 61). If the error
doesn’t exist, we run next()
. Then, we specified the login
controller at line 65.
从第48行到第66行,我们为/ api / auth / login创建了一条路由。 从第50行到第64行,我们使用express-validator
添加了验证。 让我们快速浏览一下这些内容。 从第50行到第56行,我们添加了对"email"
和"password"
字段的验证。 我们检查"email"
字段是否具有有效的电子邮件地址,并在第51行定义了验证错误消息。然后,从第53行到55行,我们定义了"password"
字段的最小和最大长度以及验证错误信息。 在第58行,我们格式化错误并将其存储在error
变量中。 然后,在第60行,我们检查error
存在。 如果存在error
,我们将向用户发送错误消息数组和状态代码422(在第61行)。 如果error
不存在,我们运行next()
。 然后,我们在第65行指定了login
控制器。
Similarly, at line 69, we created a route for /api/auth/me and specified the me
controller.
同样,在第69行,我们为/ api / auth / me创建了一条路由,并指定了me
控制器。
Finally, at line 71, we exported the router
.
最后,在第71行,我们导出了router
。
Now that we have worked on routes, let’s now work on models. For that, let’s create a directory called models inside of the main project directory. Then, let’s create a file called User.js inside of the models directory. Let’s now write the following lines of code inside of the file:
现在我们已经研究了路线,现在开始研究模型。 为此,让我们在主项目目录中创建一个名为models的目录。 然后,让我们在models目录中创建一个名为User.js的文件。 现在让我们在文件内部编写以下代码行:
const mongoose = require("mongoose");
const bcrypt = require("bcrypt");
const UserSchema = new mongoose.Schema({
name: { type: String, required: true },
email: { type: String, requried: true },
password: { type: String, required: true },
});
UserSchema.pre("save", async function (next) {
const user = this;
try {
if (!user.isModified("password")) next();
let hash = await bcrypt.hash(user.password, 13);
user.password = hash;
next();
} catch (e) {
console.error(e);
next(e);
}
});
UserSchema.methods.comparePassword = async function (password) {
try {
let result = await bcrypt.compare(password, this.password);
return result;
} catch (e) {
console.error(e);
return false;
}
};
module.exports = mongoose.model("user", UserSchema);
The first few lines are imports. At line 1, we imported mongoose
. At line 2, we imported bcrypt
.
前几行是导入。 在第1行,我们进口了mongoose
。 在第2行,我们导入了bcrypt
。
From line 4 to 8, we created UserSchema
. We have the name
, email
, and password
field in the schema. All of them have the type of String
and are required.
从第4行到第8行,我们创建了UserSchema
。 我们在模式中具有name
, email
和password
字段。 它们都具有String
类型,并且是必需的。
At line 10, we used the pre
method. This runs before the user model gets saved. At line 14, we check if the "password"
field in the user model is modified. If the "password"
field isn’t modified, we run next()
. If it’s modified, we hash the "password"
field’s data and store it in the hash
variable (at line 16). Then we stored the hash
in user.password
, at line 17. After that, we run next()
at line 19. If there occurs some error during the process, we log the error at line 21. Then, we run next(e)
at line 22.
在第10行,我们使用了pre
方法。 它在保存用户模型之前运行。 在第14行,我们检查用户模型中的"password"
字段是否被修改。 如果未修改"password"
字段,则运行next()
。 如果已修改,我们将对"password"
字段的数据进行hash
处理并将其存储在hash
变量中(第16行)。 然后,将hash
存储在第17行的user.password
中。此后,在第19行运行next()
。如果在此过程中发生错误,则在第21行记录错误。然后,运行next(e)
在第22行。
From line 26 to 36, we created the comparePassword
method in UserSchema
. At line 28, we compared plain text password ( password
) with hashed password ( this.password
) and stored it in the result
variable. We then returned the result
at line 30. If there occurs any error in this process, we log the errors at line 32. Then we return false
at line 34.
从第26行到第36行,我们在UserSchema
创建了comparePassword
方法。 在第28行,我们将纯文本密码( password
)与哈希密码( this.password
)进行了比较,并将其存储在result
变量中。 然后,我们在第30行返回result
。如果在此过程中发生任何错误,则在第32行记录错误。然后在第34行返回false
。
We, finally, exported the "user"
model, at line 38.
最后,我们在第38行导出了"user"
模型。
Until now, we have worked on models. Let’s now work on controllers. For that, let’s create a directory called controller in our main project directory. After that, let’s create a file called AuthController.js inside of the controller directory. Let’s now open up the file AuthController.js and write the following lines of code:
到目前为止,我们一直在研究模型。 现在让我们来研究控制器。 为此,让我们在主项目目录中创建一个名为controller的目录。 之后,让我们在控制器目录内创建一个名为AuthController.js的文件。 现在让我们打开文件AuthController.js并编写以下代码行:
const jwt = require("jsonwebtoken");
const User = require("../models/User");
const signup = async (req, res) => {
const { name, email, password } = req.body;
try {
let user = await User.findOne({ email });
if (!user) {
let newUser = new User({ name, email, password });
await newUser.save();
return res.status(200).json({ msg: "user successfully created" });
}
return res
.status(422)
.json({ errors: ["this email is already registered "] });
} catch (e) {
console.error(e);
return res.status(500).json({ errors: ["some error occured"] });
}
};
const login = async (req, res) => {
const { email, password } = req.body;
try {
let user = await User.findOne({ email });
if (!user) return res.status(422).json({ errors: ["no such user exists"] });
if (await user.comparePassword(password)) {
const token = jwt.sign({ id: user._id }, process.env.SECRET, {
expiresIn: "24h",
});
return res.status(200).json({ msg: "user logged in", token });
}
return res.status(403).json({ errors: ["invalid password"] });
} catch (e) {
console.error(e);
res.staus(500).json({ errors: ["some error occured"] });
}
};
const me = async (req, res) => {
let token = req.header("X-Auth");
try {
if (!token)
return res.status(403).json({ errors: ["unauthorized access"] });
let decoded = jwt.verify(token, process.env.SECRET);
let user = await User.findById(decoded.id, "name email");
if (!user) return res.status(403).json({ errors: ["unauthorized"] });
return res.status(200).json({ user });
} catch (e) {
console.error(e);
return res.status(500).json({ errors: ["some error occured"] });
}
};
module.exports = {
login,
signup,
me,
};
The first few lines are imports. At line 1, we imported jsonwebtoken
. Then, at line 3, we imported the User
model.
前几行是导入。 在第1行,我们导入了jsonwebtoken
。 然后,在第3行,我们导入了User
模型。
From line 5 to 26, we have created the signup
controller. At line 6, we stored the value of name
, email
, and password
from req.body
. At line 9, we filtered User
with email
. If the user with this email address already exists, we send the user with the status code of 422 and an error message (from line 19 to 21). If the user with this email
doesn’t exist, we create a new User
instance and store it in the newUser
variable at line 12. After that, at line 14, we saved the newUser
. After saving the newUser
, we send the user with the success message and status code of 200 (at line 16). If there occurs any error during the process, we log the error at line 23. Then, we send a status code of 500 and an error message at line 24.
从第5行到第26行,我们创建了signup
控制器。 在第6行,我们存储了来自req.body
的name
, email
和password
的req.body
。 在第9行,我们使用email
过滤了User
。 如果具有该电子邮件地址的用户已经存在,我们将向用户发送状态代码422和错误消息(从第19行到第21行)。 如果不存在使用此email
的用户,我们将创建一个新的User
实例,并将其存储在第12行的newUser
变量中。此后,在第14行,我们保存了newUser
。 保存newUser
,我们向用户发送成功消息和状态代码200(在第16行)。 如果在此过程中发生任何错误,我们将在第23行记录错误。然后,我们在第24行发送状态代码500和错误消息。
From line 28 to 49, we created the login
controller. At line 29, we stored the value of email
and password
from req.body
. At line 32, we queried the User
with email
and stored it in the user
variable. If the user
doesn’t exist, we send the user with the status code of 422 and error message at line 34. If the user
exists, we go on to compare the passwords. At line 36, we checked if the password matches or not with the help of comparePassword
method that we had created in the User
model. If the password
doesn’t match, we send the user with a status code of 403 and an error message at line 44. If the password
matches, we created JSON Web Token using jwt.sign
at line 37 and stored it in token
constant. The jwt.sign
takes in three arguments. The first argument takes in an object. This object has to contain something unique about the user
, so that we can recognize to whom the token belongs to. In our case, we have an object {id: user._id}
as the first argument. Similarly, the second argument takes in a secret key. In our case we used process.env.SECRET
. Remember that we had created this SECRET
variable earlier. The third argument takes in options object. In our case, we used the expiresIn
option and set it to "24h"
. What this does is, specifies jwt
that this token should be valid for 24 hours only. This means, the user gets automatically logged out after 24 hours of logging in. After that, we have written code to send the user with the status code of 200, a success message and token at line 41. If there occurs any error during the process, we log the error at line 46. Then, we have written code to send the user with a status code of 500 and an error message, at line 47.
从第28行到第49行,我们创建了login
控制器。 在第29行,我们存储了来自req.body
的email
和password
的值。 在第32行,我们向User
查询了email
并将其存储在user
变量中。 如果该user
不存在,则在第34行向用户发送状态代码422和错误消息。如果该user
存在,则继续比较密码。 在第36行,借助于在User
模型中创建的comparePassword
方法,我们检查了密码是否匹配。 如果password
不匹配,则在第44行向用户发送状态代码403和错误消息。如果password
匹配, jwt.sign
在第37行使用jwt.sign
创建JSON Web令牌,并将其存储在token
常量中。 jwt.sign
接受三个参数。 第一个参数接受一个对象。 该对象必须包含有关user
唯一信息,以便我们可以识别令牌属于谁。 在我们的例子中,我们有一个对象{id: user._id}
作为第一个参数。 同样,第二个参数接受一个秘密密钥。 在我们的例子中,我们使用了process.env.SECRET
。 请记住,我们之前已经创建了这个SECRET
变量。 第三个参数采用options对象。 在我们的例子中,我们使用了expiresIn
选项并将其设置为"24h"
。 它的作用是指定jwt
该令牌应仅在24小时内有效。 这意味着,用户在登录24小时后将自动注销。此后,我们在第41行编写了向用户发送状态代码200,成功消息和令牌的代码。如果在登录过程中发生任何错误,流程中,我们在第46行记录了错误。然后,我们在第47行编写了向用户发送状态代码500和错误消息的代码。
From line 51 to 68, we created a me
controller. This controller helps to get some info about the logged in user. Let’s take a closer look. At line 52, we stored the token from the "X-Auth"
header and stored it in the token
variable. This header shouldn’t necessarily be "X-Auth"
. You can choose other header for authorization too. But you have to be cautious enough so that you won’t override other headers. There are several other approaches to this. It’s up to you how you deal with it. But in our case, we’ll stick with this, okay?
从第51行到第68行,我们创建了一个me
控制器。 该控制器有助于获取有关已登录用户的一些信息。 让我们仔细看看。 在第52行,我们从"X-Auth"
标头中存储了token
并将其存储在token
变量中。 此标头不必一定是"X-Auth"
。 您也可以选择其他标题进行授权。 但是您必须足够谨慎,以免覆盖其他标头。 还有其他几种方法。 如何处理它取决于您。 但就我们而言,我们会坚持下去,好吗?
If the token doesn’t exist, we send the user with the status code of 403 and error message, at line 56. At line 58, we decode the JWT and store it in the decoded
variable. In order to decode we used jwt.verify
. This verify
method takes in two arguments. The first argument should be the token
and the second argument should be the secret key. In our case, the secret key is process.env.SECRET
.
如果令牌不存在,则在第56行向用户发送状态代码403和错误消息。在第58行,我们对JWT进行decoded
并将其存储在已decoded
变量中。 为了解码,我们使用了jwt.verify
。 这个verify
方法有两个参数。 第一个参数应该是token
,第二个参数应该是密钥。 在我们的例子中,密钥是process.env.SECRET
。
After decoding the token, we made the query of User
by id and stored it in user
variable, at line 60. While making the query by id, we have used decoded.id
. Here the decoded
contains an object. This object is the same object we used while signing. While signing JWT, in line 37, we had an object as the first argument in the jwt.sign
method. This object is the same object which we have in the decoded
variable.
解码令牌后,我们通过id进行User
查询,并将其存储在第60行的user
变量中。在通过id进行查询时,我们使用了decoded.id
。 在这里, decoded
包含一个对象。 该对象与我们在签名时使用的对象相同。 在签署JWT时,在第37行中,我们在jwt.sign
方法jwt.sign
一个对象作为第一个参数。 该对象与decoded
变量中的对象相同。
After querying the User
, we checked if the user
exists or not. If the user
doesn’t exist, we send a status code of 403 and error message, at line 62. If the user
exists, we send the user
and a status code of 200 and user’s info, at line 64.
在查询User
,我们检查了该user
存在。 如果该user
不存在,则在第62行发送状态码403和错误消息。如果该user
存在,则在第64行发送给user
和状态码200与用户信息。
If there occurs any error during the process, we log the error at line 66. Then, we send an error message with a status code of 500, at line 67.
如果在此过程中发生任何错误,我们将在66行记录该错误。然后,在67行我们将发送一条错误消息,其状态码为500。
Then, we finally exported the login
, signup
, and me
function (from line 71 to 75).
然后,我们最终导出了login
, signup
和me
函数(从第71行到第75行)。
This means, we have completed the authentication process using JWT. You might want to try this out using postman.
这意味着,我们已经使用JWT完成了身份验证过程。 您可能想使用邮递员尝试一下。
普通英语JavaScript (JavaScript In Plain English)
Did you know that we have three publications and a YouTube channel? Find links to everything at plainenglish.io!
您知道我们有三个出版物和一个YouTube频道吗? 在plainenglish.io上找到所有内容的链接!
node.js jwt