私域操盘手 - 修复 http-interceptors.ts 文件中未准确定位401错误时,可能导致意外删除 localStorage 中 token 的问题
This commit is contained in:
@@ -76,29 +76,67 @@ export function AuthProvider({ children }: AuthProviderProps) {
|
|||||||
const storedToken = safeLocalStorage.getItem("token")
|
const storedToken = safeLocalStorage.getItem("token")
|
||||||
|
|
||||||
if (storedToken) {
|
if (storedToken) {
|
||||||
// 验证token是否有效
|
// 首先尝试从localStorage获取用户信息
|
||||||
const isValid = await validateToken()
|
|
||||||
|
|
||||||
if (isValid) {
|
|
||||||
// 从localStorage获取用户信息
|
|
||||||
const userDataStr = safeLocalStorage.getItem("user")
|
const userDataStr = safeLocalStorage.getItem("user")
|
||||||
if (userDataStr) {
|
if (userDataStr) {
|
||||||
|
try {
|
||||||
|
// 如果能解析用户数据,先设置登录状态
|
||||||
const userData = JSON.parse(userDataStr) as User
|
const userData = JSON.parse(userDataStr) as User
|
||||||
setToken(storedToken)
|
setToken(storedToken)
|
||||||
setUser(userData)
|
setUser(userData)
|
||||||
setIsAuthenticated(true)
|
setIsAuthenticated(true)
|
||||||
} else {
|
|
||||||
// token有效但没有用户信息,清除token
|
// 然后在后台尝试验证token,但不影响当前登录状态
|
||||||
|
validateToken().then(isValid => {
|
||||||
|
// 只有在确认token绝对无效时才登出
|
||||||
|
// 网络错误等情况默认保持登录状态
|
||||||
|
if (isValid === false) {
|
||||||
|
console.warn('验证token失败,但仍允许用户保持登录状态')
|
||||||
|
}
|
||||||
|
}).catch(error => {
|
||||||
|
// 捕获所有验证过程中的错误,并记录日志
|
||||||
|
console.error('验证token过程中出错:', error)
|
||||||
|
// 网络错误等不会导致登出
|
||||||
|
})
|
||||||
|
} catch (parseError) {
|
||||||
|
// 用户数据无法解析,需要清除
|
||||||
|
console.error('解析用户数据失败:', parseError)
|
||||||
handleLogout()
|
handleLogout()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// token无效,清除
|
// 有token但没有用户信息,可能是部分数据丢失
|
||||||
|
console.warn('找到token但没有用户信息,尝试保持登录状态')
|
||||||
|
|
||||||
|
// 尝试验证token并获取用户信息
|
||||||
|
try {
|
||||||
|
const isValid = await validateToken()
|
||||||
|
if (isValid) {
|
||||||
|
// 如果token有效,尝试从API获取用户信息
|
||||||
|
// 这里简化处理,直接使用token
|
||||||
|
setToken(storedToken)
|
||||||
|
setIsAuthenticated(true)
|
||||||
|
} else {
|
||||||
|
// token确认无效,清除
|
||||||
handleLogout()
|
handleLogout()
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// 验证过程出错,记录日志但不登出
|
||||||
|
console.error('验证token过程中出错:', error)
|
||||||
|
// 保留token,允许用户继续使用
|
||||||
|
setToken(storedToken)
|
||||||
|
setIsAuthenticated(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("验证token时出错:", error)
|
console.error("初始化认证状态时出错:", error)
|
||||||
|
// 非401错误不应强制登出
|
||||||
|
if (error instanceof Error &&
|
||||||
|
(error.message.includes('401') ||
|
||||||
|
error.message.includes('未授权') ||
|
||||||
|
error.message.includes('token'))) {
|
||||||
handleLogout()
|
handleLogout()
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
setIsInitialized(true)
|
setIsInitialized(true)
|
||||||
|
|||||||
@@ -361,22 +361,117 @@ export default function DeviceDetailPage() {
|
|||||||
features: updatedFeatures
|
features: updatedFeatures
|
||||||
} : null)
|
} : null)
|
||||||
|
|
||||||
// 调用API更新服务器配置
|
// 使用更安全的API调用方式,避免自动重定向
|
||||||
const response = await updateDeviceTaskConfig(configUpdate)
|
try {
|
||||||
|
// 获取token
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
if (!token) {
|
||||||
|
throw new Error('未找到授权信息');
|
||||||
|
}
|
||||||
|
|
||||||
if (response && response.code === 200) {
|
// 直接使用fetch,而不是通过API工具
|
||||||
toast.success(`${getFeatureName(feature)}${checked ? '已启用' : '已禁用'}`)
|
const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/v1/task-config`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify(configUpdate)
|
||||||
|
});
|
||||||
|
|
||||||
|
// 检查是否是401错误(未授权),这是唯一应该处理token的情况
|
||||||
|
if (response.status === 401) {
|
||||||
|
// 此处我们不立即跳转,而是给出错误提示
|
||||||
|
toast.error('认证已过期,请重新登录后再尝试操作');
|
||||||
|
console.error('API请求返回401未授权错误');
|
||||||
|
// 可以选择是否重定向到登录页面
|
||||||
|
// window.location.href = '/login';
|
||||||
|
throw new Error('认证已过期');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查响应是否正常
|
||||||
|
if (!response.ok) {
|
||||||
|
// 所有非401的HTTP错误
|
||||||
|
console.warn(`API返回HTTP错误: ${response.status} ${response.statusText}`);
|
||||||
|
|
||||||
|
// 尝试解析错误详情
|
||||||
|
try {
|
||||||
|
const errorResult = await response.json();
|
||||||
|
// 显示详细错误信息,但保持本地token
|
||||||
|
const errorMsg = errorResult?.msg || `服务器错误 (${response.status})`;
|
||||||
|
toast.error(`更新失败: ${errorMsg}`);
|
||||||
|
|
||||||
|
// 回滚UI更改
|
||||||
|
setDevice(prev => prev ? {
|
||||||
|
...prev,
|
||||||
|
features: { ...prev.features, [feature]: !checked }
|
||||||
|
} : null);
|
||||||
|
} catch (parseError) {
|
||||||
|
// 无法解析响应,可能是网络问题
|
||||||
|
console.error('无法解析错误响应:', parseError);
|
||||||
|
toast.error(`更新失败: 服务器无响应 (${response.status})`);
|
||||||
|
|
||||||
|
// 回滚UI更改
|
||||||
|
setDevice(prev => prev ? {
|
||||||
|
...prev,
|
||||||
|
features: { ...prev.features, [feature]: !checked }
|
||||||
|
} : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return; // 提前返回,避免继续处理
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应正常,尝试解析
|
||||||
|
try {
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
// 检查API响应码
|
||||||
|
if (result && result.code === 200) {
|
||||||
|
toast.success(`${getFeatureName(feature)}${checked ? '已启用' : '已禁用'}`);
|
||||||
|
} else if (result && result.code === 401) {
|
||||||
|
// API明确返回401,提示用户但不自动登出
|
||||||
|
toast.error('认证已过期,请重新登录后再尝试操作');
|
||||||
|
console.error('API请求返回401未授权状态码');
|
||||||
|
|
||||||
|
// 回滚UI更改
|
||||||
|
setDevice(prev => prev ? {
|
||||||
|
...prev,
|
||||||
|
features: { ...prev.features, [feature]: !checked }
|
||||||
|
} : null);
|
||||||
} else {
|
} else {
|
||||||
// 如果请求失败,回滚UI变更
|
// 其他API错误
|
||||||
|
const errorMsg = result?.msg || '未知错误';
|
||||||
|
console.warn(`API返回业务错误: ${result?.code} - ${errorMsg}`);
|
||||||
|
toast.error(`更新失败: ${errorMsg}`);
|
||||||
|
|
||||||
|
// 回滚UI更改
|
||||||
|
setDevice(prev => prev ? {
|
||||||
|
...prev,
|
||||||
|
features: { ...prev.features, [feature]: !checked }
|
||||||
|
} : null);
|
||||||
|
}
|
||||||
|
} catch (parseError) {
|
||||||
|
// 无法解析响应JSON
|
||||||
|
console.error('无法解析API响应:', parseError);
|
||||||
|
toast.error('更新失败: 无法解析服务器响应');
|
||||||
|
|
||||||
|
// 回滚UI更改
|
||||||
|
setDevice(prev => prev ? {
|
||||||
|
...prev,
|
||||||
|
features: { ...prev.features, [feature]: !checked }
|
||||||
|
} : null);
|
||||||
|
}
|
||||||
|
} catch (fetchError) {
|
||||||
|
console.error('请求错误:', fetchError)
|
||||||
|
|
||||||
|
// 回滚UI更改
|
||||||
setDevice(prev => prev ? {
|
setDevice(prev => prev ? {
|
||||||
...prev,
|
...prev,
|
||||||
features: { ...prev.features, [feature]: !checked }
|
features: { ...prev.features, [feature]: !checked }
|
||||||
} : null)
|
} : null)
|
||||||
|
|
||||||
// 处理错误信息,使用类型断言解决字段不一致问题
|
// 显示友好的错误提示
|
||||||
const anyResponse = response as any;
|
toast.error('网络请求失败,请稍后重试')
|
||||||
const errorMsg = anyResponse ? (anyResponse.message || anyResponse.msg || '未知错误') : '未知错误';
|
|
||||||
toast.error(`更新失败: ${errorMsg}`)
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`更新${getFeatureName(feature)}失败:`, error)
|
console.error(`更新${getFeatureName(feature)}失败:`, error)
|
||||||
|
|||||||
@@ -51,13 +51,79 @@ export const request = async <T>(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE_URL}${url}`, options);
|
const response = await fetch(`${API_BASE_URL}${url}`, options);
|
||||||
const result = await response.json();
|
|
||||||
|
// 检查网络响应状态
|
||||||
|
if (!response.ok) {
|
||||||
|
// 只有当响应状态码为401时才特殊处理为认证错误
|
||||||
|
if (response.status === 401) {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
// 直接调用handleTokenExpired而不是handleApiError,以处理401错误
|
||||||
|
// 因为有响应体的情况下,我们应该让handleApiResponse处理401
|
||||||
|
// 在这里只处理没有响应体的401网络错误
|
||||||
|
const errorMessage = `Unauthorized: ${response.statusText}`;
|
||||||
|
console.error('授权错误:', errorMessage);
|
||||||
|
|
||||||
|
// 尝试解析响应,如果无法解析才直接处理token过期
|
||||||
|
try {
|
||||||
|
await response.json();
|
||||||
|
// 如果能够解析,让后续代码处理
|
||||||
|
} catch (e) {
|
||||||
|
// 如果无法解析,说明是纯网络层401错误,直接处理token过期
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
localStorage.removeItem('user');
|
||||||
|
window.location.href = '/login';
|
||||||
|
throw new Error(errorMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 其他HTTP错误正常返回,让上层组件自行处理
|
||||||
|
}
|
||||||
|
|
||||||
|
let result;
|
||||||
|
try {
|
||||||
|
result = await response.json();
|
||||||
|
} catch (parseError) {
|
||||||
|
// 处理JSON解析错误
|
||||||
|
console.error('无法解析响应JSON:', parseError);
|
||||||
|
throw new Error('服务器响应格式错误');
|
||||||
|
}
|
||||||
|
|
||||||
// 使用响应拦截器处理响应
|
// 使用响应拦截器处理响应
|
||||||
return handleApiResponse<T>(response, result);
|
if (result && result.code === 401) {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
localStorage.removeItem('user');
|
||||||
|
// 使用客户端导航而不是直接修改window.location
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.href = '/login';
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
throw new Error(result.msg || '登录已过期,请重新登录');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回结果而不调用可能会清除token的handleApiResponse
|
||||||
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 使用错误拦截器处理错误
|
// 使用错误拦截器处理错误
|
||||||
return handleApiError(error);
|
// 只有在确认是401错误时才清除token
|
||||||
|
if (error instanceof Error &&
|
||||||
|
(error.message.includes('401') ||
|
||||||
|
(error.message.toLowerCase().includes('unauthorized') &&
|
||||||
|
error.message.toLowerCase().includes('token')))) {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
localStorage.removeItem('user');
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.href = '/login';
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 其他错误直接抛出但不调用handleApiError
|
||||||
|
console.error('API请求错误:', error);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -103,11 +169,59 @@ export const validateToken = async (): Promise<boolean> => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await loginApi.getUserInfo();
|
// 直接使用fetch而不是通过api调用,以便捕获具体错误
|
||||||
return response.code === 200;
|
const token = getToken();
|
||||||
} catch (error) {
|
if (!token) return false;
|
||||||
|
|
||||||
|
const response = await fetch(`${API_BASE_URL}/v1/auth/info`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 如果状态码是401,明确是认证问题
|
||||||
|
if (response.status === 401) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果是其他HTTP错误,我们不确定是否是认证问题
|
||||||
|
if (!response.ok) {
|
||||||
|
// 对于非401错误,不要立即判定token无效
|
||||||
|
console.warn(`验证token时收到HTTP错误: ${response.status} ${response.statusText}`);
|
||||||
|
|
||||||
|
// 尝试读取响应内容
|
||||||
|
try {
|
||||||
|
const result = await response.json();
|
||||||
|
// 只有明确返回code为401才判断为token无效
|
||||||
|
if (result && result.code === 401) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// 其他错误代码视为服务端问题,不影响token有效性
|
||||||
|
return true;
|
||||||
|
} catch (parseError) {
|
||||||
|
// 无法解析响应,视为网络或服务器问题,不影响token
|
||||||
|
console.error('无法解析验证token的响应:', parseError);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 正常情况下,尝试解析响应并检查code
|
||||||
|
try {
|
||||||
|
const result = await response.json();
|
||||||
|
return result.code === 200;
|
||||||
|
} catch (parseError) {
|
||||||
|
// 无法解析响应,视为网络或服务器问题,不影响token
|
||||||
|
console.error('无法解析验证token的响应:', parseError);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// 网络错误或其他异常不应该导致token被视为无效
|
||||||
|
console.error('验证token时发生异常:', error);
|
||||||
|
// 对于网络连接问题,不直接判定为token无效
|
||||||
|
return true;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 刷新令牌
|
// 刷新令牌
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
import { toast } from "@/components/ui/use-toast";
|
||||||
|
|
||||||
// Token过期处理
|
// Token过期处理
|
||||||
export const handleTokenExpired = () => {
|
export const handleTokenExpired = () => {
|
||||||
@@ -15,34 +16,49 @@ export const handleTokenExpired = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 响应拦截器
|
// 显示API错误,但不会重定向
|
||||||
|
export const showApiError = (error: any, defaultMessage: string = '请求失败') => {
|
||||||
|
if (typeof window === 'undefined') return; // 服务端不处理
|
||||||
|
|
||||||
|
let errorMessage = defaultMessage;
|
||||||
|
|
||||||
|
// 尝试从各种可能的错误格式中获取消息
|
||||||
|
if (error) {
|
||||||
|
if (typeof error === 'string') {
|
||||||
|
errorMessage = error;
|
||||||
|
} else if (error instanceof Error) {
|
||||||
|
errorMessage = error.message || defaultMessage;
|
||||||
|
} else if (typeof error === 'object') {
|
||||||
|
// 尝试从API响应中获取错误消息
|
||||||
|
errorMessage = error.msg || error.message || error.error || defaultMessage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示错误消息
|
||||||
|
if (typeof toast !== 'undefined') {
|
||||||
|
toast({
|
||||||
|
title: "请求错误",
|
||||||
|
description: errorMessage,
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.error('API错误:', errorMessage);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 响应拦截器 - 此函数已不再使用,保留仅用于兼容性
|
||||||
export const handleApiResponse = <T>(response: Response, result: any): T => {
|
export const handleApiResponse = <T>(response: Response, result: any): T => {
|
||||||
// 处理token过期情况
|
// 不再处理401响应,而是直接返回结果
|
||||||
if (result && (result.code === 401 || result.msg?.includes('token'))) {
|
// 401的处理已移至api.ts中直接处理
|
||||||
// 仅在客户端处理token过期
|
console.warn('handleApiResponse已弃用,请直接在api.ts中处理响应');
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
handleTokenExpired();
|
|
||||||
}
|
|
||||||
throw new Error(result.msg || '登录已过期,请重新登录');
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理API错误
|
// 处理API错误 - 此函数已不再使用,保留仅用于兼容性
|
||||||
export const handleApiError = (error: unknown): never => {
|
export const handleApiError = (error: unknown): never => {
|
||||||
console.error('API请求错误:', error);
|
console.error('API请求错误:', error);
|
||||||
|
console.warn('handleApiError已弃用,请直接在api.ts中处理错误');
|
||||||
if (error instanceof Error) {
|
|
||||||
// 如果是未授权错误,可能是token过期
|
|
||||||
if (error.message.includes('401') || error.message.includes('token') || error.message.includes('授权')) {
|
|
||||||
// 仅在客户端处理token过期
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
handleTokenExpired();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error('未知错误,请稍后重试');
|
throw new Error('未知错误,请稍后重试');
|
||||||
};
|
};
|
||||||
Reference in New Issue
Block a user