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
+22 -13
View File
@@ -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';
}
}
}
}
+33 -14
View File
@@ -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;
});
}, []);
+1 -1
View File
@@ -1205,7 +1205,7 @@ function TicketManagement() {
}}
columnsState={{
onChange: ({ visibleColumns }) => {
secureStorage.set(TICKET_COLUMNS_KEY, visibleColumns);
secureStorage.set(TICKET_COLUMNS_KEY, visibleColumns).catch(() => {});
},
}}
/>
+70 -12
View File
@@ -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);
}
}