JWT 授权和身份验证、Node、Express 和 Vue 后端架构控制器 index.js 前端注册登录注销 MySQL PostreSQL 发送代码进行确认

2025-06-09

JWT 授权和身份验证、Node、Express 和 Vue

后端

架构

控制器

index.js

前端

报名

登录

登出

MySQL

PostreSQL

发送确认代码

在本教程中,我们将为博客应用添加之前教程中用到的身份验证和授权功能。我们将使用 Passport 和 JWT 两种身份验证方式。今天我们将介绍 JWT 的使用方法。

有趣的是,我也会阅读其他人的文章,那些有趣的内容总是会添加到我的阅读列表中。这篇文章解释了 JWT 的可扩展性。我不需要再添加更多内容了。

https://dev.to/kmistele/demystifying-jwt-how-to-secure-your-next-web-app-9h0

在我学习的过程中,我总是好奇地想知道如何将我所读到的内容实际应用到应用程序中。

到目前为止,我们已经了解到:

  1. MongoDB、Express、Vue 和 Node 的方法仍然属于无服务器,因为我们目前运行应用程序时无需管理基础设施。Atlas 为我们管理一切。

  2. AWS PostgreSQL、Express、Vue 和 Node 的方法属于服务器方法,因为我们目前在 EC2 中运行应用程序。我们必须管理基础设施。

  3. AWS MySQL、Express、Vue 和 Node 的方法属于服务器方法,因为我们目前在 EC2 上运行应用程序。我们必须管理基础设施。

对于初创公司来说,维护服务器的成本相当高,因此在容器和无服务器之间进行权衡是理想的。

让我们将以下组件添加到前端的身份验证目录。

登录.vue

替代文本

注册.vue

替代文本

确认.vue

替代文本

最终,我们只允许使用 GET 文章 API 来公开查看文章。其余的 CRUD 操作需要经过身份验证后才能执行。

让我们开始吧。

后端

如果您关注过我们之前的教程,那么您现在知道我们有两个应用程序后端在 Node、Express、Mongo DB 或 PostgreSQL 或 MySQL 上运行,具体取决于您选择的数据库。

我们希望为该应用程序添加身份验证和授权。我们假设如下。

  1. 我们只想允许公众通过 GET 方式访问以下路线。
  2. 我们希望允许管理员角色执行 DELETE 操作,其他所有操作均可由管理员或用户进行评估。

以下是 routes 文件夹中的 blog.js 内容

const express = require("express")
const router = express.Router()
const blog = require("../controller/blog.controller");
const { auth_jwt_token } = require("../authentication");

// /api/blog: GET, POST, DELETE
// /api/blog/:id: GET, PUT, DELETE
// /api/blog/published: GET

// Create a new blog
router.post("/", [auth_jwt_token.verifyToken], blog.create);

// Retrieve all blog
router.get("/", blog.findAll);

// Retrieve all published blog
router.get("/published", blog.findAllPublished);

// Retrieve a single blog with id
router.get("/:id", blog.findOne);

// Update a blog with id
router.put("/:id", [auth_jwt_token.verifyToken], blog.update);

// Delete a blog with id
router.delete("/:id", [auth_jwt_token.verifyToken, auth_jwt_token.isAdmin], blog.delete);

// Create a new blog
router.delete("/", [auth_jwt_token.verifyToken, auth_jwt_token.isAdmin], blog.deleteAll);

module.exports = router
Enter fullscreen mode Exit fullscreen mode

我们的博客需要两个角色:用户和管理员。
对于令牌,您可以使用 jsonwebtoken 或 express.jwt。让我们安装 bcryptjs 来哈希密码,并使用 jsonwebtoken 来获取令牌。

yarn add jsonwebtoken bcryptjs
Enter fullscreen mode Exit fullscreen mode

index.js

在 index.js 文件中,当我们的后端应用程序加载时,我们需要检查数据库是否已正确设置角色。如果为空,则需要创建角色。让我们创建一个初始化函数来处理角色的检查。

const Role = db.role // reference the Role DB
function initialize() {
    Role.estimatedDocumentCount((err, count) => {
      if (!err && count === 0) {
        new Role({
          name: "user"
        }).save(err => {
          if (err) {
            console.log("error", err);
          }
          console.log("added 'user' to roles collection");
        });

        new Role({
          name: "admin"
        }).save(err => {
          if (err) {
            console.log("error", err);
          }  
          console.log("added 'admin' to roles collection");
        });
      }
    });
  }
