fix(auth): 修复认证初始化与安全存储问题
- 添加authInitialized状态控制401跳转逻辑 - 实现安全存储的备份机制防止数据丢失 - 优化认证上下文初始化流程,添加取消处理 - 修复异步存储操作未处理Promise的问题
This commit is contained in:
+22
-13
@@ -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';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}, []);
|
||||
|
||||
@@ -1205,7 +1205,7 @@ function TicketManagement() {
|
||||
}}
|
||||
columnsState={{
|
||||
onChange: ({ visibleColumns }) => {
|
||||
secureStorage.set(TICKET_COLUMNS_KEY, visibleColumns);
|
||||
secureStorage.set(TICKET_COLUMNS_KEY, visibleColumns).catch(() => {});
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user