From 962b01d05a41b450acd4d561f3dc3b5bdadfc967 Mon Sep 17 00:00:00 2001 From: yuyx <237899745@qq.com> Date: Sun, 30 Nov 2025 10:38:40 +0800 Subject: [PATCH] =?UTF-8?q?security:=20refreshToken=20=E4=B9=9F=E5=AD=98?= =?UTF-8?q?=E5=82=A8=E5=9C=A8=20HttpOnly=20Cookie=20=E4=B8=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 后端修改 - 登录时同时设置 token 和 refreshToken 的 HttpOnly Cookie - refreshToken 有效期7天,token 有效期2小时 - 刷新接口优先从 Cookie 读取 refreshToken(向后兼容请求体) - 登出时同时清除两个 Cookie ## 前端修改 - 移除 refreshToken 变量和相关逻辑 - 简化 doRefreshToken(),不再手动传递 refreshToken - 简化 tryRefreshOrLogout(),直接尝试刷新 ## 好处 - 页面刷新后 refreshToken 不会丢失 - 完全无感刷新,用户体验更好 - 前端代码更简洁(减少约20行) - refreshToken 也无法被 XSS 窃取 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- backend/server.js | 31 ++++++++++++++++++++----------- frontend/app.js | 31 ++++++++++--------------------- 2 files changed, 30 insertions(+), 32 deletions(-) diff --git a/backend/server.js b/backend/server.js index ccec88c..c817d3c 100644 --- a/backend/server.js +++ b/backend/server.js @@ -1652,13 +1652,23 @@ app.post('/api/login', // 增强Cookie安全设置 const isSecureEnv = process.env.COOKIE_SECURE === 'true'; - res.cookie('token', token, { + const cookieOptions = { httpOnly: true, secure: isSecureEnv, - // HTTPS环境使用strict,HTTP环境使用lax(开发环境兼容) sameSite: isSecureEnv ? 'strict' : 'lax', - maxAge: 2 * 60 * 60 * 1000, // 2小时(与access token有效期一致) - path: '/' // 限制Cookie作用域 + path: '/' + }; + + // 设置 access token Cookie(2小时有效) + res.cookie('token', token, { + ...cookieOptions, + maxAge: 2 * 60 * 60 * 1000 + }); + + // 设置 refresh token Cookie(7天有效) + res.cookie('refreshToken', refreshToken, { + ...cookieOptions, + maxAge: 7 * 24 * 60 * 60 * 1000 }); // 记录登录成功日志 @@ -1667,8 +1677,6 @@ app.post('/api/login', res.json({ success: true, message: '登录成功', - token, - refreshToken, // 返回refresh token expiresIn: 2 * 60 * 60 * 1000, // 告知前端access token有效期(毫秒) user: { id: user.id, @@ -1694,9 +1702,10 @@ app.post('/api/login', } ); -// 刷新Access Token +// 刷新Access Token(从 HttpOnly Cookie 读取 refreshToken) app.post('/api/refresh-token', (req, res) => { - const { refreshToken } = req.body; + // 优先从 Cookie 读取,兼容从请求体读取(向后兼容) + const refreshToken = req.cookies?.refreshToken || req.body?.refreshToken; if (!refreshToken) { return res.status(400).json({ @@ -1720,21 +1729,21 @@ app.post('/api/refresh-token', (req, res) => { httpOnly: true, secure: isSecureEnv, sameSite: isSecureEnv ? 'strict' : 'lax', - maxAge: 2 * 60 * 60 * 1000, // 2小时 + maxAge: 2 * 60 * 60 * 1000, path: '/' }); res.json({ success: true, - token: result.token, expiresIn: 2 * 60 * 60 * 1000 }); }); // 登出(清除Cookie) app.post('/api/logout', (req, res) => { - // 清除认证Cookie + // 清除所有认证Cookie res.clearCookie('token', { path: '/' }); + res.clearCookie('refreshToken', { path: '/' }); res.json({ success: true, message: '已登出' }); }); diff --git a/frontend/app.js b/frontend/app.js index ef16b86..e7a78f4 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -10,8 +10,7 @@ createApp({ // 用户状态 isLoggedIn: false, user: null, - token: null, - refreshToken: null, + token: null, // 仅用于内部状态跟踪,实际认证通过 HttpOnly Cookie tokenRefreshTimer: null, // 视图状态 @@ -527,8 +526,7 @@ handleDragLeave(e) { const response = await axios.post(`${this.apiBase}/api/login`, this.loginForm); if (response.data.success) { - this.token = response.data.token; - this.refreshToken = response.data.refreshToken; + // token 和 refreshToken 都通过 HttpOnly Cookie 自动管理 this.user = response.data.user; this.isLoggedIn = true; this.showResendVerify = false; @@ -1002,7 +1000,6 @@ handleDragLeave(e) { this.isLoggedIn = false; this.user = null; this.token = null; - this.refreshToken = null; this.stopTokenRefresh(); localStorage.removeItem('user'); localStorage.removeItem('lastView'); @@ -1086,12 +1083,11 @@ handleDragLeave(e) { // 尝试刷新token,失败则登出 async tryRefreshOrLogout() { - if (this.refreshToken) { - const refreshed = await this.doRefreshToken(); - if (refreshed) { - await this.checkLoginStatus(); - return; - } + // refreshToken 通过 Cookie 自动管理,直接尝试刷新 + const refreshed = await this.doRefreshToken(); + if (refreshed) { + await this.checkLoginStatus(); + return; } this.handleTokenExpired(); }, @@ -1102,7 +1098,6 @@ handleDragLeave(e) { this.isLoggedIn = false; this.user = null; this.token = null; - this.refreshToken = null; this.stopTokenRefresh(); localStorage.removeItem('user'); localStorage.removeItem('lastView'); @@ -1130,18 +1125,12 @@ handleDragLeave(e) { } }, - // 执行token刷新(通过 refreshToken 刷新 HttpOnly Cookie 中的 access token) + // 执行token刷新(refreshToken 通过 HttpOnly Cookie 自动发送) 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 - }); + // refreshToken 通过 Cookie 自动携带,无需手动传递 + const response = await axios.post(`${this.apiBase}/api/refresh-token`); if (response.data.success) { // 后端已自动更新 HttpOnly Cookie 中的 token