活动公告

系统通知
05-18 21:22
系统通知
通知:本站资源由网友上传分享,如有违规等问题请到版务模块进行投诉,资源失效请在帖子内回复要求补档,会尽快处理!
10-23 09:31

Node.js与JWT完美结合构建高效安全的用户认证体系从入门到实战全攻略

SunJu_FaceMall

3万

主题

2860

科技点

3万

积分

白金月票

碾压王

积分
32872

塔罗立华奏

<font color=白金月票" /> 发表于 2025-9-18 19:40:35 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

x
引言

在现代Web应用开发中,用户认证系统是保障应用安全的核心组件。随着前后端分离架构的普及,传统的基于会话(Session)的认证方式逐渐显露出局限性,特别是在分布式系统和跨域场景下。JSON Web Token(JWT)作为一种轻量级的、基于JSON的开放标准(RFC 7519),为解决这些问题提供了优雅的方案。

JWT具有自包含、无状态、可扩展等特性,非常适合在Node.js应用中实现用户认证。本文将从基础概念入手,逐步深入到实战应用,全面介绍如何使用Node.js和JWT构建高效、安全的用户认证体系。

JWT基础:理解认证的核心

什么是JWT?

JWT(JSON Web Token)是一种开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于在各方之间安全地传输信息。JWT可以用于认证和信息交换,因为它的数字签名或加密特性确保了信息的可信度。

JWT的结构

JWT由三部分组成,用点(.)分隔:

1. Header(头部):包含令牌的类型和使用的签名算法
2. Payload(载荷):包含声明(用户信息和元数据)
3. Signature(签名):用于验证消息的完整性

一个典型的JWT看起来像这样:xxxxx.yyyyy.zzzzz

下面是一个具体的JWT示例:
  1. // Header
  2. {
  3.   "alg": "HS256",
  4.   "typ": "JWT"
  5. }
  6. // Payload
  7. {
  8.   "sub": "1234567890",
  9.   "name": "John Doe",
  10.   "iat": 1516239022
  11. }
  12. // Signature
  13. HMACSHA256(
  14.   base64UrlEncode(header) + "." +
  15.   base64UrlEncode(payload),
  16.   secret)
复制代码

JWT的工作原理

JWT认证流程如下:

1. 用户使用用户名和密码进行登录
2. 服务器验证用户凭证
3. 服务器生成一个JWT,其中包含用户信息和过期时间
4. 服务器将JWT发送给客户端
5. 客户端存储JWT(通常在localStorage或cookie中)
6. 客户端在后续请求中通过Authorization头发送JWT
7. 服务器验证JWT的有效性
8. 如果JWT有效,服务器处理请求并返回响应

Node.js环境准备:搭建开发环境

在开始构建JWT认证系统之前,我们需要准备Node.js开发环境并安装必要的依赖。

初始化项目

首先,创建一个新的项目目录并初始化Node.js项目:
  1. mkdir node-jwt-auth
  2. cd node-jwt-auth
  3. npm init -y
复制代码

安装依赖

安装所需的依赖包:
  1. npm install express jsonwebtoken bcrypt cors dotenv helmet morgan
  2. npm install --save-dev nodemon
复制代码

这些依赖的作用:

• express: Node.js的Web框架
• jsonwebtoken: 用于生成和验证JWT
• bcrypt: 用于密码哈希和验证
• cors: 处理跨域资源共享
• dotenv: 加载环境变量
• helmet: 增强应用安全性
• morgan: HTTP请求日志记录器
• nodemon: 开发时自动重启服务器

创建基本服务器

创建index.js文件并设置基本的Express服务器:
  1. require('dotenv').config();
  2. const express = require('express');
  3. const cors = require('cors');
  4. const helmet = require('helmet');
  5. const morgan = require('morgan');
  6. const app = express();
  7. // 中间件
  8. app.use(helmet()); // 安全增强
  9. app.use(cors()); // 允许跨域
  10. app.use(morgan('combined')); // 日志记录
  11. app.use(express.json()); // 解析JSON请求体
  12. app.use(express.urlencoded({ extended: true })); // 解析URL编码的请求体
  13. // 基本路由
  14. app.get('/', (req, res) => {
  15.   res.json({ message: 'Welcome to JWT Authentication API' });
  16. });
  17. // 启动服务器
  18. const PORT = process.env.PORT || 3000;
  19. app.listen(PORT, () => {
  20.   console.log(`Server is running on port ${PORT}`);
  21. });
复制代码

配置环境变量

创建.env文件来存储敏感信息:
  1. PORT=3000
  2. JWT_SECRET=your_super_secret_key
  3. JWT_EXPIRES_IN=1h
  4. BCRYPT_SALT_ROUNDS=10
  5. MONGODB_URI=mongodb://localhost:27017/jwt_auth
