diff --git a/backend/server.js b/backend/server.js index a3730f7..ccec88c 100644 --- a/backend/server.js +++ b/backend/server.js @@ -1731,6 +1731,13 @@ app.post('/api/refresh-token', (req, res) => { }); }); +// 登出(清除Cookie) +app.post('/api/logout', (req, res) => { + // 清除认证Cookie + res.clearCookie('token', { path: '/' }); + res.json({ success: true, message: '已登出' }); +}); + // ===== 需要认证的API ===== // 获取当前用户信息 diff --git a/frontend/app.js b/frontend/app.js index 83a578c..ef16b86 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -322,9 +322,7 @@ createApp({ // 加载用户主题设置(登录后调用) async loadUserTheme() { try { - const res = await axios.get(`${this.apiBase}/api/user/theme`, { - headers: { Authorization: `Bearer ${this.token}` } - }); + const res = await axios.get(`${this.apiBase}/api/user/theme`); if (res.data.success) { this.globalTheme = res.data.theme.global; this.userThemePreference = res.data.theme.user; @@ -352,7 +350,6 @@ createApp({ try { const res = await axios.post(`${this.apiBase}/api/user/theme`, { theme }, - { headers: { Authorization: `Bearer ${this.token}` }} ); if (res.data.success) { this.userThemePreference = res.data.theme.user; @@ -378,7 +375,6 @@ createApp({ console.log('[主题] 设置全局主题:', theme); const res = await axios.post(`${this.apiBase}/api/admin/settings`, { global_theme: theme }, - { headers: { Authorization: `Bearer ${this.token}` }} ); console.log('[主题] API响应:', res.data); if (res.data.success) { @@ -542,9 +538,8 @@ handleDragLeave(e) { this.showCaptcha = false; this.loginForm.captcha = ''; - // 保存token到localStorage - localStorage.setItem('token', this.token); - localStorage.setItem('refreshToken', this.refreshToken); + // 保存用户信息到localStorage(非敏感信息,用于页面刷新后恢复) + // 注意:token 通过 HttpOnly Cookie 传递,不再存储在 localStorage localStorage.setItem('user', JSON.stringify(this.user)); // 启动token自动刷新(在过期前5分钟刷新) @@ -565,11 +560,8 @@ handleDragLeave(e) { console.log('[登录] SFTP未配置但用户有本地存储权限,自动切换到本地存储'); this.storageType = 'local'; // 异步更新到后端(不等待,避免阻塞登录流程) - axios.post( - `${this.apiBase}/api/user/switch-storage`, - { storage_type: 'local' }, - { headers: { Authorization: `Bearer ${this.token}` } } - ).catch(err => console.error('[登录] 自动切换存储类型失败:', err)); + axios.post(`${this.apiBase}/api/user/switch-storage`, { storage_type: 'local' }) + .catch(err => console.error('[登录] 自动切换存储类型失败:', err)); } } @@ -755,7 +747,6 @@ handleDragLeave(e) { const response = await axios.post( `${this.apiBase}/api/user/update-ftp`, this.ftpConfigForm, - { headers: { Authorization: `Bearer ${this.token}` } } ); if (response.data.success) { @@ -769,8 +760,7 @@ handleDragLeave(e) { const switchResponse = await axios.post( `${this.apiBase}/api/user/switch-storage`, { storage_type: 'sftp' }, - { headers: { Authorization: `Bearer ${this.token}` } } - ); + ); if (switchResponse.data.success) { this.storageType = 'sftp'; @@ -800,18 +790,12 @@ handleDragLeave(e) { { username: this.adminProfileForm.username }, - { headers: { Authorization: `Bearer ${this.token}` } } ); if (response.data.success) { alert('用户名已更新!请重新登录。'); - // 更新token和用户信息 - if (response.data.token) { - this.token = response.data.token; - localStorage.setItem('token', response.data.token); - } - + // 更新用户信息(后端已通过 Cookie 更新 token) if (response.data.user) { this.user = response.data.user; localStorage.setItem('user', JSON.stringify(response.data.user)); @@ -843,7 +827,6 @@ handleDragLeave(e) { current_password: this.changePasswordForm.current_password, new_password: this.changePasswordForm.new_password }, - { headers: { Authorization: `Bearer ${this.token}` } } ); if (response.data.success) { @@ -860,7 +843,6 @@ handleDragLeave(e) { try { const response = await axios.get( `${this.apiBase}/api/user/profile`, - { headers: { Authorization: `Bearer ${this.token}` } } ); if (response.data.success && response.data.user) { @@ -975,7 +957,6 @@ handleDragLeave(e) { const response = await axios.post( `${this.apiBase}/api/user/update-username`, { username: this.usernameForm.newUsername }, - { headers: { Authorization: `Bearer ${this.token}` } } ); if (response.data.success) { @@ -995,7 +976,6 @@ handleDragLeave(e) { const response = await axios.post( `${this.apiBase}/api/user/update-profile`, { email: this.profileForm.email }, - { headers: { Authorization: `Bearer ${this.token}` } } ); if (response.data.success) { @@ -1011,14 +991,19 @@ handleDragLeave(e) { } }, - logout() { + async logout() { + // 调用后端清除 HttpOnly Cookie + try { + await axios.post(`${this.apiBase}/api/logout`); + } catch (err) { + console.error('[登出] 清除Cookie失败:', err); + } + 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; @@ -1041,82 +1026,61 @@ handleDragLeave(e) { } }, - // 检查本地存储的登录状态 + // 检查登录状态(通过 HttpOnly Cookie 验证) async checkLoginStatus() { - const token = localStorage.getItem('token'); - const refreshToken = localStorage.getItem('refreshToken'); - const user = localStorage.getItem('user'); + // 直接调用API验证,Cookie会自动携带 + try { + const response = await axios.get(`${this.apiBase}/api/user/profile`); - if (token && user) { - this.token = token; - this.refreshToken = refreshToken; - this.user = JSON.parse(user); + if (response.data.success && response.data.user) { + // Cookie有效,用户已登录 + this.user = response.data.user; + this.isLoggedIn = true; - // 先验证token是否有效 - try { - const response = await axios.get( - `${this.apiBase}/api/user/profile`, - { headers: { Authorization: `Bearer ${token}` } } - ); + // 更新localStorage中的用户信息(非敏感信息) + localStorage.setItem('user', JSON.stringify(this.user)); - if (response.data.success && response.data.user) { - // token有效,更新用户信息 - this.user = response.data.user; - this.isLoggedIn = true; + // 从最新的用户信息初始化存储相关字段 + this.storagePermission = this.user.storage_permission || 'sftp_only'; + this.storageType = this.user.current_storage_type || 'sftp'; + this.localQuota = this.user.local_storage_quota || 0; + this.localUsed = this.user.local_storage_used || 0; - // 更新localStorage中的用户信息 - localStorage.setItem('user', JSON.stringify(this.user)); + console.log('[页面加载] Cookie验证成功,存储权限:', this.storagePermission, '存储类型:', this.storageType); - // 从最新的用户信息初始化存储相关字段 - this.storagePermission = this.user.storage_permission || 'sftp_only'; - this.storageType = this.user.current_storage_type || 'sftp'; - this.localQuota = this.user.local_storage_quota || 0; - this.localUsed = this.user.local_storage_used || 0; + // 启动token自动刷新(假设剩余1.5小时,实际由服务端控制) + this.startTokenRefresh(1.5 * 60 * 60 * 1000); - console.log('[页面加载] Token验证成功,存储权限:', this.storagePermission, '存储类型:', this.storageType); + // 启动定期检查用户配置 + this.startProfileSync(); + // 加载用户主题设置 + this.loadUserTheme(); - // 启动token自动刷新(假设剩余1.5小时,实际由服务端控制) - this.startTokenRefresh(1.5 * 60 * 60 * 1000); - - // 启动定期检查用户配置 - this.startProfileSync(); - // 加载用户主题设置 - this.loadUserTheme(); - - // 读取上次停留的视图(需合法才生效) - const savedView = localStorage.getItem('lastView'); - let targetView = null; - if (savedView && this.isViewAllowed(savedView)) { - targetView = savedView; - } else if (this.user.is_admin) { - targetView = 'admin'; - } else if (this.storagePermission === 'sftp_only' && !this.user.has_ftp_config) { - targetView = 'settings'; - } else { - targetView = 'files'; - } - - // 强制切换到目标视图并加载数据 - this.switchView(targetView, true); + // 读取上次停留的视图(需合法才生效) + const savedView = localStorage.getItem('lastView'); + let targetView = null; + if (savedView && this.isViewAllowed(savedView)) { + targetView = savedView; + } else if (this.user.is_admin) { + targetView = 'admin'; + } else if (this.storagePermission === 'sftp_only' && !this.user.has_ftp_config) { + targetView = 'settings'; } else { - // 响应异常,尝试刷新token - await this.tryRefreshOrLogout(); + targetView = 'files'; } - } catch (error) { - console.warn('[页面加载] Token验证失败:', error.response?.status || error.message); - // 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(); + + // 强制切换到目标视图并加载数据 + this.switchView(targetView, true); } + } catch (error) { + // 401表示未登录或Cookie过期,静默处理(用户需要重新登录) + if (error.response?.status === 401) { + console.log('[页面加载] 未登录或Cookie已过期'); + } else { + console.warn('[页面加载] 验证登录状态失败:', error.message); + } + // 清理可能残留的用户信息 + localStorage.removeItem('user'); } }, @@ -1134,14 +1098,12 @@ handleDragLeave(e) { // 处理token过期/失效 handleTokenExpired() { - console.log('[认证] Token已失效,清除登录状态'); + console.log('[认证] Cookie已失效,清除登录状态'); 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(); @@ -1168,7 +1130,7 @@ handleDragLeave(e) { } }, - // 执行token刷新 + // 执行token刷新(通过 refreshToken 刷新 HttpOnly Cookie 中的 access token) async doRefreshToken() { if (!this.refreshToken) { console.log('[认证] 无refresh token,无法刷新'); @@ -1182,9 +1144,8 @@ handleDragLeave(e) { }); if (response.data.success) { - this.token = response.data.token; - localStorage.setItem('token', this.token); - console.log('[认证] Token刷新成功'); + // 后端已自动更新 HttpOnly Cookie 中的 token + console.log('[认证] Token刷新成功(Cookie已更新)'); // 继续下一次刷新 const expiresIn = response.data.expiresIn || (2 * 60 * 60 * 1000); @@ -1220,8 +1181,7 @@ handleDragLeave(e) { try { const response = await axios.get(`${this.apiBase}/api/files`, { - params: { path }, - headers: { Authorization: `Bearer ${this.token}` } + params: { path } }); if (response.data.success) { @@ -1325,7 +1285,6 @@ handleDragLeave(e) { const response = await axios.post( `${this.apiBase}/api/files/rename`, this.renameForm, - { headers: { Authorization: `Bearer ${this.token}` } } ); if (response.data.success) { @@ -1358,8 +1317,6 @@ handleDragLeave(e) { const response = await axios.post(`${this.apiBase}/api/files/mkdir`, { path: this.currentPath, folderName: folderName - }, { - headers: { 'Authorization': `Bearer ${this.token}` } }); if (response.data.success) { @@ -1388,8 +1345,6 @@ handleDragLeave(e) { const response = await axios.post(`${this.apiBase}/api/files/folder-info`, { path: this.currentPath, folderName: file.name - }, { - headers: { 'Authorization': `Bearer ${this.token}` } }); if (response.data.success) { @@ -1614,7 +1569,6 @@ handleDragLeave(e) { path: this.currentPath, isDirectory: file.isDirectory }, - { headers: { Authorization: `Bearer ${this.token}` } } ); if (response.data.success) { @@ -1680,7 +1634,6 @@ handleDragLeave(e) { password: this.shareAllForm.password || null, expiry_days: expiryDays }, - { headers: { Authorization: `Bearer ${this.token}` } } ); if (response.data.success) { @@ -1712,7 +1665,6 @@ handleDragLeave(e) { password: this.shareFileForm.password || null, expiry_days: expiryDays }, - { headers: { Authorization: `Bearer ${this.token}` } } ); if (response.data.success) { @@ -1798,7 +1750,6 @@ handleDragLeave(e) { const response = await axios.post(`${this.apiBase}/api/upload`, formData, { headers: { - 'Authorization': `Bearer ${this.token}`, 'Content-Type': 'multipart/form-data' }, timeout: 30 * 60 * 1000, // 30分钟超时,支持大文件上传 @@ -1864,9 +1815,7 @@ handleDragLeave(e) { async loadShares() { try { - const response = await axios.get(`${this.apiBase}/api/share/my`, { - headers: { Authorization: `Bearer ${this.token}` } - }); + const response = await axios.get(`${this.apiBase}/api/share/my`); if (response.data.success) { this.shares = response.data.shares; @@ -1881,9 +1830,7 @@ handleDragLeave(e) { this.shareForm.path = this.currentPath; try { - const response = await axios.post(`${this.apiBase}/api/share/create`, this.shareForm, { - headers: { Authorization: `Bearer ${this.token}` } - }); + const response = await axios.post(`${this.apiBase}/api/share/create`, this.shareForm); if (response.data.success) { this.shareResult = response.data; @@ -1899,9 +1846,7 @@ handleDragLeave(e) { if (!confirm('确定要删除这个分享吗?')) return; try { - const response = await axios.delete(`${this.apiBase}/api/share/${id}`, { - headers: { Authorization: `Bearer ${this.token}` } - }); + const response = await axios.delete(`${this.apiBase}/api/share/${id}`); if (response.data.success) { alert('分享已删除'); @@ -2000,9 +1945,7 @@ handleDragLeave(e) { async loadUsers() { try { - const response = await axios.get(`${this.apiBase}/api/admin/users`, { - headers: { Authorization: `Bearer ${this.token}` } - }); + const response = await axios.get(`${this.apiBase}/api/admin/users`); if (response.data.success) { this.adminUsers = response.data.users; @@ -2021,7 +1964,6 @@ handleDragLeave(e) { const response = await axios.post( `${this.apiBase}/api/admin/users/${userId}/ban`, { banned }, - { headers: { Authorization: `Bearer ${this.token}` } } ); if (response.data.success) { @@ -2038,9 +1980,7 @@ handleDragLeave(e) { if (!confirm('确定要删除这个用户吗?此操作不可恢复!')) return; try { - const response = await axios.delete(`${this.apiBase}/api/admin/users/${userId}`, { - headers: { Authorization: `Bearer ${this.token}` } - }); + const response = await axios.delete(`${this.apiBase}/api/admin/users/${userId}`); if (response.data.success) { alert('用户已删除'); @@ -2123,8 +2063,7 @@ handleDragLeave(e) { const response = await axios.get( `${this.apiBase}/api/admin/users/${this.inspectionUser.id}/files`, { - params: { path }, - headers: { Authorization: `Bearer ${this.token}` } + params: { path } } ); @@ -2166,7 +2105,6 @@ handleDragLeave(e) { try { const response = await axios.get( `${this.apiBase}/api/user/profile`, - { headers: { Authorization: `Bearer ${this.token}` } } ); if (response.data.success && response.data.user) { @@ -2228,7 +2166,6 @@ handleDragLeave(e) { try { const response = await axios.get( `${this.apiBase}/api/user/sftp-usage`, - { headers: { Authorization: `Bearer ${this.token}` } } ); if (response.data.success) { @@ -2287,7 +2224,6 @@ handleDragLeave(e) { const response = await axios.post( `${this.apiBase}/api/user/switch-storage`, { storage_type: type }, - { headers: { Authorization: `Bearer ${this.token}` } } ); if (response.data.success) { @@ -2426,7 +2362,6 @@ handleDragLeave(e) { storage_permission: this.editStorageForm.storage_permission, local_storage_quota: quotaBytes }, - { headers: { Authorization: `Bearer ${this.token}` } } ); if (response.data.success) { @@ -2506,9 +2441,7 @@ handleDragLeave(e) { async loadSystemSettings() { try { - const response = await axios.get(`${this.apiBase}/api/admin/settings`, { - headers: { Authorization: `Bearer ${this.token}` } - }); + const response = await axios.get(`${this.apiBase}/api/admin/settings`); if (response.data.success) { const settings = response.data.settings; @@ -2537,9 +2470,7 @@ handleDragLeave(e) { async loadServerStorageStats() { try { - const response = await axios.get(`${this.apiBase}/api/admin/storage-stats`, { - headers: { Authorization: `Bearer ${this.token}` } - }); + const response = await axios.get(`${this.apiBase}/api/admin/storage-stats`); if (response.data.success) { this.serverStorageStats = response.data.stats; @@ -2570,8 +2501,7 @@ handleDragLeave(e) { const response = await axios.post( `${this.apiBase}/api/admin/settings`, - payload, - { headers: { Authorization: `Bearer ${this.token}` } } + payload ); if (response.data.success) { @@ -2590,7 +2520,6 @@ handleDragLeave(e) { const response = await axios.post( `${this.apiBase}/api/admin/settings/test-smtp`, { to: this.systemSettings.smtp.user }, - { headers: { Authorization: `Bearer ${this.token}` } } ); this.showToast('success', '成功', response.data.message || '测试邮件已发送'); } catch (error) { @@ -2604,9 +2533,7 @@ handleDragLeave(e) { async loadHealthCheck() { this.healthCheck.loading = true; try { - const response = await axios.get(`${this.apiBase}/api/admin/health-check`, { - headers: { Authorization: `Bearer ${this.token}` } - }); + const response = await axios.get(`${this.apiBase}/api/admin/health-check`); if (response.data.success) { this.healthCheck.overallStatus = response.data.overallStatus; @@ -2680,9 +2607,7 @@ handleDragLeave(e) { params.append('keyword', this.systemLogs.filters.keyword); } - const response = await axios.get(`${this.apiBase}/api/admin/logs?${params}`, { - headers: { Authorization: `Bearer ${this.token}` } - }); + const response = await axios.get(`${this.apiBase}/api/admin/logs?${params}`); if (response.data.success) { this.systemLogs.logs = response.data.logs; @@ -2766,7 +2691,6 @@ handleDragLeave(e) { const response = await axios.post( `${this.apiBase}/api/admin/logs/cleanup`, { keepDays: 90 }, - { headers: { Authorization: `Bearer ${this.token}` } } ); if (response.data.success) { @@ -2787,7 +2711,6 @@ handleDragLeave(e) { try { const response = await axios.get( `${this.apiBase}/api/admin/check-upload-tool`, - { headers: { Authorization: `Bearer ${this.token}` } } ); if (response.data.success) { @@ -2844,7 +2767,6 @@ handleDragLeave(e) { formData, { headers: { - Authorization: `Bearer ${this.token}`, 'Content-Type': 'multipart/form-data' } }