Enter fullscreen mode Exit fullscreen mode

以下是首次运行后端时的结果。我们之前没有角色,它初始化了角色表并添加了新角色。

替代文本

路线

我们将在 routes 文件夹中添加一个名为 auth.js 的新路由并更新博客路由。

auth.js
此路由将处理 signup 和 signin 两个函数。verify_user_email 函数用于检查邮箱是否已注册,以避免重复。

const { verify_user_email } = require("../authentication");
const express = require("express")
const router = express.Router()
const auth = require("../controller/auth.controller");

router.post("/signin", auth.signin);

router.post("/signup", 
  [
      verify_user_email.checkDuplicateUsernameOrEmail,
      verify_user_email.checkRolesExisted
  ],
  auth.signup
)

module.exports = router
Enter fullscreen mode Exit fullscreen mode

blog.js
我已经在上面分享了我们的 blog.js 路由文件夹应该是什么样子。

这就是我们在 routes 文件夹中需要做的全部工作。接下来,我们需要更新 index.js 文件并导入我们的路由。使用 express.js,您可以使用可选的挂载路径加载应用程序级和路由器级中间件。您还可以将一系列中间件函数一起加载,从而在挂载点创建中间件系统的子堆栈。

index.js

// routes
const blog = require('./app/routes/blog') // blog routes
const auth = require('./app/routes/auth') // user authentication

app.use('/api/blog',blog, function(req, res, next){
  res.header(
    "Access-Control-Allow-Headers",
    "x-access-token, Origin, Content-Type, Accept"
  );
  next();
}) // user authorization
app.use('/api/auth', auth, function(req, res, next){
  res.header(
    "Access-Control-Allow-Headers",
    "x-access-token, Origin, Content-Type, Accept"
  );
  next();
}) // auth authentication
Enter fullscreen mode Exit fullscreen mode

我希望我们目前能达成共识。保持密切联系

架构

让我们定义用户和角色的架构。这将在模型文件夹中完成,我们也在其中保存了博客架构。

role.model.js
我们的角色将有一个名称和一个 ID。

module.exports = mongoose => {
    const Role = mongoose.model(
      "Role",
      mongoose.Schema(
        {
          name: String,
        },
        { timestamps: true }
      ) 
    );
    return Role;
  };
Enter fullscreen mode Exit fullscreen mode

user.model.js
在用户模型中,我们需要为用户添加用户名、邮箱、密码和角色。默认情况下,用户将拥有一个用户角色,之后会升级为管理员。
注意,我们引用了角色,以便获取角色的正确 ID。

module.exports = mongoose => {
    const User = mongoose.model(
      "User",
      mongoose.Schema(
        {
          username: String,
          email: String,
          password: String,
          roles: [
            {
              type: mongoose.Schema.Types.ObjectId,
              ref: "Role"
            }
          ]
        },
        { timestamps: true }
      )
    );
    return User;
  };
Enter fullscreen mode Exit fullscreen mode

控制器

在控制器文件夹中,让我们添加一个处理身份验证的控制器

auth.controller.js 中的
注册函数会创建一个新用户,而签名函数则会确认用户是否存在。之后,用户的有效负载将使用私钥进行签名,并生成一个令牌。为了验证令牌,我们可以验证签名后使用 JWT 进行解码,或者直接解码 JWT 令牌。我们将处理这两种情况。

const crypto = require('crypto');
const db = require("../models");
const User = db.user;
const Role = db.role;

var jwt = require("jsonwebtoken");
var bcrypt = require("bcryptjs");

exports.signup = (req, res) => {
  const user = new User({
    username: req.body.username,
    email: req.body.email,
    password: bcrypt.hashSync(req.body.password, 8)
  });

  user.save((err, user) => {
    if (err) {
      res.status(500).send({ message: err });
      return;
    }

    if (req.body.roles) {
      Role.find(
        {
          name: { $in: req.body.roles }
        },
        (err, roles) => {
          if (err) {
            res.status(500).send({ message: err });
            return;
          }

          user.roles = roles.map(role => role._id);
          user.save(err => {
            if (err) {
              res.status(500).send({ message: err });
              return;
            }

            res.send({ message: "User was registered successfully!" });
          });
        }
      );
    } else {
      Role.findOne({ name: "user" }, (err, role) => {
        if (err) {
          res.status(500).send({ message: err });
          return;
        }

        user.roles = [role._id];
        user.save(err => {
          if (err) {
            res.status(500).send({ message: err });
            return;
          }

          res.send({ message: "User was registered successfully!" });
        });
      });
    }
  });
};

