Express的使用笔记10 给登录接口添加返回token与其它接口进行token校验处理

按照常规,用户登录成功的时候是会返回一个token值,前端就可以将这个token存储到cookie中随后在其他接口使用的时候放置在Headers中进行传递。

实现这个功能,首先需要了解JWT Secret(密钥)与JWT(Token)。
JWT Secret (密钥):

  • 这是一个私有的字符串,仅在服务器端使用。
  • 它用来对Token进行签名,以确保Token的完整性和真实性。
  • 服务器在创建Token时使用这个密钥来签名,在接收到Token时也用它来验证Token的有效性。
  • 密钥应该保密,并且不应该暴露给客户端或第三方。

JWT (Token):

  • 每个用户登录成功后都会收到一个唯一的JWT。
  • JWT包含了一些声明(claims),比如用户的ID、过期时间等。
  • 客户端(例如浏览器或移动应用)会在后续请求中携带这个Token,通常是通过HTTP头部的Authorization字段。
  • 服务器会验证Token的签名,检查它的有效性和完整性,然后根据Token中的信息来识别用户身份。

接下来,需要考虑如何安全的生成一个密钥且放置在一个安全的地方~
将JWT密钥配置在环境变量中(如process.env.JWT_SECRET)是目前推荐的做法之一,但安全性取决于你如何管理和保护这些环境变量。以下是几个关键点来确保这种方式的安全性:

  1. 不要硬编码密钥
    避免将密钥直接写入代码库或配置文件中,因为这可能会导致它们被意外泄露。使用环境变量可以有效防止这种情况发生。

  2. 使用.env文件时要小心
    如果你选择使用.env文件来管理环境变量,请确保这个文件不会被提交到版本控制系统中。你应该在项目的根目录下创建一个.gitignore文件,并添加如下内容:

.env

这样可以确保.env文件不会被Git追踪和上传。

  1. 限制对环境变量的访问
    确保只有必要的进程和服务能够读取环境变量。在生产环境中,操作系统级别的权限设置和容器化技术(如Docker)可以帮助你更好地控制谁可以访问这些变量。

  2. 在生产环境中安全地传递环境变量
    对于生产部署,考虑使用云服务提供商的安全特性来管理秘密信息。例如:

  • AWS Secrets Manager 或 AWS Systems Manager Parameter Store
  • Azure Key Vault
  • Google Cloud Secret Manager
  • HashiCorp Vault
    这些服务允许你在不暴露敏感信息的情况下,安全地存储和检索密钥等重要数据。
  1. 定期更新密钥
    即使采取了所有预防措施,也应该定期更新你的JWT密钥。这有助于减少长期使用的同一密钥可能带来的风险。当更换密钥时,确保有适当的策略处理现有Token的有效性问题,比如实现刷新Token机制。

  2. 使用强随机密钥
    确保生成的JWT密钥足够强大且随机。你可以使用密码学安全的伪随机数生成器(CSPRNG)来创建密钥,而不是简单的字符串。

  3. 加密环境变量(可选)
    在某些情况下,你还可以选择加密环境变量本身,然后在应用程序启动时解密。不过这种方法增加了复杂性,通常只在高度敏感的应用场景中使用。

  4. 审计和监控
    定期审查和监控对环境变量的访问日志,以检测任何异常行为。这可以帮助你及时发现潜在的安全漏洞。

这里咱们的条件有限,就只模拟一下在环境变量中存储密钥吧。
先安装需要的插件

npm install jsonwebtoken bcryptjs

根目录下创建init-env.js,.gitignore(这个看项目是否提交到github咯,不提交的话可以不搞),init-env.js的作用就是没有.env文件的时候在根目录下自动生成一个,然后赋值

const fs = require('fs');
const crypto = require('crypto');

if (!fs.existsSync('.env')) {
  const jwtSecret = crypto.randomBytes(32).toString('base64');
  fs.writeFileSync('.env', `JWT_SECRET=${jwtSecret}`);
  console.log('.env file created with JWT_SECRET.');
} else {
  console.log('.env file already exists.');
}

这里还需要在package.json中加入一段代码,项目运行之后就可以正常执行是否生成.env

 "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "predev": "node init-env.js", 
    "dev": "nodemon app.js"
  },

