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