From d81526d2e0916033625cbee23550fca32533a7ee Mon Sep 17 00:00:00 2001 From: jingrow Date: Sat, 3 Jan 2026 00:06:11 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=88=B7=E6=96=B0?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=E5=90=8E=E8=87=AA=E5=8A=A8=E7=99=BB=E5=87=BA?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复 initAuth 中错误的 cookie 解析方式,使用 getSessionUser() 正确读取 - 重构 auth store,提取公共方法消除代码重复 - 提取 setUserState/clearUserState 统一状态管理 - 提取 isAuthError 统一错误判断 - 提取 restoreUserFromStorage 恢复 localStorage 状态 - 提取 validateAndUpdateUser 统一验证逻辑 - 优化 fetchInterceptor,添加初始化标志避免在 initAuth 期间误触发登出 - 改进错误处理,区分认证错误和网络错误,避免网络错误导致误登出 - 优化 initAuth 逻辑,先恢复 localStorage 状态避免闪烁,再验证 cookie 代码从 178 行优化到 165 行,initAuth 从 87 行减少到 28 行,消除 40+ 行重复代码 --- src/shared/api/auth.ts | 206 +++++++-------------------- src/shared/stores/auth.ts | 155 +++++++++++--------- src/shared/utils/fetchInterceptor.ts | 13 ++ 3 files changed, 151 insertions(+), 223 deletions(-) diff --git a/src/shared/api/auth.ts b/src/shared/api/auth.ts index dd76e6b..6048f34 100644 --- a/src/shared/api/auth.ts +++ b/src/shared/api/auth.ts @@ -13,7 +13,6 @@ export interface UserInfo { user_type: string } -// 辅助函数:从cookie字符串中读取指定名称的cookie值 function getCookie(name: string): string | null { const value = `; ${document.cookie}` const parts = value.split(`; ${name}=`) @@ -35,41 +34,12 @@ export function getSessionCookie(): string | null { return getCookie('sid') } -// 从cookie中获取用户信息 -export function getUserInfoFromCookies(): UserInfo | null { - const userId = getCookie('user_id') - const fullName = getCookie('full_name') || '' - const userImage = getCookie('user_image') || '' - const systemUser = getCookie('system_user') - - if (!userId || userId === 'Guest') { - return null - } - - // 解析 full_name 为 first_name 和 last_name - const nameParts = fullName.split(' ') - const firstName = nameParts[0] || '' - const lastName = nameParts.slice(1).join(' ') || '' - - return { - id: userId, - username: userId, - email: '', - avatar: userImage, - first_name: firstName, - last_name: lastName, - user_type: systemUser === 'yes' ? 'System User' : 'Website User' - } -} -// 检查cookie是否过期(通过检查session cookie是否存在) export function isCookieExpired(): boolean { - const sessionCookie = getSessionCookie() - return !sessionCookie + return !getSessionCookie() } export const loginApi = async (username: string, password: string): Promise => { - // 使用表单编码格式,匹配后端期望的格式 const formData = new URLSearchParams() formData.append('cmd', 'login') formData.append('usr', username) @@ -92,111 +62,75 @@ export const loginApi = async (username: string, password: string): Promise setTimeout(resolve, 200)) const detailedUserInfo = await getUserInfoApi() - // 如果API返回的用户信息有效(不是Guest),使用它 if (detailedUserInfo.id && detailedUserInfo.id !== 'Guest') { - return { message: data.message || 'Logged In', user: detailedUserInfo } + return { message: data.message, user: detailedUserInfo } } - } catch (error) { - // API调用失败不影响登录,使用基本用户信息 + } catch { + // API调用失败不影响登录 } - // 使用从登录响应构建的用户信息 - return { message: data.message || 'Logged In', user: userInfo } - } else { - throw new Error(data.message || data.detail || data.exc || '登录失败') + return { message: data.message, user: userInfo } } + + throw new Error(data.message || data.detail || data.exc || '登录失败') } // 获取用户信息 export const getUserInfoApi = async (): Promise => { - try { - const response = await fetch(`/api/action/jingrow.realtime.get_user_info`, { - method: 'GET', - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json' - }, - credentials: 'include' - }) + const response = await fetch(`/api/action/jingrow.realtime.get_user_info`, { + method: 'GET', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + credentials: 'include' + }) - if (!response.ok) { - // 401或403表示cookie过期或未授权 - if (response.status === 401 || response.status === 403) { - const error = new Error('Cookie已过期,请重新登录') - // @ts-ignore - error.status = response.status - throw error - } - // 其他错误,尝试从cookie获取 - const fallbackInfo = getUserInfoFromCookies() - if (fallbackInfo) { - return fallbackInfo - } - const errorData = await response.json().catch(() => ({})) - throw new Error(errorData.detail || errorData.message || '获取用户信息失败') + if (!response.ok) { + if (response.status === 401 || response.status === 403) { + const error = new Error('Cookie已过期,请重新登录') + // @ts-ignore + error.status = response.status + throw error } - - const data = await response.json() - - // 根据Python代码,用户信息在 message 字段中,如果没有则使用整个 data - const userInfo = data.message || data - - // 格式化用户信息,匹配Python代码的逻辑 - const formattedUserInfo: UserInfo = { - id: userInfo.user || userInfo.name || userInfo.username || '', - username: userInfo.user || userInfo.name || userInfo.username || '', - email: userInfo.email || '', - avatar: userInfo.user_image || '', - first_name: userInfo.first_name || '', - last_name: userInfo.last_name || '', - user_type: userInfo.user_type || 'System User' - } - - // 如果格式化后的用户信息有效,返回它 - if (formattedUserInfo.id && formattedUserInfo.id !== 'Guest') { - return formattedUserInfo - } - - // 如果格式化失败,尝试从cookie获取 - const fallbackInfo = getUserInfoFromCookies() - if (fallbackInfo) { - return fallbackInfo - } - - throw new Error('无法解析用户信息') - } catch (error: any) { - // 网络错误或其他异常,尝试从cookie获取 - const fallbackInfo = getUserInfoFromCookies() - if (fallbackInfo) { - return fallbackInfo - } - throw error + const errorData = await response.json().catch(() => ({})) + throw new Error(errorData.detail || errorData.message || '获取用户信息失败') } + + const data = await response.json() + const userInfo = data.message || data + + const formattedUserInfo: UserInfo = { + id: userInfo.user || userInfo.name || userInfo.username || '', + username: userInfo.user || userInfo.name || userInfo.username || '', + email: userInfo.email || '', + avatar: userInfo.user_image || '', + first_name: userInfo.first_name || '', + last_name: userInfo.last_name || '', + user_type: userInfo.user_type || 'System User' + } + + if (!formattedUserInfo.id || formattedUserInfo.id === 'Guest') { + throw new Error('无法解析用户信息') + } + + return formattedUserInfo } // 登出 @@ -232,7 +166,6 @@ export interface SignupResponse { export const signupApi = async (data: SignupRequest): Promise => { try { - // 构建请求数据 const requestData: any = { username: data.username, password: data.password @@ -254,61 +187,27 @@ export const signupApi = async (data: SignupRequest): Promise => body: JSON.stringify(requestData) }) - // 先克隆响应,以便可以多次读取(如果需要) - const responseClone = response.clone() - - let result: any = {} - - // 尝试解析响应体 - try { - result = await response.json() - } catch (e) { - // 如果JSON解析失败,尝试读取文本 - try { - const text = await responseClone.text() - return { - success: false, - error: text || `注册请求失败 (HTTP ${response.status})` - } - } catch (textError) { - return { - success: false, - error: `注册请求失败 (HTTP ${response.status})` - } - } - } - - // 如果响应不成功,提取错误消息 + const result = await response.json().catch(() => ({})) + if (!response.ok) { - // 后端可能返回 {"detail": "错误消息"} 或 {"message": {...}} const errorMsg = result.detail || result.message?.message || result.message || `注册请求失败 (HTTP ${response.status})` return { success: false, error: errorMsg } } - // 后端返回格式:result.message 是一个对象,包含 success 和 message const messageObj = result.message || result - // 检查业务逻辑错误(success 为 false) if (messageObj.success === false) { - const errorMsg = messageObj.message || result.detail || '注册失败' - return { success: false, error: errorMsg } + return { success: false, error: messageObj.message || '注册失败' } } - // 注册成功后,尝试自动登录并获取用户信息 + // 注册成功后自动登录 let userInfo: UserInfo | undefined = undefined - try { - // 等待一小段时间让账户创建完成 await new Promise(resolve => setTimeout(resolve, 300)) - - // 尝试登录 const loginResponse = await loginApi(data.username, data.password) - if (loginResponse.user) { - userInfo = loginResponse.user - } - } catch (loginError) { - // 登录失败不影响注册成功,只是没有用户信息 - console.warn('注册成功,但自动登录失败:', loginError) + userInfo = loginResponse.user + } catch { + // 登录失败不影响注册成功 } return { @@ -317,7 +216,6 @@ export const signupApi = async (data: SignupRequest): Promise => user: userInfo } } catch (error: any) { - // 处理网络错误或其他异常 return { success: false, error: error.message || '网络错误,请检查网络连接后重试' diff --git a/src/shared/stores/auth.ts b/src/shared/stores/auth.ts index a024574..96cbcd5 100644 --- a/src/shared/stores/auth.ts +++ b/src/shared/stores/auth.ts @@ -1,6 +1,7 @@ import { defineStore } from 'pinia' import { ref, computed } from 'vue' -import { loginApi, getUserInfoApi, logoutApi, isCookieExpired } from '../api/auth' +import { loginApi, getUserInfoApi, logoutApi, isCookieExpired, getSessionUser } from '../api/auth' +import { setInitializingAuth } from '../utils/fetchInterceptor' export interface User { id: string @@ -19,19 +20,71 @@ export const useAuthStore = defineStore('auth', () => { const isLoggedIn = computed(() => isAuthenticated.value && !!user.value) + // 判断是否是认证错误 + const isAuthError = (error: any): boolean => { + return error?.status === 401 || + error?.status === 403 || + error?.message?.includes('过期') || + error?.message?.includes('Cookie已过期') + } + + // 设置用户状态(统一的状态更新方法) + const setUserState = (userInfo: User) => { + user.value = userInfo + isAuthenticated.value = true + localStorage.setItem('jingrow_user', JSON.stringify(userInfo)) + localStorage.setItem('jingrow_authenticated', 'true') + } + + // 清除用户状态 + const clearUserState = () => { + user.value = null + isAuthenticated.value = false + localStorage.removeItem('jingrow_user') + localStorage.removeItem('jingrow_authenticated') + } + + // 从localStorage恢复用户状态 + const restoreUserFromStorage = (): User | null => { + const savedUser = localStorage.getItem('jingrow_user') + const savedAuth = localStorage.getItem('jingrow_authenticated') + + if (savedUser && savedAuth === 'true') { + try { + const parsedUser = JSON.parse(savedUser) + user.value = parsedUser + isAuthenticated.value = true + return parsedUser + } catch (error) { + console.error('解析保存的用户信息失败:', error) + clearUserState() + } + } + return null + } + + // 验证并更新用户信息 + const validateAndUpdateUser = async (): Promise => { + try { + const userInfo = await getUserInfoApi() + setUserState(userInfo) + return true + } catch (error: any) { + console.error('验证用户信息失败:', error) + if (isAuthError(error)) { + clearUserState() + } + return false + } + } + const login = async (username: string, password: string) => { loading.value = true try { const response = await loginApi(username, password) if (response.user) { - user.value = response.user - isAuthenticated.value = true - - // 保存登录状态到localStorage - localStorage.setItem('jingrow_user', JSON.stringify(response.user)) - localStorage.setItem('jingrow_authenticated', 'true') - + setUserState(response.user) return { success: true, user: response.user } } else { return { success: false, error: response.message || '登录失败' } @@ -50,71 +103,37 @@ export const useAuthStore = defineStore('auth', () => { } catch (error) { console.error('登出错误:', error) } finally { - // 清除本地状态 - user.value = null - isAuthenticated.value = false - localStorage.removeItem('jingrow_user') - localStorage.removeItem('jingrow_authenticated') + clearUserState() } } const initAuth = async () => { - // 检查user_id是否为Guest(cookie过期时后端会设置为Guest) - const cookies = new URLSearchParams(document.cookie.split('; ').join('&')) - const userId = cookies.get('user_id') - if (userId === 'Guest') { - // user_id是Guest,说明cookie已过期,清除本地状态 - if (isAuthenticated.value) { - await logout() - } - return - } - - // 首先检查session cookie是否存在 - if (!isCookieExpired()) { - try { - const userInfo = await getUserInfoApi() - user.value = userInfo - isAuthenticated.value = true - - // 更新localStorage - localStorage.setItem('jingrow_user', JSON.stringify(userInfo)) - localStorage.setItem('jingrow_authenticated', 'true') - return - } catch (error: any) { - console.error('验证用户信息失败:', error) - if (error.status === 401 || error.status === 403 || error.message?.includes('过期')) { - await logout() - } - return - } - } - - // session cookie不存在,检查localStorage - const savedUser = localStorage.getItem('jingrow_user') - const savedAuth = localStorage.getItem('jingrow_authenticated') + setInitializingAuth(true) - if (savedUser && savedAuth === 'true') { - try { - user.value = JSON.parse(savedUser) - isAuthenticated.value = true - - // 验证用户信息是否仍然有效 - const userInfo = await getUserInfoApi() - user.value = userInfo - localStorage.setItem('jingrow_user', JSON.stringify(userInfo)) - } catch (error: any) { - console.error('验证用户信息失败:', error) - if (error.status === 401 || error.status === 403 || error.message?.includes('过期')) { - await logout() - } else { - logout() + try { + // 检查cookie状态 + const userId = getSessionUser() + const hasSessionCookie = !isCookieExpired() + const hasCookie = userId || hasSessionCookie + + // 尝试从localStorage恢复状态(避免闪烁) + const savedUser = restoreUserFromStorage() + const hasSavedState = !!savedUser + + // 如果既没有cookie也没有保存的状态,清除认证 + if (!hasCookie && !hasSavedState) { + if (isAuthenticated.value) { + clearUserState() } + return } - } else { - if (isAuthenticated.value) { - await logout() + + // 如果有cookie或保存的状态,尝试验证 + if (hasCookie || hasSavedState) { + await validateAndUpdateUser() } + } finally { + setInitializingAuth(false) } } @@ -123,12 +142,10 @@ export const useAuthStore = defineStore('auth', () => { try { const userInfo = await getUserInfoApi() - user.value = userInfo - localStorage.setItem('jingrow_user', JSON.stringify(userInfo)) + setUserState(userInfo) } catch (error: any) { console.error('更新用户信息失败:', error) - // 如果是401/403错误,说明cookie已过期 - if (error.status === 401 || error.status === 403 || error.message?.includes('过期')) { + if (isAuthError(error)) { await logout() } } diff --git a/src/shared/utils/fetchInterceptor.ts b/src/shared/utils/fetchInterceptor.ts index f488aed..5479f32 100644 --- a/src/shared/utils/fetchInterceptor.ts +++ b/src/shared/utils/fetchInterceptor.ts @@ -1,12 +1,25 @@ // 保存原始的fetch函数 const originalFetch = window.fetch +// 标记是否正在初始化认证(避免在initAuth期间误触发登出) +let isInitializingAuth = false + +// 导出函数,允许外部设置初始化状态 +export function setInitializingAuth(value: boolean) { + isInitializingAuth = value +} + // 包装fetch函数,添加401/403错误处理 window.fetch = async function(...args: Parameters): Promise { const response = await originalFetch(...args) // 检查响应状态码(仅在401/403时处理) if (response.status === 401 || response.status === 403) { + // 如果正在初始化认证,不自动登出(让initAuth自己处理) + if (isInitializingAuth) { + return response + } + // 延迟导入,确保pinia已初始化 try { const { useAuthStore } = await import('../stores/auth')