From 540c292d70a4fa1493466b1ee092d757b79d26a1 Mon Sep 17 00:00:00 2001 From: yuyx <237899745@qq.com> Date: Fri, 28 Nov 2025 13:46:51 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0Token=E5=88=B7?= =?UTF-8?q?=E6=96=B0=E6=9C=BA=E5=88=B6=EF=BC=8C=E7=BC=A9=E7=9F=AD=E7=99=BB?= =?UTF-8?q?=E5=BD=95=E6=9C=89=E6=95=88=E6=9C=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 安全改进: - Access Token有效期从7天缩短为2小时 - 添加Refresh Token机制(有效期7天) - 关闭浏览器后较快失效,提升安全性 后端修改(auth.js): - 添加generateRefreshToken函数生成刷新令牌 - 添加refreshAccessToken函数验证并刷新access token - 分离ACCESS_TOKEN_EXPIRES和REFRESH_TOKEN_EXPIRES配置 后端修改(server.js): - 登录时返回refreshToken和expiresIn - 添加/api/refresh-token接口用于刷新token - Cookie有效期同步调整为2小时 前端修改(app.js): - 保存refreshToken到localStorage - 添加自动刷新定时器(过期前5分钟刷新) - 页面加载时若token过期自动尝试刷新 - 登出时清除refreshToken和定时器 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- backend/auth.js | 77 ++++++++++++++++++++++++++++++++++-- backend/server.js | 44 ++++++++++++++++++++- frontend/app.js | 99 +++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 211 insertions(+), 9 deletions(-) diff --git a/backend/auth.js b/backend/auth.js index f363372..e5f47c3 100644 --- a/backend/auth.js +++ b/backend/auth.js @@ -1,8 +1,15 @@ const jwt = require('jsonwebtoken'); +const crypto = require('crypto'); const { UserDB } = require('./database'); // JWT密钥(必须在环境变量中设置) const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-in-production'; +// Refresh Token密钥(使用不同的密钥) +const REFRESH_SECRET = process.env.REFRESH_SECRET || JWT_SECRET + '-refresh'; + +// Token有效期配置 +const ACCESS_TOKEN_EXPIRES = '2h'; // Access token 2小时 +const REFRESH_TOKEN_EXPIRES = '7d'; // Refresh token 7天 // 安全检查:验证JWT密钥配置 const DEFAULT_SECRETS = [ @@ -36,19 +43,77 @@ if (DEFAULT_SECRETS.includes(JWT_SECRET)) { console.log('[安全] JWT密钥已配置'); -// 生成JWT Token +// 生成Access Token(短期) function generateToken(user) { return jwt.sign( { id: user.id, username: user.username, - is_admin: user.is_admin + is_admin: user.is_admin, + type: 'access' }, JWT_SECRET, - { expiresIn: '7d' } + { expiresIn: ACCESS_TOKEN_EXPIRES } ); } +// 生成Refresh Token(长期) +function generateRefreshToken(user) { + return jwt.sign( + { + id: user.id, + type: 'refresh', + // 添加随机标识,使每次生成的refresh token不同 + jti: crypto.randomBytes(16).toString('hex') + }, + REFRESH_SECRET, + { expiresIn: REFRESH_TOKEN_EXPIRES } + ); +} + +// 验证Refresh Token并返回新的Access Token +function refreshAccessToken(refreshToken) { + try { + const decoded = jwt.verify(refreshToken, REFRESH_SECRET); + + if (decoded.type !== 'refresh') { + return { success: false, message: '无效的刷新令牌类型' }; + } + + const user = UserDB.findById(decoded.id); + + if (!user) { + return { success: false, message: '用户不存在' }; + } + + if (user.is_banned) { + return { success: false, message: '账号已被封禁' }; + } + + if (!user.is_active) { + return { success: false, message: '账号未激活' }; + } + + // 生成新的access token + const newAccessToken = generateToken(user); + + return { + success: true, + token: newAccessToken, + user: { + id: user.id, + username: user.username, + is_admin: user.is_admin + } + }; + } catch (error) { + if (error.name === 'TokenExpiredError') { + return { success: false, message: '刷新令牌已过期,请重新登录' }; + } + return { success: false, message: '无效的刷新令牌' }; + } +} + // 验证Token中间件 function authMiddleware(req, res, next) { // 从请求头或HttpOnly Cookie获取token(不再接受URL参数以避免泄露) @@ -142,7 +207,11 @@ function isJwtSecretSecure() { module.exports = { JWT_SECRET, generateToken, + generateRefreshToken, + refreshAccessToken, authMiddleware, adminMiddleware, - isJwtSecretSecure + isJwtSecretSecure, + ACCESS_TOKEN_EXPIRES, + REFRESH_TOKEN_EXPIRES }; diff --git a/backend/server.js b/backend/server.js index 800303d..2a217fa 100644 --- a/backend/server.js +++ b/backend/server.js @@ -21,7 +21,7 @@ const execAsync = util.promisify(exec); const execFileAsync = util.promisify(execFile); const { db, UserDB, ShareDB, SettingsDB, VerificationDB, PasswordResetTokenDB, SystemLogDB } = require('./database'); -const { generateToken, authMiddleware, adminMiddleware, isJwtSecretSecure } = require('./auth'); +const { generateToken, generateRefreshToken, refreshAccessToken, authMiddleware, adminMiddleware, isJwtSecretSecure } = require('./auth'); const app = express(); const PORT = process.env.PORT || 40001; @@ -1632,6 +1632,7 @@ app.post('/api/login', } const token = generateToken(user); + const refreshToken = generateRefreshToken(user); // 清除失败记录 if (req.rateLimitKeys) { @@ -1648,7 +1649,7 @@ app.post('/api/login', secure: isSecureEnv, // HTTPS环境使用strict,HTTP环境使用lax(开发环境兼容) sameSite: isSecureEnv ? 'strict' : 'lax', - maxAge: 7 * 24 * 60 * 60 * 1000, // 7天 + maxAge: 2 * 60 * 60 * 1000, // 2小时(与access token有效期一致) path: '/' // 限制Cookie作用域 }); @@ -1659,6 +1660,8 @@ app.post('/api/login', success: true, message: '登录成功', token, + refreshToken, // 返回refresh token + expiresIn: 2 * 60 * 60 * 1000, // 告知前端access token有效期(毫秒) user: { id: user.id, username: user.username, @@ -1683,6 +1686,43 @@ app.post('/api/login', } ); +// 刷新Access Token +app.post('/api/refresh-token', (req, res) => { + const { refreshToken } = req.body; + + if (!refreshToken) { + return res.status(400).json({ + success: false, + message: '缺少刷新令牌' + }); + } + + const result = refreshAccessToken(refreshToken); + + if (!result.success) { + return res.status(401).json({ + success: false, + message: result.message + }); + } + + // 更新Cookie中的token + const isSecureEnv = process.env.COOKIE_SECURE === 'true'; + res.cookie('token', result.token, { + httpOnly: true, + secure: isSecureEnv, + sameSite: isSecureEnv ? 'strict' : 'lax', + maxAge: 2 * 60 * 60 * 1000, // 2小时 + path: '/' + }); + + res.json({ + success: true, + token: result.token, + expiresIn: 2 * 60 * 60 * 1000 + }); +}); + // ===== 需要认证的API ===== // 获取当前用户信息 diff --git a/frontend/app.js b/frontend/app.js index fe454d5..4ded9ae 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -11,6 +11,8 @@ createApp({ isLoggedIn: false, user: null, token: null, + refreshToken: null, + tokenRefreshTimer: null, // 视图状态 currentView: 'files', @@ -523,6 +525,7 @@ handleDragLeave(e) { if (response.data.success) { this.token = response.data.token; + this.refreshToken = response.data.refreshToken; this.user = response.data.user; this.isLoggedIn = true; this.showResendVerify = false; @@ -534,8 +537,13 @@ handleDragLeave(e) { // 保存token到localStorage localStorage.setItem('token', this.token); + localStorage.setItem('refreshToken', this.refreshToken); localStorage.setItem('user', JSON.stringify(this.user)); + // 启动token自动刷新(在过期前5分钟刷新) + const expiresIn = response.data.expiresIn || (2 * 60 * 60 * 1000); + this.startTokenRefresh(expiresIn); + // 直接从登录响应中获取存储信息 this.storagePermission = this.user.storage_permission || 'sftp_only'; this.storageType = this.user.current_storage_type || 'sftp'; @@ -976,7 +984,10 @@ handleDragLeave(e) { this.isLoggedIn = false; this.user = null; this.token = null; + this.refreshToken = null; + this.stopTokenRefresh(); localStorage.removeItem('token'); + localStorage.removeItem('refreshToken'); localStorage.removeItem('user'); localStorage.removeItem('lastView'); this.showResendVerify = false; @@ -1002,10 +1013,12 @@ handleDragLeave(e) { // 检查本地存储的登录状态 async checkLoginStatus() { const token = localStorage.getItem('token'); + const refreshToken = localStorage.getItem('refreshToken'); const user = localStorage.getItem('user'); if (token && user) { this.token = token; + this.refreshToken = refreshToken; this.user = JSON.parse(user); // 先验证token是否有效 @@ -1031,6 +1044,9 @@ handleDragLeave(e) { console.log('[页面加载] Token验证成功,存储权限:', this.storagePermission, '存储类型:', this.storageType); + // 启动token自动刷新(假设剩余1.5小时,实际由服务端控制) + this.startTokenRefresh(1.5 * 60 * 60 * 1000); + // 启动定期检查用户配置 this.startProfileSync(); // 加载用户主题设置 @@ -1052,29 +1068,106 @@ handleDragLeave(e) { // 强制切换到目标视图并加载数据 this.switchView(targetView, true); } else { - // 响应异常,清除登录状态 - this.handleTokenExpired(); + // 响应异常,尝试刷新token + await this.tryRefreshOrLogout(); } } catch (error) { console.warn('[页面加载] Token验证失败:', error.response?.status || error.message); - // token无效或过期,清除登录状态 + // token无效或过期,尝试使用refresh token刷新 + if (error.response?.status === 401 && this.refreshToken) { + console.log('[页面加载] 尝试使用refresh token刷新...'); + const refreshed = await this.doRefreshToken(); + if (refreshed) { + // 刷新成功,重新检查登录状态 + await this.checkLoginStatus(); + return; + } + } + // 刷新失败或无refresh token,清除登录状态 this.handleTokenExpired(); } } }, + // 尝试刷新token,失败则登出 + async tryRefreshOrLogout() { + if (this.refreshToken) { + const refreshed = await this.doRefreshToken(); + if (refreshed) { + await this.checkLoginStatus(); + return; + } + } + this.handleTokenExpired(); + }, + // 处理token过期/失效 handleTokenExpired() { console.log('[认证] Token已失效,清除登录状态'); this.isLoggedIn = false; this.user = null; this.token = null; + this.refreshToken = null; + this.stopTokenRefresh(); localStorage.removeItem('token'); + localStorage.removeItem('refreshToken'); localStorage.removeItem('user'); localStorage.removeItem('lastView'); this.stopProfileSync(); }, + // 启动token自动刷新定时器 + startTokenRefresh(expiresIn) { + this.stopTokenRefresh(); // 先清除旧的定时器 + + // 在token过期前5分钟刷新 + const refreshTime = Math.max(expiresIn - 5 * 60 * 1000, 60 * 1000); + console.log(`[认证] Token将在 ${Math.round(refreshTime / 60000)} 分钟后刷新`); + + this.tokenRefreshTimer = setTimeout(async () => { + await this.doRefreshToken(); + }, refreshTime); + }, + + // 停止token刷新定时器 + stopTokenRefresh() { + if (this.tokenRefreshTimer) { + clearTimeout(this.tokenRefreshTimer); + this.tokenRefreshTimer = null; + } + }, + + // 执行token刷新 + async doRefreshToken() { + if (!this.refreshToken) { + console.log('[认证] 无refresh token,无法刷新'); + return false; + } + + try { + console.log('[认证] 正在刷新access token...'); + const response = await axios.post(`${this.apiBase}/api/refresh-token`, { + refreshToken: this.refreshToken + }); + + if (response.data.success) { + this.token = response.data.token; + localStorage.setItem('token', this.token); + console.log('[认证] Token刷新成功'); + + // 继续下一次刷新 + const expiresIn = response.data.expiresIn || (2 * 60 * 60 * 1000); + this.startTokenRefresh(expiresIn); + return true; + } + } catch (error) { + console.error('[认证] Token刷新失败:', error.response?.data?.message || error.message); + // 刷新失败,需要重新登录 + this.handleTokenExpired(); + } + return false; + }, + // 检查URL参数 checkUrlParams() { const urlParams = new URLSearchParams(window.location.search);