exports.signin = (req, res) => {
  User.findOne({
    username: req.body.username
  })
    .populate("roles", "-__v")
    .exec((err, user) => {

      if (err) {
        res.status(500).send({ message: err });
        return;
      }

      if (!user) {
        return res.status(404).send({ message: "User Not found." });
      }

      var passwordIsValid = bcrypt.compareSync(
        req.body.password,
        user.password
      );

      if (!passwordIsValid) {
        return res.status(401).send({
          accessToken: null,
          message: "Invalid Password!"
        });
      }

      const { privateKey, publicKey } = crypto.generateKeyPairSync('ec', {
        namedCurve: 'sect239k1'
      });

      // generate a signature of the payload
      const sign = crypto.createSign('SHA256');
      sign.write(`${user}`);
      sign.end();
      var signature = sign.sign(privateKey, 'hex');
      console.log(signature)


      // sign username
      var token = jwt.sign({ id: user.id }, signature, {
        expiresIn: 86400 // 24 hours
      });

      var authorities = [];

      for (let i = 0; i < user.roles.length; i++) {
        authorities.push("ROLE_" + user.roles[i].name.toUpperCase());
      }
      res.status(200).send({
        id: user._id,
        username: user.username,
        email: user.email,
        roles: authorities,
        accessToken: token, // access token
        signature: signature // signature
      });
    });
};
Enter fullscreen mode Exit fullscreen mode

最后,让我们在应用程序文件夹中创建一个身份验证文件夹。

touch /app/authentication
Enter fullscreen mode Exit fullscreen mode

然后创建三个文件 index.js、auth.js 和 verify.js。verify.js 将处理用户电子邮件的验证,而 auth.js 将处理用户令牌的验证以及用户是否为管理员。

授权.js

const jwt = require("jsonwebtoken");
const db = require("../models");
const User = db.user;
const Role = db.role;

verifyToken = (req, res, next) => {
  let token = req.headers["x-access-token"];
  let secret = req.headers["x-access-signature"];

  if (!token) {
    return res.status(403).send({ message: "No token provided!" });
  }


  // Prints: true
  jwt.verify(token, secret, (err, decoded) => {
    if (err) {
      return res.status(401).send({ message: "Unauthorized!" });
    }
    req.userId = decoded.id;
    next();
  });
};

isAdmin = (req, res, next) => {
  User.findById(req.userId).exec((err, user) => {
    if (err) {
      res.status(500).send({ message: err });
      return;
    }

    Role.find(
      {
        _id: { $in: user.roles }
      },
      (err, roles) => {
        if (err) {
          res.status(500).send({ message: err });
          return;
        }

        for (let i = 0; i < roles.length; i++) {
          if (roles[i].name === "admin") {
            next();
            return;
          }
        }

        res.status(403).send({ message: "Require Admin Role!" });
        return;
      }
    );
  });
};

const authJwt = {
  verifyToken,
  isAdmin,
};
module.exports = authJwt;
Enter fullscreen mode Exit fullscreen mode

验证.js

const db = require("../models");
const ROLES = db.ROLES;
const User = db.user;

checkDuplicateUsernameOrEmail = (req, res, next) => {
  // Username
  User.findOne({
    username: req.body.username
  }).exec((err, user) => {
    if (err) {
      res.status(500).send({ message: err });
      return;
    }

    if (user) {
      res.status(400).send({ message: "Failed! Username is already in use!" });
      return;
    }

    // Email
    User.findOne({
      email: req.body.email
    }).exec((err, user) => {
      if (err) {
        res.status(500).send({ message: err });
        return;
      }

      if (user) {
        res.status(400).send({ message: "Failed! Email is already in use!" });
        return;
      }

      next();
    });
  });
};

checkRolesExisted = (req, res, next) => {
  if (req.body.roles) {
    for (let i = 0; i < req.body.roles.length; i++) {
      if (!ROLES.includes(req.body.roles[i])) {
        res.status(400).send({
          message: `Failed! Role ${req.body.roles[i]} does not exist!`
        });
        return;
      }
    }
  }

  next();
};