复制代码

实现用户注册:创建用户账户

设置数据库模型

为了简单起见,我们将使用内存数组来模拟数据库。在实际应用中,你应该使用像MongoDB、MySQL或PostgreSQL这样的数据库。

创建models/User.js文件:
  1. const bcrypt = require('bcrypt');
  2. // 模拟数据库
  3. let users = [];
  4. class User {
  5.   constructor(userData) {
  6.     this.id = users.length + 1;
  7.     this.username = userData.username;
  8.     this.email = userData.email;
  9.     this.passwordHash = this.hashPassword(userData.password);
  10.     this.createdAt = new Date();
  11.   }
  12.   // 哈希密码
  13.   hashPassword(password) {
  14.     return bcrypt.hashSync(password, parseInt(process.env.BCRYPT_SALT_ROUNDS));
  15.   }
  16.   // 验证密码
  17.   validatePassword(password) {
  18.     return bcrypt.compareSync(password, this.passwordHash);
  19.   }
  20.   // 转换为安全对象(不包含密码)
  21.   toSafeObject() {
  22.     const { passwordHash, ...userWithoutPassword } = this;
  23.     return userWithoutPassword;
  24.   }
  25.   // 静态方法:查找用户
  26.   static findByEmail(email) {
  27.     return users.find(user => user.email === email);
  28.   }
  29.   // 静态方法:查找用户ByID
  30.   static findById(id) {
  31.     return users.find(user => user.id === id);
  32.   }
  33.   // 静态方法:创建用户
  34.   static create(userData) {
  35.     // 检查用户是否已存在
  36.     if (this.findByEmail(userData.email)) {
  37.       throw new Error('User already exists');
  38.     }
  39.     const user = new User(userData);
  40.     users.push(user);
  41.     return user;
  42.   }
  43. }
  44. module.exports = User;
复制代码

创建注册路由

创建routes/auth.js文件来处理认证相关的路由:
  1. const express = require('express');
  2. const jwt = require('jsonwebtoken');
  3. const User = require('../models/User');
  4. const router = express.Router();
  5. // 用户注册
  6. router.post('/register', async (req, res) => {
  7.   try {
  8.     const { username, email, password } = req.body;
  9.     // 验证输入
  10.     if (!username || !email || !password) {
  11.       return res.status(400).json({
  12.         message: 'Please provide username, email and password'
  13.       });
  14.     }
  15.     // 创建新用户
  16.     const user = User.create({ username, email, password });
  17.     // 返回用户信息(不包含密码)
  18.     res.status(201).json({
  19.       message: 'User created successfully',
  20.       user: user.toSafeObject()
  21.     });
  22.   } catch (error) {
  23.     res.status(400).json({ message: error.message });
  24.   }
  25. });
  26. module.exports = router;
复制代码

将路由集成到主应用

在index.js中添加认证路由:
  1. const authRouter = require('./routes/auth');
  2. // 使用认证路由
  3. app.use('/api/auth', authRouter);
复制代码

实现用户登录:生成JWT令牌

现在,让我们实现用户登录功能,并在成功登录后生成JWT。

扩展认证路由

在routes/auth.js中添加登录路由:
  1. // 用户登录
  2. router.post('/login', async (req, res) => {
  3.   try {
  4.     const { email, password } = req.body;
  5.     // 验证输入
  6.     if (!email || !password) {
  7.       return res.status(400).json({
  8.         message: 'Please provide email and password'
  9.       });
  10.     }
  11.     // 查找用户
  12.     const user = User.findByEmail(email);
  13.     if (!user) {
  14.       return res.status(401).json({ message: 'Invalid credentials' });
  15.     }
  16.     // 验证密码
  17.     const isPasswordValid = user.validatePassword(password);
  18.     if (!isPasswordValid) {
  19.       return res.status(401).json({ message: 'Invalid credentials' });
  20.     }
  21.     // 生成JWT
  22.     const token = jwt.sign(
  23.       {
  24.         sub: user.id,
  25.         username: user.username,
  26.         email: user.email
  27.       },
  28.       process.env.JWT_SECRET,
  29.       { expiresIn: process.env.JWT_EXPIRES_IN }
  30.     );
  31.     // 返回令牌和用户信息
  32.     res.json({
  33.       message: 'Login successful',
  34.       token,
  35.       user: user.toSafeObject()
  36.     });
  37.   } catch (error) {
  38.     res.status(500).json({ message: error.message });
  39.   }
  40. });
复制代码

JWT验证中间件:保护路由

为了保护需要认证的路由,我们需要创建一个中间件来验证JWT。

创建认证中间件

