|
|
马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
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示例:
- // Header
- {
- "alg": "HS256",
- "typ": "JWT"
- }
- // Payload
- {
- "sub": "1234567890",
- "name": "John Doe",
- "iat": 1516239022
- }
- // Signature
- HMACSHA256(
- base64UrlEncode(header) + "." +
- base64UrlEncode(payload),
- 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项目:
- mkdir node-jwt-auth
- cd node-jwt-auth
- npm init -y
复制代码
安装依赖
安装所需的依赖包:
- npm install express jsonwebtoken bcrypt cors dotenv helmet morgan
- npm install --save-dev nodemon
复制代码
这些依赖的作用:
• express: Node.js的Web框架
• jsonwebtoken: 用于生成和验证JWT
• bcrypt: 用于密码哈希和验证
• cors: 处理跨域资源共享
• dotenv: 加载环境变量
• helmet: 增强应用安全性
• morgan: HTTP请求日志记录器
• nodemon: 开发时自动重启服务器
创建基本服务器
创建index.js文件并设置基本的Express服务器:
- require('dotenv').config();
- const express = require('express');
- const cors = require('cors');
- const helmet = require('helmet');
- const morgan = require('morgan');
- const app = express();
- // 中间件
- app.use(helmet()); // 安全增强
- app.use(cors()); // 允许跨域
- app.use(morgan('combined')); // 日志记录
- app.use(express.json()); // 解析JSON请求体
- app.use(express.urlencoded({ extended: true })); // 解析URL编码的请求体
- // 基本路由
- app.get('/', (req, res) => {
- res.json({ message: 'Welcome to JWT Authentication API' });
- });
- // 启动服务器
- const PORT = process.env.PORT || 3000;
- app.listen(PORT, () => {
- console.log(`Server is running on port ${PORT}`);
- });
复制代码
配置环境变量
创建.env文件来存储敏感信息:
- PORT=3000
- JWT_SECRET=your_super_secret_key
- JWT_EXPIRES_IN=1h
- BCRYPT_SALT_ROUNDS=10
- MONGODB_URI=mongodb://localhost:27017/jwt_auth
复制代码
实现用户注册:创建用户账户
设置数据库模型
为了简单起见,我们将使用内存数组来模拟数据库。在实际应用中,你应该使用像MongoDB、MySQL或PostgreSQL这样的数据库。
创建models/User.js文件:
- const bcrypt = require('bcrypt');
- // 模拟数据库
- let users = [];
- class User {
- constructor(userData) {
- this.id = users.length + 1;
- this.username = userData.username;
- this.email = userData.email;
- this.passwordHash = this.hashPassword(userData.password);
- this.createdAt = new Date();
- }
- // 哈希密码
- hashPassword(password) {
- return bcrypt.hashSync(password, parseInt(process.env.BCRYPT_SALT_ROUNDS));
- }
- // 验证密码
- validatePassword(password) {
- return bcrypt.compareSync(password, this.passwordHash);
- }
- // 转换为安全对象(不包含密码)
- toSafeObject() {
- const { passwordHash, ...userWithoutPassword } = this;
- return userWithoutPassword;
- }
- // 静态方法:查找用户
- static findByEmail(email) {
- return users.find(user => user.email === email);
- }
- // 静态方法:查找用户ByID
- static findById(id) {
- return users.find(user => user.id === id);
- }
- // 静态方法:创建用户
- static create(userData) {
- // 检查用户是否已存在
- if (this.findByEmail(userData.email)) {
- throw new Error('User already exists');
- }
- const user = new User(userData);
- users.push(user);
- return user;
- }
- }
- module.exports = User;
复制代码
创建注册路由
创建routes/auth.js文件来处理认证相关的路由:
- const express = require('express');
- const jwt = require('jsonwebtoken');
- const User = require('../models/User');
- const router = express.Router();
- // 用户注册
- router.post('/register', async (req, res) => {
- try {
- const { username, email, password } = req.body;
- // 验证输入
- if (!username || !email || !password) {
- return res.status(400).json({
- message: 'Please provide username, email and password'
- });
- }
- // 创建新用户
- const user = User.create({ username, email, password });
- // 返回用户信息(不包含密码)
- res.status(201).json({
- message: 'User created successfully',
- user: user.toSafeObject()
- });
- } catch (error) {
- res.status(400).json({ message: error.message });
- }
- });
- module.exports = router;
复制代码
将路由集成到主应用
在index.js中添加认证路由:
- const authRouter = require('./routes/auth');
- // 使用认证路由
- app.use('/api/auth', authRouter);
复制代码
实现用户登录:生成JWT令牌
现在,让我们实现用户登录功能,并在成功登录后生成JWT。
扩展认证路由
在routes/auth.js中添加登录路由:
- // 用户登录
- router.post('/login', async (req, res) => {
- try {
- const { email, password } = req.body;
- // 验证输入
- if (!email || !password) {
- return res.status(400).json({
- message: 'Please provide email and password'
- });
- }
- // 查找用户
- const user = User.findByEmail(email);
- if (!user) {
- return res.status(401).json({ message: 'Invalid credentials' });
- }
- // 验证密码
- const isPasswordValid = user.validatePassword(password);
- if (!isPasswordValid) {
- return res.status(401).json({ message: 'Invalid credentials' });
- }
- // 生成JWT
- const token = jwt.sign(
- {
- sub: user.id,
- username: user.username,
- email: user.email
- },
- process.env.JWT_SECRET,
- { expiresIn: process.env.JWT_EXPIRES_IN }
- );
- // 返回令牌和用户信息
- res.json({
- message: 'Login successful',
- token,
- user: user.toSafeObject()
- });
- } catch (error) {
- res.status(500).json({ message: error.message });
- }
- });
复制代码
JWT验证中间件:保护路由
为了保护需要认证的路由,我们需要创建一个中间件来验证JWT。
创建认证中间件
创建middleware/auth.js文件:
- const jwt = require('jsonwebtoken');
- module.exports = function(req, res, next) {
- // 从请求头获取令牌
- const authHeader = req.headers['authorization'];
- const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
- if (!token) {
- return res.status(401).json({ message: 'Access token required' });
- }
- // 验证令牌
- jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
- if (err) {
- return res.status(403).json({ message: 'Invalid or expired token' });
- }
- // 将用户信息添加到请求对象
- req.user = user;
- next();
- });
- };
复制代码
可选角色验证中间件
创建middleware/roles.js文件,用于基于角色的访问控制:
- module.exports = function(...roles) {
- return function(req, res, next) {
- if (!req.user) {
- return res.status(401).json({ message: 'Access denied' });
- }
- // 在实际应用中,你可能需要从数据库获取用户的角色
- // 这里我们假设用户信息中包含角色
- const userRoles = req.user.roles || [];
-
- const hasRole = roles.some(role => userRoles.includes(role));
-
- if (!hasRole) {
- return res.status(403).json({ message: 'Insufficient permissions' });
- }
-
- next();
- };
- };
复制代码
受保护路由:实现需要认证的API
现在,让我们创建一些需要JWT认证的路由。
创建受保护的路由
创建routes/protected.js文件:
- const express = require('express');
- const authMiddleware = require('../middleware/auth');
- const rolesMiddleware = require('../middleware/roles');
- const router = express.Router();
- // 所有认证用户都可以访问的路由
- router.get('/profile', authMiddleware, (req, res) => {
- // 在实际应用中,你可能需要从数据库获取完整的用户信息
- res.json({
- message: 'Access granted to protected profile',
- user: req.user
- });
- });
- // 只有管理员可以访问的路由
- router.get('/admin', authMiddleware, rolesMiddleware('admin'), (req, res) => {
- res.json({
- message: 'Welcome, admin!',
- user: req.user
- });
- });
- module.exports = router;
复制代码
将受保护路由集成到主应用
在index.js中添加受保护路由:
- const protectedRouter = require('./routes/protected');
- // 使用受保护路由
- app.use('/api/protected', protectedRouter);
复制代码
JWT刷新令牌:实现令牌刷新机制
为了提高安全性并改善用户体验,我们可以实现JWT刷新令牌机制。这样,当访问令牌过期时,客户端可以使用刷新令牌获取新的访问令牌,而无需重新登录。
修改用户模型
更新models/User.js以支持刷新令牌:
- const bcrypt = require('bcrypt');
- const crypto = require('crypto');
- // 模拟数据库
- let users = [];
- let refreshTokens = []; // 存储刷新令牌
- class User {
- constructor(userData) {
- this.id = users.length + 1;
- this.username = userData.username;
- this.email = userData.email;
- this.passwordHash = this.hashPassword(userData.password);
- this.createdAt = new Date();
- }
- // 哈希密码
- hashPassword(password) {
- return bcrypt.hashSync(password, parseInt(process.env.BCRYPT_SALT_ROUNDS));
- }
- // 验证密码
- validatePassword(password) {
- return bcrypt.compareSync(password, this.passwordHash);
- }
- // 转换为安全对象(不包含密码)
- toSafeObject() {
- const { passwordHash, ...userWithoutPassword } = this;
- return userWithoutPassword;
- }
- // 静态方法:查找用户
- static findByEmail(email) {
- return users.find(user => user.email === email);
- }
- // 静态方法:查找用户ByID
- static findById(id) {
- return users.find(user => user.id === id);
- }
- // 静态方法:创建用户
- static create(userData) {
- // 检查用户是否已存在
- if (this.findByEmail(userData.email)) {
- throw new Error('User already exists');
- }
- const user = new User(userData);
- users.push(user);
- return user;
- }
- // 静态方法:添加刷新令牌
- static addRefreshToken(userId, token) {
- refreshTokens.push({
- userId,
- token,
- expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) // 7天后过期
- });
- }
- // 静态方法:验证刷新令牌
- static verifyRefreshToken(userId, token) {
- const refreshToken = refreshTokens.find(
- rt => rt.userId === userId && rt.token === token
- );
-
- if (!refreshToken) {
- return false;
- }
-
- // 检查是否过期
- if (refreshToken.expiresAt < new Date()) {
- // 删除过期令牌
- refreshTokens = refreshTokens.filter(rt => rt.token !== token);
- return false;
- }
-
- return true;
- }
- // 静态方法:删除刷新令牌
- static removeRefreshToken(token) {
- refreshTokens = refreshTokens.filter(rt => rt.token !== token);
- }
- }
- module.exports = User;
复制代码
更新认证路由
在routes/auth.js中添加令牌刷新和注销功能:
- const crypto = require('crypto');
- // 生成随机刷新令牌
- function generateRefreshToken() {
- return crypto.randomBytes(40).toString('hex');
- }
- // 用户登录(更新版)
- router.post('/login', async (req, res) => {
- try {
- const { email, password } = req.body;
- // 验证输入
- if (!email || !password) {
- return res.status(400).json({
- message: 'Please provide email and password'
- });
- }
- // 查找用户
- const user = User.findByEmail(email);
- if (!user) {
- return res.status(401).json({ message: 'Invalid credentials' });
- }
- // 验证密码
- const isPasswordValid = user.validatePassword(password);
- if (!isPasswordValid) {
- return res.status(401).json({ message: 'Invalid credentials' });
- }
- // 生成访问令牌
- const accessToken = jwt.sign(
- {
- sub: user.id,
- username: user.username,
- email: user.email
- },
- process.env.JWT_SECRET,
- { expiresIn: process.env.JWT_EXPIRES_IN }
- );
- // 生成刷新令牌
- const refreshToken = generateRefreshToken();
- User.addRefreshToken(user.id, refreshToken);
- // 返回令牌和用户信息
- res.json({
- message: 'Login successful',
- accessToken,
- refreshToken,
- user: user.toSafeObject()
- });
- } catch (error) {
- res.status(500).json({ message: error.message });
- }
- });
- // 刷新令牌
- router.post('/refresh-token', (req, res) => {
- try {
- const { refreshToken } = req.body;
-
- if (!refreshToken) {
- return res.status(401).json({ message: 'Refresh token required' });
- }
-
- // 验证刷新令牌
- const decoded = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
-
- // 检查刷新令牌是否在数据库中
- const isValid = User.verifyRefreshToken(decoded.sub, refreshToken);
- if (!isValid) {
- return res.status(403).json({ message: 'Invalid refresh token' });
- }
-
- // 获取用户信息
- const user = User.findById(decoded.sub);
- if (!user) {
- return res.status(404).json({ message: 'User not found' });
- }
-
- // 生成新的访问令牌
- const newAccessToken = jwt.sign(
- {
- sub: user.id,
- username: user.username,
- email: user.email
- },
- process.env.JWT_SECRET,
- { expiresIn: process.env.JWT_EXPIRES_IN }
- );
-
- res.json({
- accessToken: newAccessToken
- });
- } catch (error) {
- res.status(403).json({ message: 'Invalid or expired refresh token' });
- }
- });
- // 注销
- router.post('/logout', authMiddleware, (req, res) => {
- try {
- const { refreshToken } = req.body;
-
- if (refreshToken) {
- // 删除刷新令牌
- User.removeRefreshToken(refreshToken);
- }
-
- res.json({ message: 'Logout successful' });
- } catch (error) {
- res.status(500).json({ message: error.message });
- }
- });
复制代码
更新环境变量
在.env文件中添加刷新令牌相关的配置:
- PORT=3000
- JWT_SECRET=your_super_secret_key
- JWT_EXPIRES_IN=1h
- JWT_REFRESH_SECRET=your_super_refresh_secret_key
- BCRYPT_SALT_ROUNDS=10
- MONGODB_URI=mongodb://localhost:27017/jwt_auth
复制代码
安全最佳实践:提高JWT安全性
使用JWT时,遵循安全最佳实践至关重要,以防止常见的安全漏洞。
1. 使用强密钥
确保你的JWT密钥足够强大且随机。不要使用简单或常见的字符串。
- // 生成强随机密钥的示例
- const crypto = require('crypto');
- const jwtSecret = crypto.randomBytes(64).toString('hex');
复制代码
2. 设置合理的过期时间
访问令牌应该有较短的过期时间(如15分钟到1小时),而刷新令牌可以有较长的过期时间(如几天到几周)。
- // 短期访问令牌
- const accessToken = jwt.sign(payload, secret, { expiresIn: '15m' });
- // 长期刷新令牌
- const refreshToken = jwt.sign(payload, refreshSecret, { expiresIn: '7d' });
复制代码
3. 使用HTTPS
始终通过HTTPS传输JWT,以防止中间人攻击。
- // 在生产环境中,确保使用HTTPS
- if (process.env.NODE_ENV === 'production') {
- app.use(express.redirect('https'));
- }
复制代码
4. 存储令牌的安全方式
• 访问令牌:可以存储在内存中或短期存储在浏览器的会话存储中
• 刷新令牌:应该存储在HttpOnly cookie中,以防止XSS攻击
- // 设置HttpOnly cookie的示例
- res.cookie('refreshToken', refreshToken, {
- httpOnly: true,
- secure: process.env.NODE_ENV === 'production',
- sameSite: 'strict',
- maxAge: 7 * 24 * 60 * 60 * 1000 // 7天
- });
复制代码
5. 实现令牌撤销机制
提供一种机制来撤销特定的令牌,例如在用户更改密码或注销时。
- // 令牌黑名单示例
- const tokenBlacklist = new Set();
- // 在注销时添加令牌到黑名单
- router.post('/logout', (req, res) => {
- const token = req.headers.authorization?.split(' ')[1];
- if (token) {
- tokenBlacklist.add(token);
- }
- res.json({ message: 'Logged out successfully' });
- });
- // 在中间件中检查黑名单
- const authMiddleware = (req, res, next) => {
- const token = req.headers.authorization?.split(' ')[1];
-
- if (!token) {
- return res.status(401).json({ message: 'No token provided' });
- }
-
- if (tokenBlacklist.has(token)) {
- return res.status(401).json({ message: 'Token revoked' });
- }
-
- jwt.verify(token, process.env.JWT_SECRET, (err, decoded) => {
- if (err) {
- return res.status(403).json({ message: 'Failed to authenticate token' });
- }
-
- req.user = decoded;
- next();
- });
- };
复制代码
6. 避免在JWT中存储敏感信息
JWT的Payload只是Base64编码的,不是加密的。不要在JWT中存储敏感信息,如密码、信用卡号等。
- // 好的做法 - 只存储非敏感信息
- const token = jwt.sign(
- {
- sub: user.id,
- username: user.username,
- role: user.role
- },
- secret
- );
- // 不好的做法 - 存储敏感信息
- const token = jwt.sign(
- {
- sub: user.id,
- username: user.username,
- password: user.password, // 不要这样做!
- creditCard: user.creditCard // 不要这样做!
- },
- secret
- );
复制代码
7. 使用适当的签名算法
使用强签名算法,如HS256或RS256,避免使用无签名算法(如none)。
- // 使用HS256算法
- const token = jwt.sign(payload, secret, { algorithm: 'HS256' });
- // 或者使用RS256算法(非对称加密)
- const token = jwt.sign(payload, privateKey, { algorithm: 'RS256' });
复制代码
完整示例:一个完整的认证系统示例
让我们整合所有部分,创建一个完整的认证系统示例。
完整的index.js文件
- require('dotenv').config();
- const express = require('express');
- const cors = require('cors');
- const helmet = require('helmet');
- const morgan = require('morgan');
- const crypto = require('crypto');
- const authRouter = require('./routes/auth');
- const protectedRouter = require('./routes/protected');
- const app = express();
- // 中间件
- app.use(helmet()); // 安全增强
- app.use(cors()); // 允许跨域
- app.use(morgan('combined')); // 日志记录
- app.use(express.json()); // 解析JSON请求体
- app.use(express.urlencoded({ extended: true })); // 解析URL编码的请求体
- // 基本路由
- app.get('/', (req, res) => {
- res.json({ message: 'Welcome to JWT Authentication API' });
- });
- // 使用路由
- app.use('/api/auth', authRouter);
- app.use('/api/protected', protectedRouter);
- // 错误处理中间件
- app.use((err, req, res, next) => {
- console.error(err.stack);
- res.status(500).json({ message: 'Something went wrong!' });
- });
- // 启动服务器
- const PORT = process.env.PORT || 3000;
- app.listen(PORT, () => {
- console.log(`Server is running on port ${PORT}`);
- });
复制代码
完整的models/User.js文件
- const bcrypt = require('bcrypt');
- const crypto = require('crypto');
- // 模拟数据库
- let users = [];
- let refreshTokens = []; // 存储刷新令牌
- class User {
- constructor(userData) {
- this.id = users.length + 1;
- this.username = userData.username;
- this.email = userData.email;
- this.passwordHash = this.hashPassword(userData.password);
- this.role = userData.role || 'user'; // 默认角色为user
- this.createdAt = new Date();
- }
- // 哈希密码
- hashPassword(password) {
- return bcrypt.hashSync(password, parseInt(process.env.BCRYPT_SALT_ROUNDS));
- }
- // 验证密码
- validatePassword(password) {
- return bcrypt.compareSync(password, this.passwordHash);
- }
- // 转换为安全对象(不包含密码)
- toSafeObject() {
- const { passwordHash, ...userWithoutPassword } = this;
- return userWithoutPassword;
- }
- // 静态方法:查找用户
- static findByEmail(email) {
- return users.find(user => user.email === email);
- }
- // 静态方法:查找用户ByID
- static findById(id) {
- return users.find(user => user.id === id);
- }
- // 静态方法:创建用户
- static create(userData) {
- // 检查用户是否已存在
- if (this.findByEmail(userData.email)) {
- throw new Error('User already exists');
- }
- const user = new User(userData);
- users.push(user);
- return user;
- }
- // 静态方法:添加刷新令牌
- static addRefreshToken(userId, token) {
- // 移除该用户的所有现有刷新令牌
- refreshTokens = refreshTokens.filter(rt => rt.userId !== userId);
-
- // 添加新刷新令牌
- refreshTokens.push({
- userId,
- token,
- expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) // 7天后过期
- });
- }
- // 静态方法:验证刷新令牌
- static verifyRefreshToken(userId, token) {
- const refreshToken = refreshTokens.find(
- rt => rt.userId === userId && rt.token === token
- );
-
- if (!refreshToken) {
- return false;
- }
-
- // 检查是否过期
- if (refreshToken.expiresAt < new Date()) {
- // 删除过期令牌
- refreshTokens = refreshTokens.filter(rt => rt.token !== token);
- return false;
- }
-
- return true;
- }
- // 静态方法:删除刷新令牌
- static removeRefreshToken(token) {
- refreshTokens = refreshTokens.filter(rt => rt.token !== token);
- }
- }
- // 创建一个管理员用户(用于测试)
- if (users.length === 0) {
- User.create({
- username: 'admin',
- email: 'admin@example.com',
- password: 'admin123',
- role: 'admin'
- });
- }
- module.exports = User;
复制代码
完整的routes/auth.js文件
- const express = require('express');
- const jwt = require('jsonwebtoken');
- const User = require('../models/User');
- const router = express.Router();
- // 生成随机刷新令牌
- function generateRefreshToken() {
- return crypto.randomBytes(40).toString('hex');
- }
- // 用户注册
- router.post('/register', async (req, res) => {
- try {
- const { username, email, password, role } = req.body;
- // 验证输入
- if (!username || !email || !password) {
- return res.status(400).json({
- message: 'Please provide username, email and password'
- });
- }
- // 创建新用户
- const user = User.create({ username, email, password, role });
- // 返回用户信息(不包含密码)
- res.status(201).json({
- message: 'User created successfully',
- user: user.toSafeObject()
- });
- } catch (error) {
- res.status(400).json({ message: error.message });
- }
- });
- // 用户登录
- router.post('/login', async (req, res) => {
- try {
- const { email, password } = req.body;
- // 验证输入
- if (!email || !password) {
- return res.status(400).json({
- message: 'Please provide email and password'
- });
- }
- // 查找用户
- const user = User.findByEmail(email);
- if (!user) {
- return res.status(401).json({ message: 'Invalid credentials' });
- }
- // 验证密码
- const isPasswordValid = user.validatePassword(password);
- if (!isPasswordValid) {
- return res.status(401).json({ message: 'Invalid credentials' });
- }
- // 生成访问令牌
- const accessToken = jwt.sign(
- {
- sub: user.id,
- username: user.username,
- email: user.email,
- role: user.role
- },
- process.env.JWT_SECRET,
- { expiresIn: process.env.JWT_EXPIRES_IN }
- );
- // 生成刷新令牌
- const refreshToken = generateRefreshToken();
- User.addRefreshToken(user.id, refreshToken);
- // 返回令牌和用户信息
- res.json({
- message: 'Login successful',
- accessToken,
- refreshToken,
- user: user.toSafeObject()
- });
- } catch (error) {
- res.status(500).json({ message: error.message });
- }
- });
- // 刷新令牌
- router.post('/refresh-token', (req, res) => {
- try {
- const { refreshToken } = req.body;
-
- if (!refreshToken) {
- return res.status(401).json({ message: 'Refresh token required' });
- }
-
- // 验证刷新令牌
- const decoded = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
-
- // 检查刷新令牌是否在数据库中
- const isValid = User.verifyRefreshToken(decoded.sub, refreshToken);
- if (!isValid) {
- return res.status(403).json({ message: 'Invalid refresh token' });
- }
-
- // 获取用户信息
- const user = User.findById(decoded.sub);
- if (!user) {
- return res.status(404).json({ message: 'User not found' });
- }
-
- // 生成新的访问令牌
- const newAccessToken = jwt.sign(
- {
- sub: user.id,
- username: user.username,
- email: user.email,
- role: user.role
- },
- process.env.JWT_SECRET,
- { expiresIn: process.env.JWT_EXPIRES_IN }
- );
-
- res.json({
- accessToken: newAccessToken
- });
- } catch (error) {
- res.status(403).json({ message: 'Invalid or expired refresh token' });
- }
- });
- // 注销
- router.post('/logout', (req, res) => {
- try {
- const { refreshToken } = req.body;
-
- if (refreshToken) {
- // 删除刷新令牌
- User.removeRefreshToken(refreshToken);
- }
-
- res.json({ message: 'Logout successful' });
- } catch (error) {
- res.status(500).json({ message: error.message });
- }
- });
- module.exports = router;
复制代码
完整的routes/protected.js文件
- const express = require('express');
- const authMiddleware = require('../middleware/auth');
- const rolesMiddleware = require('../middleware/roles');
- const router = express.Router();
- // 所有认证用户都可以访问的路由
- router.get('/profile', authMiddleware, (req, res) => {
- // 在实际应用中,你可能需要从数据库获取完整的用户信息
- res.json({
- message: 'Access granted to protected profile',
- user: req.user
- });
- });
- // 只有管理员可以访问的路由
- router.get('/admin', authMiddleware, rolesMiddleware('admin'), (req, res) => {
- res.json({
- message: 'Welcome, admin!',
- user: req.user
- });
- });
- // 普通用户和管理员都可以访问的路由
- router.get('/user-or-admin', authMiddleware, rolesMiddleware('user', 'admin'), (req, res) => {
- res.json({
- message: 'Welcome, user or admin!',
- user: req.user
- });
- });
- module.exports = router;
复制代码
完整的middleware/auth.js文件
- const jwt = require('jsonwebtoken');
- module.exports = function(req, res, next) {
- // 从请求头获取令牌
- const authHeader = req.headers['authorization'];
- const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
- if (!token) {
- return res.status(401).json({ message: 'Access token required' });
- }
- // 验证令牌
- jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
- if (err) {
- return res.status(403).json({ message: 'Invalid or expired token' });
- }
- // 将用户信息添加到请求对象
- req.user = user;
- next();
- });
- };
复制代码
完整的middleware/roles.js文件
- module.exports = function(...roles) {
- return function(req, res, next) {
- if (!req.user) {
- return res.status(401).json({ message: 'Access denied' });
- }
- // 在实际应用中,你可能需要从数据库获取用户的角色
- // 这里我们假设用户信息中包含角色
- const userRole = req.user.role;
-
- const hasRole = roles.includes(userRole);
-
- if (!hasRole) {
- return res.status(403).json({ message: 'Insufficient permissions' });
- }
-
- next();
- };
- };
复制代码
完整的.env文件
- PORT=3000
- JWT_SECRET=your_super_secret_key_that_should_be_very_long_and_random
- JWT_EXPIRES_IN=1h
- JWT_REFRESH_SECRET=your_super_refresh_secret_key_that_should_also_be_long_and_random
- BCRYPT_SALT_ROUNDS=10
- MONGODB_URI=mongodb://localhost:27017/jwt_auth
复制代码
如何使用这个认证系统
- curl -X POST http://localhost:3000/api/auth/register \
- -H "Content-Type: application/json" \
- -d '{"username":"john", "email":"john@example.com", "password":"password123"}'
复制代码- curl -X POST http://localhost:3000/api/auth/login \
- -H "Content-Type: application/json" \
- -d '{"email":"john@example.com", "password":"password123"}'
复制代码- # 将YOUR_ACCESS_TOKEN替换为登录后获得的令牌
- curl -X GET http://localhost:3000/api/protected/profile \
- -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
复制代码- curl -X POST http://localhost:3000/api/auth/refresh-token \
- -H "Content-Type: application/json" \
- -d '{"refreshToken":"YOUR_REFRESH_TOKEN"}'
复制代码- curl -X POST http://localhost:3000/api/auth/logout \
- -H "Content-Type: application/json" \
- -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应用的认证系统提供了强大而灵活的解决方案。通过遵循本文介绍的最佳实践和实现方法,你可以构建一个既安全又高效的认证系统,为你的应用提供可靠的安全保障。
随着技术的不断发展,认证和安全领域也在不断演进。保持对最新安全趋势和最佳实践的关注,持续改进你的认证系统,是确保应用长期安全的关键。 |
|