const verifySignUp = {
  checkDuplicateUsernameOrEmail,
  checkRolesExisted
};

module.exports = verifySignUp;
Enter fullscreen mode Exit fullscreen mode

通过文件夹中的index.js导出认证文件中的所有内容。

index.js

const auth_jwt_token = require("./auth");
const verify_user_email = require("./verify");

module.exports = {
  auth_jwt_token,
  verify_user_email
};
Enter fullscreen mode Exit fullscreen mode

让我们测试一下后端,确保所有配置都正确。我将使用 Postman 进行以下测试:

  1. 注册一个没有管理员权限的用户。
  2. 登录用户。
  3. 创建博客(需要令牌)
  4. 删除博客(需要令牌和管理员访问权限)

前端

让我们配置前端并链接两者之间的通信。让我们在 components 目录中创建一个名为 auth.script.js 的文件。

import axios from "axios";

export const signup = async item => {
  let data = {
    username: item.username,
    email: item.email,
    password: item.password,
    roles: ["user"]
  };
  let request = {
    url: "http://localhost:3000/api/auth/signup", // should be replaced after going to production with domain url
    method: "post",
    headers: {
      "Content-type": "application/json"
    },
    data: JSON.stringify(data)
  };

  const response = await axios(request);
  return response;
};

export const login = async item => {
  let data = {
    username: item.username,
    password: item.password
  };
  let request = {
    url: "http://localhost:3000/api/auth/signin", // should be replaced after going to production with domain url
    method: "post",
    headers: {
      "Content-type": "application/json"
    },
    data: JSON.stringify(data)
  };

  const response = await axios(request);
  return response;
};

Enter fullscreen mode Exit fullscreen mode

登录成功后,我们需要确保安全地保存用户信息。这里有一篇关于如何安全地保存有效载荷的文章。https ://dev.to/gkoniaris/how-to-securely-store-jwt-tokens-51cf ​​。

注销功能应该清除存储并将用户重定向回登录页面或主页。

报名

在注册组件的方法部分添加以下函数,并在用户提交时调用它。

// import the signup function from auth.script.js

// sibmit signup
    async submit() {
      this.loading = true;
      const response = await signup(this.item);
      if (response === "User was registered successfully!") {
        // DO NOT USE LOCAL STORAGE
        localStorage.setItem("user", JSON.stringify(response.data));
        this.item = {
          username: "",
          email: "",
          password: "",
          roles: ["user"]
        };
        this.loading = false;
        this.$router.push("/dashboard");
      } else {
        // error
        console.log("Error", response);
        setTimeout(() => {
          this.loading = false;
        }, 1000);
      }
    }
Enter fullscreen mode Exit fullscreen mode

登录

在登录组件中,在方法部分添加以下函数,并在用户提交时调用它。

// import the login function from auth.script.js

// sibmit login
    async submit() {
      this.loading = true;
      const response = await login(this.item);
      if (response.data.accessToken) {
         // DO NOT USE LOCAL STORAGE
        localStorage.setItem("user", JSON.stringify(response.data));
        this.item = {
          username: "",
          password: ""
        };
        this.loading = false;
        this.$router.push("/dashboard");
      } else {
        // error
        console.log("Error", response);
      }
    }
Enter fullscreen mode Exit fullscreen mode

登出

在仪表板中更新注销功能,添加清除已保存的用户信息的方法。

// DO NOT USE LOCAL STORAGE
localStorage.removeItem("user")
Enter fullscreen mode Exit fullscreen mode

笔记

对于博客路由中需要身份验证的每个请求,请确保您的标头包含以下内容:

headers: {
      "Content-type": "application/json",
      'x-access-token': item.accessToken,
      'x-access-signature': item.signature
    },
Enter fullscreen mode Exit fullscreen mode

最后,在路由器中保护 Vue 应用程序中的所有路由。在路由器文件夹中,确保按如下方式更新 index.js 文件。

const router = new VueRouter({
  routes: [
    {
      path: '/dashboard',
      component: Dashboard,
      // save you have a means of updating isAuthenticated
      beforeEach((to, from, next) => {
         if (to.name !== 'Login' && !isAuthenticated) next({ name: 
           'Login' })
          else next()
      })
    }
  ]
})
Enter fullscreen mode Exit fullscreen mode