创建middleware/auth.js文件:
  1. const jwt = require('jsonwebtoken');
  2. module.exports = function(req, res, next) {
  3.   // 从请求头获取令牌
  4.   const authHeader = req.headers['authorization'];
  5.   const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
  6.   if (!token) {
  7.     return res.status(401).json({ message: 'Access token required' });
  8.   }
  9.   // 验证令牌
  10.   jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
  11.     if (err) {
  12.       return res.status(403).json({ message: 'Invalid or expired token' });
  13.     }
  14.     // 将用户信息添加到请求对象
  15.     req.user = user;
  16.     next();
  17.   });
  18. };
复制代码

可选角色验证中间件

创建middleware/roles.js文件,用于基于角色的访问控制:
  1. module.exports = function(...roles) {
  2.   return function(req, res, next) {
  3.     if (!req.user) {
  4.       return res.status(401).json({ message: 'Access denied' });
  5.     }
  6.     // 在实际应用中,你可能需要从数据库获取用户的角色
  7.     // 这里我们假设用户信息中包含角色
  8.     const userRoles = req.user.roles || [];
  9.    
  10.     const hasRole = roles.some(role => userRoles.includes(role));
  11.    
  12.     if (!hasRole) {
  13.       return res.status(403).json({ message: 'Insufficient permissions' });
  14.     }
  15.    
  16.     next();
  17.   };
  18. };
复制代码

受保护路由:实现需要认证的API

现在,让我们创建一些需要JWT认证的路由。

创建受保护的路由

创建routes/protected.js文件:
  1. const express = require('express');
  2. const authMiddleware = require('../middleware/auth');
  3. const rolesMiddleware = require('../middleware/roles');
  4. const router = express.Router();
  5. // 所有认证用户都可以访问的路由
  6. router.get('/profile', authMiddleware, (req, res) => {
  7.   // 在实际应用中,你可能需要从数据库获取完整的用户信息
  8.   res.json({
  9.     message: 'Access granted to protected profile',
  10.     user: req.user
  11.   });
  12. });
  13. // 只有管理员可以访问的路由
  14. router.get('/admin', authMiddleware, rolesMiddleware('admin'), (req, res) => {
  15.   res.json({
  16.     message: 'Welcome, admin!',
  17.     user: req.user
  18.   });
  19. });
  20. module.exports = router;
复制代码

将受保护路由集成到主应用

在index.js中添加受保护路由:
  1. const protectedRouter = require('./routes/protected');
  2. // 使用受保护路由
  3. app.use('/api/protected', protectedRouter);
复制代码

JWT刷新令牌:实现令牌刷新机制

为了提高安全性并改善用户体验,我们可以实现JWT刷新令牌机制。这样,当访问令牌过期时,客户端可以使用刷新令牌获取新的访问令牌,而无需重新登录。

修改用户模型

更新models/User.js以支持刷新令牌:
  1. const bcrypt = require('bcrypt');
  2. const crypto = require('crypto');
  3. // 模拟数据库
  4. let users = [];
  5. let refreshTokens = []; // 存储刷新令牌
  6. class User {
  7.   constructor(userData) {
  8.     this.id = users.length + 1;
  9.     this.username = userData.username;
  10.     this.email = userData.email;
  11.     this.passwordHash = this.hashPassword(userData.password);
  12.     this.createdAt = new Date();
  13.   }
  14.   // 哈希密码
  15.   hashPassword(password) {
  16.     return bcrypt.hashSync(password, parseInt(process.env.BCRYPT_SALT_ROUNDS));
  17.   }
  18.   // 验证密码
  19.   validatePassword(password) {
  20.     return bcrypt.compareSync(password, this.passwordHash);
  21.   }
  22.   // 转换为安全对象(不包含密码)
  23.   toSafeObject() {
  24.     const { passwordHash, ...userWithoutPassword } = this;
  25.     return userWithoutPassword;
  26.   }
  27.   // 静态方法:查找用户
  28.   static findByEmail(email) {
  29.     return users.find(user => user.email === email);
  30.   }
  31.   // 静态方法:查找用户ByID
  32.   static findById(id) {
  33.     return users.find(user => user.id === id);
  34.   }
  35.   // 静态方法:创建用户
  36.   static create(userData) {
  37.     // 检查用户是否已存在
  38.     if (this.findByEmail(userData.email)) {
  39.       throw new Error('User already exists');
  40.     }
  41.     const user = new User(userData);
  42.     users.push(user);
  43.     return user;
  44.   }
  45.   // 静态方法:添加刷新令牌
  46.   static addRefreshToken(userId, token) {
  47.     refreshTokens.push({
  48.       userId,
  49.       token,
  50.       expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) // 7天后过期
  51.     });
  52.   }
  53.   // 静态方法:验证刷新令牌
  54.   static verifyRefreshToken(userId, token) {
  55.     const refreshToken = refreshTokens.find(
  56.       rt => rt.userId === userId && rt.token === token
  57.     );
  58.    
  59.     if (!refreshToken) {
  60.       return false;
  61.     }
  62.    
  63.     // 检查是否过期
  64.     if (refreshToken.expiresAt < new Date()) {
  65.       // 删除过期令牌
  66.       refreshTokens = refreshTokens.filter(rt => rt.token !== token);
  67.       return false;
  68.     }
  69.    
  70.     return true;
  71.   }
  72.   // 静态方法:删除刷新令牌
  73.   static removeRefreshToken(token) {
  74.     refreshTokens = refreshTokens.filter(rt => rt.token !== token);
  75.   }
  76. }
  77. module.exports = User;
