node.js jwt_如何在Node.js中实现JWT用户身份验证

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.

有了上面的命令,我们已经安装了expressjsonwebtokenexpress-validatormongoosebcryptdotenv 。 您显然知道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 ,并validationResultexpress-validator 。 在第4行,我们导入了loginsignupme控制器。 在第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 。 我们在模式中具有nameemailpassword字段。 它们都具有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.bodynameemailpasswordreq.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.bodyemailpassword的值。 在第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).

然后,我们最终导出了loginsignupme函数(从第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上找到所有内容的链接!

翻译自: https://medium.com/javascript-in-plain-english/how-to-implement-jwt-user-authentication-in-node-js-b6d093f6fa4

node.js jwt

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值