fix(auth): 修复认证初始化与安全存储问题

- 添加authInitialized状态控制401跳转逻辑
- 实现安全存储的备份机制防止数据丢失
- 优化认证上下文初始化流程,添加取消处理
- 修复异步存储操作未处理Promise的问题
This commit is contained in:
zhang1106
2026-05-07 09:17:50 +08:00
parent fa39f2855f
commit c717ce30d6
4 changed files with 126 additions and 40 deletions
+9
View File
@@ -3,11 +3,16 @@ import { API_CONFIG } from '../config/api';
import secureStorage, { TOKEN_KEY } from '../utils/secureStorage'; import secureStorage, { TOKEN_KEY } from '../utils/secureStorage';
let maintenanceCallback = null; let maintenanceCallback = null;
let authInitialized = false;
export function setMaintenanceCallback(callback) { export function setMaintenanceCallback(callback) {
maintenanceCallback = callback; maintenanceCallback = callback;
} }
export function setAuthInitialized(value) {
authInitialized = value;
}
// 给全局 axios 默认实例添加 Token 拦截器 // 给全局 axios 默认实例添加 Token 拦截器
// 确保所有页面中直接使用 axios.get/post 的请求也能自动携带 Token // 确保所有页面中直接使用 axios.get/post 的请求也能自动携带 Token
axios.interceptors.request.use(config => { axios.interceptors.request.use(config => {
@@ -22,6 +27,7 @@ axios.interceptors.response.use(
response => response, response => response,
error => { error => {
if (error.response?.status === 401) { if (error.response?.status === 401) {
if (authInitialized) {
const currentPath = window.location.pathname; const currentPath = window.location.pathname;
if (!currentPath.startsWith('/login')) { if (!currentPath.startsWith('/login')) {
secureStorage.remove(TOKEN_KEY); secureStorage.remove(TOKEN_KEY);
@@ -29,6 +35,7 @@ axios.interceptors.response.use(
window.location.href = '/login'; window.location.href = '/login';
} }
} }
}
return Promise.reject(error); return Promise.reject(error);
} }
); );
@@ -86,6 +93,7 @@ api.interceptors.response.use(
} }
if (status === 401) { if (status === 401) {
if (authInitialized) {
const currentPath = window.location.pathname; const currentPath = window.location.pathname;
if (!currentPath.startsWith('/login')) { if (!currentPath.startsWith('/login')) {
@@ -98,6 +106,7 @@ api.interceptors.response.use(
} }
} }
} }
}
error.friendlyMessage = data.message || '请求失败'; error.friendlyMessage = data.message || '请求失败';
error.message = data.message || '请求失败'; error.message = data.message || '请求失败';
+27 -8
View File
@@ -1,5 +1,5 @@
import React, { createContext, useContext, useState, useEffect, useCallback, useMemo } from 'react'; 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'; import secureStorage, { TOKEN_KEY, USER_KEY } from '../utils/secureStorage';
const AuthContext = createContext({ const AuthContext = createContext({
@@ -26,11 +26,15 @@ export const AuthProvider = ({ children }) => {
const [initialized, setInitialized] = useState(false); const [initialized, setInitialized] = useState(false);
useEffect(() => { useEffect(() => {
let cancelled = false;
const initializeAuth = async () => { const initializeAuth = async () => {
try { try {
const storedToken = await secureStorage.loadFromStorage(TOKEN_KEY); const storedToken = await secureStorage.loadFromStorage(TOKEN_KEY);
const storedUser = await secureStorage.loadFromStorage(USER_KEY); const storedUser = await secureStorage.loadFromStorage(USER_KEY);
if (cancelled) return;
if (!storedToken) { if (!storedToken) {
setToken(null); setToken(null);
setUser(null); setUser(null);
@@ -44,26 +48,41 @@ export const AuthProvider = ({ children }) => {
try { try {
const response = await authAPI.getProfile(); const response = await authAPI.getProfile();
if (cancelled) return;
if (response.success) { if (response.success) {
setUser(response.data.user); setUser(response.data.user);
secureStorage.set(USER_KEY, response.data.user); await secureStorage.set(USER_KEY, response.data.user);
} }
} catch { } catch (error) {
if (cancelled) return;
const status = error?.response?.status;
if (status === 401 || status === 403) {
secureStorage.remove(TOKEN_KEY); secureStorage.remove(TOKEN_KEY);
secureStorage.remove(USER_KEY); secureStorage.remove(USER_KEY);
setToken(null); setToken(null);
setUser(null); setUser(null);
} }
}
} catch { } catch {
if (cancelled) return;
setToken(null); setToken(null);
setUser(null); setUser(null);
} finally { } finally {
if (!cancelled) {
setLoading(false); setLoading(false);
setInitialized(true); setInitialized(true);
setAuthInitialized(true);
}
} }
}; };
initializeAuth(); initializeAuth();
return () => {
cancelled = true;
};
}, []); }, []);
const login = useCallback(async (username, password) => { const login = useCallback(async (username, password) => {
@@ -71,8 +90,8 @@ export const AuthProvider = ({ children }) => {
const response = await authAPI.login({ username, password }); const response = await authAPI.login({ username, password });
if (response.success) { if (response.success) {
const { token: newToken, user: userData } = response.data; const { token: newToken, user: userData } = response.data;
secureStorage.set(TOKEN_KEY, newToken); await secureStorage.set(TOKEN_KEY, newToken);
secureStorage.set(USER_KEY, userData); await secureStorage.set(USER_KEY, userData);
setToken(newToken); setToken(newToken);
setUser(userData); setUser(userData);
return { success: true }; return { success: true };
@@ -90,8 +109,8 @@ export const AuthProvider = ({ children }) => {
if (response.success) { if (response.success) {
const { token: newToken, user: newUser, isFirstUser, pendingApproval } = response.data; const { token: newToken, user: newUser, isFirstUser, pendingApproval } = response.data;
if (newToken) { if (newToken) {
secureStorage.set(TOKEN_KEY, newToken); await secureStorage.set(TOKEN_KEY, newToken);
secureStorage.set(USER_KEY, newUser); await secureStorage.set(USER_KEY, newUser);
setToken(newToken); setToken(newToken);
setUser(newUser); setUser(newUser);
} }
@@ -114,7 +133,7 @@ export const AuthProvider = ({ children }) => {
const updateUser = useCallback(newUserData => { const updateUser = useCallback(newUserData => {
setUser(prev => { setUser(prev => {
const updated = { ...prev, ...newUserData }; const updated = { ...prev, ...newUserData };
secureStorage.set(USER_KEY, updated); secureStorage.set(USER_KEY, updated).catch(() => {});
return updated; return updated;
}); });
}, []); }, []);
+1 -1
View File
@@ -1205,7 +1205,7 @@ function TicketManagement() {
}} }}
columnsState={{ columnsState={{
onChange: ({ visibleColumns }) => { onChange: ({ visibleColumns }) => {
secureStorage.set(TICKET_COLUMNS_KEY, visibleColumns); secureStorage.set(TICKET_COLUMNS_KEY, visibleColumns).catch(() => {});
}, },
}} }}
/> />
+69 -11
View File
@@ -1,5 +1,6 @@
const STORAGE_PREFIX = 'idc_'; const STORAGE_PREFIX = 'idc_';
const KEY_STORAGE_ID = '__idc_sk'; const KEY_STORAGE_ID = '__idc_sk';
const BACKUP_PREFIX = 'idc_bak_';
const SALT = new TextEncoder().encode('idc-secure-storage-salt-v1'); const SALT = new TextEncoder().encode('idc-secure-storage-salt-v1');
const PBKDF2_ITERATIONS = 100000; const PBKDF2_ITERATIONS = 100000;
const AES_KEY_LENGTH = 256; 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 = { export const secureStorage = {
set: (key, value, options = {}) => { set: async (key, value, options = {}) => {
memoryCache.set(key, value); memoryCache.set(key, value);
const storageKey = `${STORAGE_PREFIX}${key}`; const storageKey = `${STORAGE_PREFIX}${key}`;
@@ -110,19 +138,29 @@ export const secureStorage = {
}; };
const serialized = JSON.stringify(data); const serialized = JSON.stringify(data);
encrypt(serialized) saveBackup(key, value);
.then(encrypted => {
localStorage.setItem(storageKey, encrypted);
})
.catch(() => {});
try {
const encrypted = await encrypt(serialized);
localStorage.setItem(storageKey, encrypted);
return true; return true;
} catch (error) {
console.error('[secureStorage] 加密存储失败', key, error);
return false;
}
}, },
get: (key) => { get: (key) => {
if (memoryCache.has(key)) { if (memoryCache.has(key)) {
return memoryCache.get(key); return memoryCache.get(key);
} }
const backup = loadBackup(key);
if (backup !== null) {
memoryCache.set(key, backup);
return backup;
}
return null; return null;
}, },
@@ -130,16 +168,35 @@ export const secureStorage = {
try { try {
const storageKey = `${STORAGE_PREFIX}${key}`; const storageKey = `${STORAGE_PREFIX}${key}`;
const encrypted = localStorage.getItem(storageKey); 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); const value = await decryptAndParse(encrypted);
if (value !== null) { if (value !== null) {
memoryCache.set(key, value); memoryCache.set(key, value);
} else { saveBackup(key, value);
localStorage.removeItem(storageKey);
}
return value; return value;
}
const backup = loadBackup(key);
if (backup !== null) {
memoryCache.set(key, backup);
return backup;
}
return null;
} catch { } catch {
const backup = loadBackup(key);
if (backup !== null) {
memoryCache.set(key, backup);
return backup;
}
return null; return null;
} }
}, },
@@ -150,6 +207,7 @@ export const secureStorage = {
const storageKey = `${STORAGE_PREFIX}${key}`; const storageKey = `${STORAGE_PREFIX}${key}`;
localStorage.removeItem(storageKey); localStorage.removeItem(storageKey);
} catch {} } catch {}
removeBackup(key);
return true; return true;
}, },
@@ -159,7 +217,7 @@ export const secureStorage = {
const keys = []; const keys = [];
for (let i = 0; i < localStorage.length; i++) { for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(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); keys.push(key);
} }
} }