复制代码

更新认证路由

在routes/auth.js中添加令牌刷新和注销功能:
  1. const crypto = require('crypto');
  2. // 生成随机刷新令牌
  3. function generateRefreshToken() {
  4.   return crypto.randomBytes(40).toString('hex');
  5. }
  6. // 用户登录(更新版)
  7. router.post('/login', async (req, res) => {
  8.   try {
  9.     const { email, password } = req.body;
  10.     // 验证输入
  11.     if (!email || !password) {
  12.       return res.status(400).json({
  13.         message: 'Please provide email and password'
  14.       });
  15.     }
  16.     // 查找用户
  17.     const user = User.findByEmail(email);
  18.     if (!user) {
  19.       return res.status(401).json({ message: 'Invalid credentials' });
  20.     }
  21.     // 验证密码
  22.     const isPasswordValid = user.validatePassword(password);
  23.     if (!isPasswordValid) {
  24.       return res.status(401).json({ message: 'Invalid credentials' });
  25.     }
  26.     // 生成访问令牌
  27.     const accessToken = jwt.sign(
  28.       {
  29.         sub: user.id,
  30.         username: user.username,
  31.         email: user.email
  32.       },
  33.       process.env.JWT_SECRET,
  34.       { expiresIn: process.env.JWT_EXPIRES_IN }
  35.     );
  36.     // 生成刷新令牌
  37.     const refreshToken = generateRefreshToken();
  38.     User.addRefreshToken(user.id, refreshToken);
  39.     // 返回令牌和用户信息
  40.     res.json({
  41.       message: 'Login successful',
  42.       accessToken,
  43.       refreshToken,
  44.       user: user.toSafeObject()
  45.     });
  46.   } catch (error) {
  47.     res.status(500).json({ message: error.message });
  48.   }
  49. });
  50. // 刷新令牌
  51. router.post('/refresh-token', (req, res) => {
  52.   try {
  53.     const { refreshToken } = req.body;
  54.    
  55.     if (!refreshToken) {
  56.       return res.status(401).json({ message: 'Refresh token required' });
  57.     }
  58.    
  59.     // 验证刷新令牌
  60.     const decoded = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
  61.    
  62.     // 检查刷新令牌是否在数据库中
  63.     const isValid = User.verifyRefreshToken(decoded.sub, refreshToken);
  64.     if (!isValid) {
  65.       return res.status(403).json({ message: 'Invalid refresh token' });
  66.     }
  67.    
  68.     // 获取用户信息
  69.     const user = User.findById(decoded.sub);
  70.     if (!user) {
  71.       return res.status(404).json({ message: 'User not found' });
  72.     }
  73.    
  74.     // 生成新的访问令牌
  75.     const newAccessToken = jwt.sign(
  76.       {
  77.         sub: user.id,
  78.         username: user.username,
  79.         email: user.email
  80.       },
  81.       process.env.JWT_SECRET,
  82.       { expiresIn: process.env.JWT_EXPIRES_IN }
  83.     );
  84.    
  85.     res.json({
  86.       accessToken: newAccessToken
  87.     });
  88.   } catch (error) {
  89.     res.status(403).json({ message: 'Invalid or expired refresh token' });
  90.   }
  91. });
  92. // 注销
  93. router.post('/logout', authMiddleware, (req, res) => {
  94.   try {
  95.     const { refreshToken } = req.body;
  96.    
  97.     if (refreshToken) {
  98.       // 删除刷新令牌
  99.       User.removeRefreshToken(refreshToken);
  100.     }
  101.    
  102.     res.json({ message: 'Logout successful' });
  103.   } catch (error) {
  104.     res.status(500).json({ message: error.message });
  105.   }
  106. });
复制代码

更新环境变量

在.env文件中添加刷新令牌相关的配置:
  1. PORT=3000
  2. JWT_SECRET=your_super_secret_key
  3. JWT_EXPIRES_IN=1h
  4. JWT_REFRESH_SECRET=your_super_refresh_secret_key
  5. BCRYPT_SALT_ROUNDS=10
  6. MONGODB_URI=mongodb://localhost:27017/jwt_auth
复制代码

安全最佳实践:提高JWT安全性