阅读有关在 Vue 应用程序中保护路由的更多信息https://router.vuejs.org/guide/advanced/navigation-guards.html#global-before-guards

MySQL

对于 MySQL,我将提供需要更改的文件。以下是 MySQL 的文章:https://dev.to/kevin_odongo35/aws-rds-mysql-express-vue-and-node-jfj

index.js

const Role = db.role // reference the Role DB
function initialize() {
  Role.create({
    id: 1,
    name: "user"
  });

  Role.create({
    id: 3,
    name: "admin"
  });
}
Enter fullscreen mode Exit fullscreen mode

角色.模型.js

module.exports = (sequelize, Sequelize) => {
    const Role = sequelize.define("roles", {
    id: {
      type: Sequelize.INTEGER,
      primaryKey: true
    },
    name: {
      type: Sequelize.STRING
    }
  });
  };
Enter fullscreen mode Exit fullscreen mode

用户.模型.js

const User = sequelize.define("users", {
    username: {
      type: Sequelize.STRING
    },
    email: {
      type: Sequelize.STRING
    },
    password: {
      type: Sequelize.STRING
    }
  });

  return User;
Enter fullscreen mode Exit fullscreen mode

/模型/index.js

db.user = require("../models/user.model.js")(sequelize, Sequelize);
db.role = require("../models/role.model.js")(sequelize, Sequelize);

db.role.belongsToMany(db.user, {
  through: "user_roles",
  foreignKey: "roleId",
  otherKey: "userId"
});
db.user.belongsToMany(db.role, {
  through: "user_roles",
  foreignKey: "userId",
  otherKey: "roleId"
});

db.ROLES = ["user", "admin"];
Enter fullscreen mode Exit fullscreen mode

其余内容与我上面详述的一致。只需编辑以下文件即可。

PostreSQL

对于 PostgreSQL,我将提供需要更改的文件。以下是 PostgreSQL 的文章:https://dev.to/kevin_odongo35/aws-rds-postgresql-express-vue-and-node-1k99

index.js

const Role = db.role // reference the Role DB
function initialize() {
  Role.create({
    id: 1,
    name: "user"
  });

  Role.create({
    id: 3,
    name: "admin"
  });
}
Enter fullscreen mode Exit fullscreen mode

角色.模型.js

module.exports = (sequelize, Sequelize) => {
    const Role = sequelize.define("roles", {
    id: {
      type: Sequelize.INTEGER,
      primaryKey: true
    },
    name: {
      type: Sequelize.STRING
    }
  });
  };
Enter fullscreen mode Exit fullscreen mode

用户.模型.js

const User = sequelize.define("users", {
    username: {
      type: Sequelize.STRING
    },
    email: {
      type: Sequelize.STRING
    },
    password: {
      type: Sequelize.STRING
    }
  });

  return User;
Enter fullscreen mode Exit fullscreen mode

/模型/index.js

db.user = require("../models/user.model.js")(sequelize, Sequelize);
db.role = require("../models/role.model.js")(sequelize, Sequelize);

db.role.belongsToMany(db.user, {
  through: "user_roles",
  foreignKey: "roleId",
  otherKey: "userId"
});
db.user.belongsToMany(db.role, {
  through: "user_roles",
  foreignKey: "userId",
  otherKey: "roleId"
});

db.ROLES = ["user", "admin"];
Enter fullscreen mode Exit fullscreen mode

其余内容与我上面详述的一致。只需编辑以下文件即可。

发送确认代码

您可以使用不同的产品来实现这一点,但我推荐 AWS SES。我曾经讨论过如何配置 AWS SES https://dev.to/kevin_odongo35/build-a-bulk-email-and-sms-app-with-vue-and-aws-ses-aws-sns-or-twilio-part-1-33jp。我将在一个课程中详细介绍整个逻辑。从头到尾。请密切关注

我希望本教程对如何实现 JWT 有所帮助。我提供了一些文章来帮助您更好地理解 JWT。如何安全地使用它们。

谢谢

鏂囩珷鏉ユ簮锛�https://dev.to/kevin_odongo35/jwt-authorization-and-authentication-node-express-and-vue-2p8c
PREV
使用 React Hooks 和 context API 构建可重用的通知系统
NEXT
如果你知道 CSS 的工作原理,你就会更擅长 CSS