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)是目前推荐的做法之一,但安全性取决于你如何管理和保护这些环境变量。以下是几个关键点来确保这种方式的安全性:
-
不要硬编码密钥
避免将密钥直接写入代码库或配置文件中,因为这可能会导致它们被意外泄露。使用环境变量可以有效防止这种情况发生。 -
使用.env文件时要小心
如果你选择使用.env文件来管理环境变量,请确保这个文件不会被提交到版本控制系统中。你应该在项目的根目录下创建一个.gitignore文件,并添加如下内容:
.env
这样可以确保.env文件不会被Git追踪和上传。
-
限制对环境变量的访问
确保只有必要的进程和服务能够读取环境变量。在生产环境中,操作系统级别的权限设置和容器化技术(如Docker)可以帮助你更好地控制谁可以访问这些变量。 -
在生产环境中安全地传递环境变量
对于生产部署,考虑使用云服务提供商的安全特性来管理秘密信息。例如:
- AWS Secrets Manager 或 AWS Systems Manager Parameter Store
- Azure Key Vault
- Google Cloud Secret Manager
- HashiCorp Vault
这些服务允许你在不暴露敏感信息的情况下,安全地存储和检索密钥等重要数据。
-
定期更新密钥
即使采取了所有预防措施,也应该定期更新你的JWT密钥。这有助于减少长期使用的同一密钥可能带来的风险。当更换密钥时,确保有适当的策略处理现有Token的有效性问题,比如实现刷新Token机制。 -
使用强随机密钥
确保生成的JWT密钥足够强大且随机。你可以使用密码学安全的伪随机数生成器(CSPRNG)来创建密钥,而不是简单的字符串。 -
加密环境变量(可选)
在某些情况下,你还可以选择加密环境变量本身,然后在应用程序启动时解密。不过这种方法增加了复杂性,通常只在高度敏感的应用场景中使用。 -
审计和监控
定期审查和监控对环境变量的访问日志,以检测任何异常行为。这可以帮助你及时发现潜在的安全漏洞。
这里咱们的条件有限,就只模拟一下在环境变量中存储密钥吧。
先安装需要的插件
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
);
- 进行测试
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律