使用JWT时,遵循安全最佳实践至关重要,以防止常见的安全漏洞。

1. 使用强密钥

确保你的JWT密钥足够强大且随机。不要使用简单或常见的字符串。
  1. // 生成强随机密钥的示例
  2. const crypto = require('crypto');
  3. const jwtSecret = crypto.randomBytes(64).toString('hex');
复制代码

2. 设置合理的过期时间

访问令牌应该有较短的过期时间(如15分钟到1小时),而刷新令牌可以有较长的过期时间(如几天到几周)。
  1. // 短期访问令牌
  2. const accessToken = jwt.sign(payload, secret, { expiresIn: '15m' });
  3. // 长期刷新令牌
  4. const refreshToken = jwt.sign(payload, refreshSecret, { expiresIn: '7d' });
复制代码

3. 使用HTTPS

始终通过HTTPS传输JWT,以防止中间人攻击。
  1. // 在生产环境中,确保使用HTTPS
  2. if (process.env.NODE_ENV === 'production') {
  3.   app.use(express.redirect('https'));
  4. }
复制代码

4. 存储令牌的安全方式

• 访问令牌:可以存储在内存中或短期存储在浏览器的会话存储中
• 刷新令牌:应该存储在HttpOnly cookie中,以防止XSS攻击
  1. // 设置HttpOnly cookie的示例
  2. res.cookie('refreshToken', refreshToken, {
  3.   httpOnly: true,
  4.   secure: process.env.NODE_ENV === 'production',
  5.   sameSite: 'strict',
  6.   maxAge: 7 * 24 * 60 * 60 * 1000 // 7天
  7. });
复制代码

5. 实现令牌撤销机制

提供一种机制来撤销特定的令牌,例如在用户更改密码或注销时。
  1. // 令牌黑名单示例
  2. const tokenBlacklist = new Set();
  3. // 在注销时添加令牌到黑名单
  4. router.post('/logout', (req, res) => {
  5.   const token = req.headers.authorization?.split(' ')[1];
  6.   if (token) {
  7.     tokenBlacklist.add(token);
  8.   }
  9.   res.json({ message: 'Logged out successfully' });
  10. });
  11. // 在中间件中检查黑名单
  12. const authMiddleware = (req, res, next) => {
  13.   const token = req.headers.authorization?.split(' ')[1];
  14.   
  15.   if (!token) {
  16.     return res.status(401).json({ message: 'No token provided' });
  17.   }
  18.   
  19.   if (tokenBlacklist.has(token)) {
  20.     return res.status(401).json({ message: 'Token revoked' });
  21.   }
  22.   
  23.   jwt.verify(token, process.env.JWT_SECRET, (err, decoded) => {
  24.     if (err) {
  25.       return res.status(403).json({ message: 'Failed to authenticate token' });
  26.     }
  27.    
  28.     req.user = decoded;
  29.     next();
  30.   });
  31. };
复制代码

6. 避免在JWT中存储敏感信息

JWT的Payload只是Base64编码的,不是加密的。不要在JWT中存储敏感信息,如密码、信用卡号等。
  1. // 好的做法 - 只存储非敏感信息
  2. const token = jwt.sign(
  3.   {
  4.     sub: user.id,
  5.     username: user.username,
  6.     role: user.role
  7.   },
  8.   secret
  9. );
  10. // 不好的做法 - 存储敏感信息
  11. const token = jwt.sign(
  12.   {
  13.     sub: user.id,
  14.     username: user.username,
  15.     password: user.password, // 不要这样做!
  16.     creditCard: user.creditCard // 不要这样做!
  17.   },
  18.   secret
  19. );
复制代码

7. 使用适当的签名算法

使用强签名算法,如HS256或RS256,避免使用无签名算法(如none)。
  1. // 使用HS256算法
  2. const token = jwt.sign(payload, secret, { algorithm: 'HS256' });
  3. // 或者使用RS256算法(非对称加密)
  4. const token = jwt.sign(payload, privateKey, { algorithm: 'RS256' });
复制代码

完整示例:一个完整的认证系统示例

让我们整合所有部分,创建一个完整的认证系统示例。

完整的index.js文件
  1. require('dotenv').config();
  2. const express = require('express');
  3. const cors = require('cors');
  4. const helmet = require('helmet');
  5. const morgan = require('morgan');
  6. const crypto = require('crypto');
  7. const authRouter = require('./routes/auth');
  8. const protectedRouter = require('./routes/protected');
  9. const app = express();
  10. // 中间件
  11. app.use(helmet()); // 安全增强
  12. app.use(cors()); // 允许跨域
  13. app.use(morgan('combined')); // 日志记录
  14. app.use(express.json()); // 解析JSON请求体
  15. app.use(express.urlencoded({ extended: true })); // 解析URL编码的请求体
  16. // 基本路由
  17. app.get('/', (req, res) => {
  18.   res.json({ message: 'Welcome to JWT Authentication API' });
  19. });
  20. // 使用路由
  21. app.use('/api/auth', authRouter);
  22. app.use('/api/protected', protectedRouter);
  23. // 错误处理中间件
  24. app.use((err, req, res, next) => {
  25.   console.error(err.stack);
  26.   res.status(500).json({ message: 'Something went wrong!' });
  27. });
  28. // 启动服务器
  29. const PORT = process.env.PORT || 3000;
  30. app.listen(PORT, () => {
  31.   console.log(`Server is running on port ${PORT}`);
  32. });