.env 文件生成后就有一个变量咯

JWT_SECRET=xdHKwg41O6SjdRD61e/iCi9PYIVCtaIy430xo3wcXyk=

在app.js入口文件中也加上

const crypto = require('crypto');

// 如果 .env 文件中没有 JWT_SECRET,则生成一个新的
if (!process.env.JWT_SECRET) {
  const newSecret = crypto.randomBytes(32).toString('base64');
  // 这里可以根据实际情况选择是否更新 .env 文件
  // fs.writeFileSync('.env', `JWT_SECRET=${newSecret}`, { flag: 'a' });
  process.env.JWT_SECRET = newSecret;
}

最后回到我们的路由中去进行处理,在登录的api方法中,添加登录成功后token的生成,其实token的值就是根据上面的JWT_SECRET 与用户信息结合生成

const jwt = require("jsonwebtoken");
// 用户登录
router.post(
  "/users/login",
  [
    body("user.username").notEmpty().withMessage("用户名不能为空"),
    body("user.password")
      .notEmpty()
      .withMessage("密码不能为空")
      .bail()
      .custom(async (password, { req }) => {
        const user = await User.findOne({ username: req.body.user.username });
        if (!user) {
          return Promise.reject("账户或密码错误,请检查!");
        }
        // 比较提供的密码与数据库中存储的哈希密码
        const isMatch = await bcrypt.compare(password, user.password);
        if (!isMatch) {
          return Promise.reject("账户或密码错误,请检查!");
        }
        return true;
      }),
  ],
  async (req, res, next) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({ errors: errors.array()[0] });
    };
    try {
      // 获取用户信息
      const user = await User.findOne({ username: req.body.user.username });
      // 创建token
      const token = jwt.sign({ userId: user._id }, process.env.JWT_SECRET, {
        expiresIn: "1h",
      });
      // 返回token
      res.json({
        message: "登录成功",
        token,
        user: {
          id: user._id,
          username: user.username,
        },
      });
    } catch (error) {
      console.error(error);
      res.status(500).json({ message: "服务器错误" });
    }
  },
  userCtrl.login
);

然后,来到postman,我们尝试调用这个api,看看返回值

接下来,继续看看token的校验咯,按照原计划,我们将进一步完善其它的接口,将其它的接口的头部加上token进行传递,然后我们添加token校验。以获取当前登录用户信息为例。
1.创建middleware\authMiddleware.js,封装中间件

const jwt = require("jsonwebtoken");

function authenticateToken(req, res, next) {
  const authHeader = req.headers["authorization"];
  const token = authHeader && authHeader.split(" ")[1]; // Bearer <token>

  if (!token) return res.sendStatus(401); // 如果没有token,返回401 Unauthorized

  jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
    if (err) return res.sendStatus(403); // 如果验证失败,返回403 Forbidden

    // 将用户信息附加到req对象上,以便后续路由使用
    req.user = user;
    next(); // 继续执行下一个中间件或路由处理函数
  });
}
module.exports = authenticateToken;

2.在controller/user.js文件中完善根据用户id获取用户信息的get API

// 获取当前登录用户的信息
exports.getCurUser = async (req, res) => {
  try {
    const user = await User.findById(req.body.user.id).select('-password');//获取除密码以外的信息
    if (!user) {
      return res.status(404).json({ message: "用户未找到" });
    }
    res.json({
      message: "获取用户信息成功",
      data: {
        id: user._id,
        username: user.username,
        email: user.email,
      },
    });
  } catch (error) {
    console.error(error);
    res.status(500).json({ message: "服务器错误" });
  }
};

3.在router\user.js文件中引入token的校验中间件方法

const authenticateToken  = require("../middleware/authMiddleware");
// 获取当前登录用户信息
router.get(
  "/user",
  [body("user.id").notEmpty().withMessage("用户id不能为空")],
  authenticateToken,
  (req, res,next) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({ errors: errors.array()[0] });
    }
    next()
  },
  userCtrl.getCurUser
);
  1. 进行测试


posted @   JocelynFung  阅读(87)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律
Live2D 看板娘 / Demo
点击右上角即可分享
微信分享提示