From c717ce30d62a380fdde12af9ea44f10aa6eb9782 Mon Sep 17 00:00:00 2001 From: zhang1106 <849185023@qq.com> Date: Thu, 7 May 2026 09:17:50 +0800 Subject: [PATCH] =?UTF-8?q?fix(auth):=20=E4=BF=AE=E5=A4=8D=E8=AE=A4?= =?UTF-8?q?=E8=AF=81=E5=88=9D=E5=A7=8B=E5=8C=96=E4=B8=8E=E5=AE=89=E5=85=A8?= =?UTF-8?q?=E5=AD=98=E5=82=A8=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加authInitialized状态控制401跳转逻辑 - 实现安全存储的备份机制防止数据丢失 - 优化认证上下文初始化流程,添加取消处理 - 修复异步存储操作未处理Promise的问题 --- frontend/src/api/index.js | 35 +++++++---- frontend/src/context/AuthContext.jsx | 47 +++++++++----- frontend/src/pages/TicketManagement.jsx | 2 +- frontend/src/utils/secureStorage.js | 82 +++++++++++++++++++++---- 4 files changed, 126 insertions(+), 40 deletions(-) diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js index d37f26e..6eafffb 100644 --- a/frontend/src/api/index.js +++ b/frontend/src/api/index.js @@ -3,11 +3,16 @@ import { API_CONFIG } from '../config/api'; import secureStorage, { TOKEN_KEY } from '../utils/secureStorage'; let maintenanceCallback = null; +let authInitialized = false; export function setMaintenanceCallback(callback) { maintenanceCallback = callback; } +export function setAuthInitialized(value) { + authInitialized = value; +} + // 给全局 axios 默认实例添加 Token 拦截器 // 确保所有页面中直接使用 axios.get/post 的请求也能自动携带 Token axios.interceptors.request.use(config => { @@ -22,11 +27,13 @@ axios.interceptors.response.use( response => response, error => { if (error.response?.status === 401) { - const currentPath = window.location.pathname; - if (!currentPath.startsWith('/login')) { - secureStorage.remove(TOKEN_KEY); - secureStorage.remove('user'); - window.location.href = '/login'; + if (authInitialized) { + const currentPath = window.location.pathname; + if (!currentPath.startsWith('/login')) { + secureStorage.remove(TOKEN_KEY); + secureStorage.remove('user'); + window.location.href = '/login'; + } } } return Promise.reject(error); @@ -86,15 +93,17 @@ api.interceptors.response.use( } if (status === 401) { - const currentPath = window.location.pathname; + if (authInitialized) { + const currentPath = window.location.pathname; - if (!currentPath.startsWith('/login')) { - secureStorage.remove(TOKEN_KEY); - secureStorage.remove('user'); - if (window.__navigate) { - window.__navigate('/login'); - } else { - window.location.href = '/login'; + if (!currentPath.startsWith('/login')) { + secureStorage.remove(TOKEN_KEY); + secureStorage.remove('user'); + if (window.__navigate) { + window.__navigate('/login'); + } else { + window.location.href = '/login'; + } } } } diff --git a/frontend/src/context/AuthContext.jsx b/frontend/src/context/AuthContext.jsx index 069aa72..63c1b9b 100644 --- a/frontend/src/context/AuthContext.jsx +++ b/frontend/src/context/AuthContext.jsx @@ -1,5 +1,5 @@ import React, { createContext, useContext, useState, useEffect, useCallback, useMemo } from 'react'; -import { authAPI } from '../api'; +import { authAPI, setAuthInitialized } from '../api'; import secureStorage, { TOKEN_KEY, USER_KEY } from '../utils/secureStorage'; const AuthContext = createContext({ @@ -26,11 +26,15 @@ export const AuthProvider = ({ children }) => { const [initialized, setInitialized] = useState(false); useEffect(() => { + let cancelled = false; + const initializeAuth = async () => { try { const storedToken = await secureStorage.loadFromStorage(TOKEN_KEY); const storedUser = await secureStorage.loadFromStorage(USER_KEY); + if (cancelled) return; + if (!storedToken) { setToken(null); setUser(null); @@ -44,26 +48,41 @@ export const AuthProvider = ({ children }) => { try { const response = await authAPI.getProfile(); + if (cancelled) return; + if (response.success) { setUser(response.data.user); - secureStorage.set(USER_KEY, response.data.user); + await secureStorage.set(USER_KEY, response.data.user); + } + } catch (error) { + if (cancelled) return; + + const status = error?.response?.status; + if (status === 401 || status === 403) { + secureStorage.remove(TOKEN_KEY); + secureStorage.remove(USER_KEY); + setToken(null); + setUser(null); } - } catch { - secureStorage.remove(TOKEN_KEY); - secureStorage.remove(USER_KEY); - setToken(null); - setUser(null); } } catch { + if (cancelled) return; setToken(null); setUser(null); } finally { - setLoading(false); - setInitialized(true); + if (!cancelled) { + setLoading(false); + setInitialized(true); + setAuthInitialized(true); + } } }; initializeAuth(); + + return () => { + cancelled = true; + }; }, []); const login = useCallback(async (username, password) => { @@ -71,8 +90,8 @@ export const AuthProvider = ({ children }) => { const response = await authAPI.login({ username, password }); if (response.success) { const { token: newToken, user: userData } = response.data; - secureStorage.set(TOKEN_KEY, newToken); - secureStorage.set(USER_KEY, userData); + await secureStorage.set(TOKEN_KEY, newToken); + await secureStorage.set(USER_KEY, userData); setToken(newToken); setUser(userData); return { success: true }; @@ -90,8 +109,8 @@ export const AuthProvider = ({ children }) => { if (response.success) { const { token: newToken, user: newUser, isFirstUser, pendingApproval } = response.data; if (newToken) { - secureStorage.set(TOKEN_KEY, newToken); - secureStorage.set(USER_KEY, newUser); + await secureStorage.set(TOKEN_KEY, newToken); + await secureStorage.set(USER_KEY, newUser); setToken(newToken); setUser(newUser); } @@ -114,7 +133,7 @@ export const AuthProvider = ({ children }) => { const updateUser = useCallback(newUserData => { setUser(prev => { const updated = { ...prev, ...newUserData }; - secureStorage.set(USER_KEY, updated); + secureStorage.set(USER_KEY, updated).catch(() => {}); return updated; }); }, []); diff --git a/frontend/src/pages/TicketManagement.jsx b/frontend/src/pages/TicketManagement.jsx index 60c87e9..ac6795c 100644 --- a/frontend/src/pages/TicketManagement.jsx +++ b/frontend/src/pages/TicketManagement.jsx @@ -1205,7 +1205,7 @@ function TicketManagement() { }} columnsState={{ onChange: ({ visibleColumns }) => { - secureStorage.set(TICKET_COLUMNS_KEY, visibleColumns); + secureStorage.set(TICKET_COLUMNS_KEY, visibleColumns).catch(() => {}); }, }} /> diff --git a/frontend/src/utils/secureStorage.js b/frontend/src/utils/secureStorage.js index 0e56405..039cf3d 100644 --- a/frontend/src/utils/secureStorage.js +++ b/frontend/src/utils/secureStorage.js @@ -1,5 +1,6 @@ const STORAGE_PREFIX = 'idc_'; const KEY_STORAGE_ID = '__idc_sk'; +const BACKUP_PREFIX = 'idc_bak_'; const SALT = new TextEncoder().encode('idc-secure-storage-salt-v1'); const PBKDF2_ITERATIONS = 100000; const AES_KEY_LENGTH = 256; @@ -98,8 +99,35 @@ async function decryptAndParse(encrypted) { } } +function saveBackup(key, value) { + try { + const backupKey = `${BACKUP_PREFIX}${key}`; + const data = { value, timestamp: Date.now() }; + localStorage.setItem(backupKey, JSON.stringify(data)); + } catch {} +} + +function loadBackup(key) { + try { + const backupKey = `${BACKUP_PREFIX}${key}`; + const raw = localStorage.getItem(backupKey); + if (!raw) return null; + const data = JSON.parse(raw); + return data.value; + } catch { + return null; + } +} + +function removeBackup(key) { + try { + const backupKey = `${BACKUP_PREFIX}${key}`; + localStorage.removeItem(backupKey); + } catch {} +} + export const secureStorage = { - set: (key, value, options = {}) => { + set: async (key, value, options = {}) => { memoryCache.set(key, value); const storageKey = `${STORAGE_PREFIX}${key}`; @@ -110,19 +138,29 @@ export const secureStorage = { }; const serialized = JSON.stringify(data); - encrypt(serialized) - .then(encrypted => { - localStorage.setItem(storageKey, encrypted); - }) - .catch(() => {}); + saveBackup(key, value); - return true; + try { + const encrypted = await encrypt(serialized); + localStorage.setItem(storageKey, encrypted); + return true; + } catch (error) { + console.error('[secureStorage] 加密存储失败', key, error); + return false; + } }, get: (key) => { if (memoryCache.has(key)) { return memoryCache.get(key); } + + const backup = loadBackup(key); + if (backup !== null) { + memoryCache.set(key, backup); + return backup; + } + return null; }, @@ -130,16 +168,35 @@ export const secureStorage = { try { const storageKey = `${STORAGE_PREFIX}${key}`; const encrypted = localStorage.getItem(storageKey); - if (!encrypted) return null; + if (!encrypted) { + const backup = loadBackup(key); + if (backup !== null) { + memoryCache.set(key, backup); + return backup; + } + return null; + } const value = await decryptAndParse(encrypted); if (value !== null) { memoryCache.set(key, value); - } else { - localStorage.removeItem(storageKey); + saveBackup(key, value); + return value; } - return value; + + const backup = loadBackup(key); + if (backup !== null) { + memoryCache.set(key, backup); + return backup; + } + + return null; } catch { + const backup = loadBackup(key); + if (backup !== null) { + memoryCache.set(key, backup); + return backup; + } return null; } }, @@ -150,6 +207,7 @@ export const secureStorage = { const storageKey = `${STORAGE_PREFIX}${key}`; localStorage.removeItem(storageKey); } catch {} + removeBackup(key); return true; }, @@ -159,7 +217,7 @@ export const secureStorage = { const keys = []; for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); - if (key && key.startsWith(STORAGE_PREFIX)) { + if (key && (key.startsWith(STORAGE_PREFIX) || key.startsWith(BACKUP_PREFIX))) { keys.push(key); } }