复制代码

完整的models/User.js文件
  1. const bcrypt = require('bcrypt');
  2. const crypto = require('crypto');
  3. // 模拟数据库
  4. let users = [];
  5. let refreshTokens = []; // 存储刷新令牌
  6. class User {
  7.   constructor(userData) {
  8.     this.id = users.length + 1;
  9.     this.username = userData.username;
  10.     this.email = userData.email;
  11.     this.passwordHash = this.hashPassword(userData.password);
  12.     this.role = userData.role || 'user'; // 默认角色为user
  13.     this.createdAt = new Date();
  14.   }
  15.   // 哈希密码
  16.   hashPassword(password) {
  17.     return bcrypt.hashSync(password, parseInt(process.env.BCRYPT_SALT_ROUNDS));
  18.   }
  19.   // 验证密码
  20.   validatePassword(password) {
  21.     return bcrypt.compareSync(password, this.passwordHash);
  22.   }
  23.   // 转换为安全对象(不包含密码)
  24.   toSafeObject() {
  25.     const { passwordHash, ...userWithoutPassword } = this;
  26.     return userWithoutPassword;
  27.   }
  28.   // 静态方法:查找用户
  29.   static findByEmail(email) {
  30.     return users.find(user => user.email === email);
  31.   }
  32.   // 静态方法:查找用户ByID
  33.   static findById(id) {
  34.     return users.find(user => user.id === id);
  35.   }
  36.   // 静态方法:创建用户
  37.   static create(userData) {
  38.     // 检查用户是否已存在
  39.     if (this.findByEmail(userData.email)) {
  40.       throw new Error('User already exists');
  41.     }
  42.     const user = new User(userData);
  43.     users.push(user);
  44.     return user;
  45.   }
  46.   // 静态方法:添加刷新令牌
  47.   static addRefreshToken(userId, token) {
  48.     // 移除该用户的所有现有刷新令牌
  49.     refreshTokens = refreshTokens.filter(rt => rt.userId !== userId);
  50.    
  51.     // 添加新刷新令牌
  52.     refreshTokens.push({
  53.       userId,
  54.       token,
  55.       expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) // 7天后过期
  56.     });
  57.   }
  58.   // 静态方法:验证刷新令牌
  59.   static verifyRefreshToken(userId, token) {
  60.     const refreshToken = refreshTokens.find(
  61.       rt => rt.userId === userId && rt.token === token
  62.     );
  63.    
  64.     if (!refreshToken) {
  65.       return false;
  66.     }
  67.    
  68.     // 检查是否过期
  69.     if (refreshToken.expiresAt < new Date()) {
  70.       // 删除过期令牌
  71.       refreshTokens = refreshTokens.filter(rt => rt.token !== token);
  72.       return false;
  73.     }
  74.    
  75.     return true;
  76.   }
  77.   // 静态方法:删除刷新令牌
  78.   static removeRefreshToken(token) {
  79.     refreshTokens = refreshTokens.filter(rt => rt.token !== token);
  80.   }
  81. }
  82. // 创建一个管理员用户(用于测试)
  83. if (users.length === 0) {
  84.   User.create({
  85.     username: 'admin',
  86.     email: 'admin@example.com',
  87.     password: 'admin123',
  88.     role: 'admin'
  89.   });
  90. }
  91. module.exports = User;
复制代码

完整的routes/auth.js文件
  1. const express = require('express');
  2. const jwt = require('jsonwebtoken');
  3. const User = require('../models/User');
  4. const router = express.Router();
  5. // 生成随机刷新令牌
  6. function generateRefreshToken() {
  7.   return crypto.randomBytes(40).toString('hex');
  8. }
  9. // 用户注册
  10. router.post('/register', async (req, res) => {
  11.   try {
  12.     const { username, email, password, role } = req.body;
  13.     // 验证输入
  14.     if (!username || !email || !password) {
  15.       return res.status(400).json({
  16.         message: 'Please provide username, email and password'
  17.       });
  18.     }
  19.     // 创建新用户
  20.     const user = User.create({ username, email, password, role });
  21.     // 返回用户信息(不包含密码)
  22.     res.status(201).json({
  23.       message: 'User created successfully',
  24.       user: user.toSafeObject()
  25.     });
  26.   } catch (error) {
  27.     res.status(400).json({ message: error.message });
  28.   }
  29. });
  30. // 用户登录
  31. router.post('/login', async (req, res) => {
  32.   try {
  33.     const { email, password } = req.body;
  34.     // 验证输入
  35.     if (!email || !password) {
  36.       return res.status(400).json({
  37.         message: 'Please provide email and password'
  38.       });
  39.     }
  40.     // 查找用户
  41.     const user = User.findByEmail(email);
  42.     if (!user) {
  43.       return res.status(401).json({ message: 'Invalid credentials' });
  44.     }
  45.     // 验证密码
  46.     const isPasswordValid = user.validatePassword(password);
  47.     if (!isPasswordValid) {
  48.       return res.status(401).json({ message: 'Invalid credentials' });
  49.     }
  50.     // 生成访问令牌
  51.     const accessToken = jwt.sign(
  52.       {
  53.         sub: user.id,
  54.         username: user.username,
  55.         email: user.email,
  56.         role: user.role
  57.       },
  58.       process.env.JWT_SECRET,
  59.       { expiresIn: process.env.JWT_EXPIRES_IN }
  60.     );
  61.     // 生成刷新令牌
  62.     const refreshToken = generateRefreshToken();
  63.     User.addRefreshToken(user.id, refreshToken);
  64.     // 返回令牌和用户信息
  65.     res.json({
  66.       message: 'Login successful',
  67.       accessToken,
  68.       refreshToken,
  69.       user: user.toSafeObject()
  70.     });
  71.   } catch (error) {
  72.     res.status(500).json({ message: error.message });
  73.   }
  74. });
  75. // 刷新令牌
  76. router.post('/refresh-token', (req, res) => {
  77.   try {
  78.     const { refreshToken } = req.body;
  79.    
  80.     if (!refreshToken) {
  81.       return res.status(401).json({ message: 'Refresh token required' });
  82.     }
  83.    
  84.     // 验证刷新令牌
  85.     const decoded = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
  86.    
  87.     // 检查刷新令牌是否在数据库中
  88.     const isValid = User.verifyRefreshToken(decoded.sub, refreshToken);
  89.     if (!isValid) {
  90.       return res.status(403).json({ message: 'Invalid refresh token' });
  91.     }
  92.    
  93.     // 获取用户信息
  94.     const user = User.findById(decoded.sub);
  95.     if (!user) {
  96.       return res.status(404).json({ message: 'User not found' });
  97.     }
  98.    
  99.     // 生成新的访问令牌
  100.     const newAccessToken = jwt.sign(
  101.       {
  102.         sub: user.id,
  103.         username: user.username,
  104.         email: user.email,
  105.         role: user.role
  106.       },
  107.       process.env.JWT_SECRET,
  108.       { expiresIn: process.env.JWT_EXPIRES_IN }
  109.     );
  110.    
  111.     res.json({
  112.       accessToken: newAccessToken
  113.     });
  114.   } catch (error) {
  115.     res.status(403).json({ message: 'Invalid or expired refresh token' });
  116.   }
  117. });
  118. // 注销
  119. router.post('/logout', (req, res) => {
  120.   try {
  121.     const { refreshToken } = req.body;
  122.    
  123.     if (refreshToken) {
  124.       // 删除刷新令牌
  125.       User.removeRefreshToken(refreshToken);
  126.     }
  127.    
  128.     res.json({ message: 'Logout successful' });
  129.   } catch (error) {
  130.     res.status(500).json({ message: error.message });
  131.   }
  132. });
  133. module.exports = router;
复制代码

完整的routes/protected.js文件
  1. const express = require('express');
  2. const authMiddleware = require('../middleware/auth');
  3. const rolesMiddleware = require('../middleware/roles');
  4. const router = express.Router();
  5. // 所有认证用户都可以访问的路由
  6. router.get('/profile', authMiddleware, (req, res) => {
  7.   // 在实际应用中,你可能需要从数据库获取完整的用户信息
  8.   res.json({
  9.     message: 'Access granted to protected profile',
  10.     user: req.user
  11.   });
  12. });
  13. // 只有管理员可以访问的路由
  14. router.get('/admin', authMiddleware, rolesMiddleware('admin'), (req, res) => {
  15.   res.json({
  16.     message: 'Welcome, admin!',
  17.     user: req.user
  18.   });
  19. });
  20. // 普通用户和管理员都可以访问的路由
  21. router.get('/user-or-admin', authMiddleware, rolesMiddleware('user', 'admin'), (req, res) => {
  22.   res.json({
  23.     message: 'Welcome, user or admin!',
  24.     user: req.user
  25.   });
  26. });
  27. module.exports = router;
复制代码

完整的middleware/auth.js文件
  1. const jwt = require('jsonwebtoken');
  2. module.exports = function(req, res, next) {
  3.   // 从请求头获取令牌
  4.   const authHeader = req.headers['authorization'];
  5.   const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
  6.   if (!token) {
  7.     return res.status(401).json({ message: 'Access token required' });
  8.   }
  9.   // 验证令牌
  10.   jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
  11.     if (err) {
  12.       return res.status(403).json({ message: 'Invalid or expired token' });
  13.     }
  14.     // 将用户信息添加到请求对象
  15.     req.user = user;
  16.     next();
  17.   });
  18. };
复制代码

完整的middleware/roles.js文件
  1. module.exports = function(...roles) {
  2.   return function(req, res, next) {
  3.     if (!req.user) {
  4.       return res.status(401).json({ message: 'Access denied' });
  5.     }
  6.     // 在实际应用中,你可能需要从数据库获取用户的角色
  7.     // 这里我们假设用户信息中包含角色
  8.     const userRole = req.user.role;
  9.    
  10.     const hasRole = roles.includes(userRole);
  11.    
  12.     if (!hasRole) {
  13.       return res.status(403).json({ message: 'Insufficient permissions' });
  14.     }
  15.    
  16.     next();
  17.   };
  18. };
复制代码

完整的.env文件
  1. PORT=3000
  2. JWT_SECRET=your_super_secret_key_that_should_be_very_long_and_random
  3. JWT_EXPIRES_IN=1h
  4. JWT_REFRESH_SECRET=your_super_refresh_secret_key_that_should_also_be_long_and_random
  5. BCRYPT_SALT_ROUNDS=10
  6. MONGODB_URI=mongodb://localhost:27017/jwt_auth
复制代码

如何使用这个认证系统
  1. curl -X POST http://localhost:3000/api/auth/register \
  2.   -H "Content-Type: application/json" \
  3.   -d '{"username":"john", "email":"john@example.com", "password":"password123"}'
复制代码
  1. curl -X POST http://localhost:3000/api/auth/login \
  2.   -H "Content-Type: application/json" \
  3.   -d '{"email":"john@example.com", "password":"password123"}'
复制代码
  1. # 将YOUR_ACCESS_TOKEN替换为登录后获得的令牌
  2. curl -X GET http://localhost:3000/api/protected/profile \
  3.   -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
复制代码
  1. curl -X POST http://localhost:3000/api/auth/refresh-token \
  2.   -H "Content-Type: application/json" \
  3.   -d '{"refreshToken":"YOUR_REFRESH_TOKEN"}'
复制代码
  1. curl -X POST http://localhost:3000/api/auth/logout \
  2.   -H "Content-Type: application/json" \
  3.   -d '{"refreshToken":"YOUR_REFRESH_TOKEN"}'
复制代码

总结:构建安全高效的认证系统

通过本文,我们详细介绍了如何使用Node.js和JWT构建一个完整、安全的用户认证系统。我们从JWT的基础知识开始,逐步实现了用户注册、登录、令牌验证、令牌刷新和注销等功能,并讨论了安全最佳实践。

关键要点回顾

1. JWT的优势:JWT是无状态的、自包含的,适合分布式系统和跨域场景。
2. 安全性:使用强密钥、设置合理的过期时间、使用HTTPS、安全存储令牌等措施至关重要。
3. 令牌刷新:实现刷新令牌机制可以提高用户体验和安全性。
4. 角色访问控制:基于角色的访问控制可以灵活地管理用户权限。
5. 最佳实践:避免在JWT中存储敏感信息,使用适当的签名算法,实现令牌撤销机制等。

进一步改进方向

虽然我们已经构建了一个功能完整的认证系统,但仍有改进的空间:

1. 数据库集成:将用户数据存储在MongoDB、MySQL或PostgreSQL等数据库中,而不是内存数组。
2. 密码策略:实现密码强度检查、密码历史记录和密码过期策略。
3. 账户锁定:在多次登录失败后锁定账户,防止暴力破解。
4. 多因素认证:添加短信或电子邮件验证码、TOTP等多因素认证。
5. 审计日志:记录用户登录、注销和重要操作,以便审计和安全分析。
6. OAuth集成:支持通过Google、Facebook等第三方服务登录。

结语

JWT和Node.js的结合为构建现代Web应用的认证系统提供了强大而灵活的解决方案。通过遵循本文介绍的最佳实践和实现方法,你可以构建一个既安全又高效的认证系统,为你的应用提供可靠的安全保障。

随着技术的不断发展,认证和安全领域也在不断演进。保持对最新安全趋势和最佳实践的关注,持续改进你的认证系统,是确保应用长期安全的关键。
「七転び八起き(ななころびやおき)」
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则