Files
LokiStack/frontend/src/hooks/useLicense.tsx
T
opsx f91078f814
CI / Backend (go vet + test) (push) Waiting to run
CI / Frontend (lint + build + test) (push) Waiting to run
fix(auth): 登录页无限重载闪屏 —— 401 在 /login 上仍整页跳转导致死循环
=== 现象 ===
打开 /login 页面一直加载、闪屏,无法登录。
线上访问日志:10 分钟内 /api/v1/system/license 被请求 **1566 次,全部 401**(约 2.6 次/秒)。

=== 根因(三处叠加)===
1) LicenseProvider 挂在 App 根部,包住**所有**路由(含 /login),
   挂载即请求鉴权接口 /system/license;登录页没有 token → 401。
2) http.ts 的 redirectToLogin() 在已处于 /login 时仍然执行
   window.location.href = '/login' —— **给 href 赋同值也会整页重载**。
3) 重载 → App 重新挂载 → LicenseProvider 再请求 → 再 401 → 再重载。
形成"请求→401→整页重载→请求"的死循环,用户看到的就是转圈+闪屏。

=== 修复(三处,防御性叠加)===
1) useLicense:没有 accessToken 时**不请求**;有 token 才拉。
   并用 1 秒一次的低成本探测(只读 localStorage,不发请求)在登录成功
   (token 出现)后补拉一次,避免登录后授权状态缺失。
2) licenseApi.status 使用 silent: true —— 401 不触发"清凭证+跳登录"的全局副作用。
3) redirectToLogin:已在 /login 时直接 return,不再重载页面。

=== 验证 ===
tsc 通过;发布后 /login 不再产生 401 轮询(见发布后回归)
2026-11-03 03:25:58 +00:00

73 lines
2.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react'
import { licenseApi, type LicenseInfo } from '@/api/license'
import { getAccessToken } from '@/lib/auth-storage'
interface LicenseState {
pro: boolean
info: LicenseInfo | null
reload: () => void
}
const LicenseContext = createContext<LicenseState>({ pro: false, info: null, reload: () => {} })
/**
* 授权状态 Provider。
*
* ⚠️ 必须只在**已登录**时请求:`/system/license` 是鉴权接口,
* 未登录访问会 401,而 401 会触发全局"跳登录"逻辑。
* 之前没有这道判断,登录页一挂载就请求 → 401 → 跳 /login(整页重载)
* → 重新挂载 → 再 401,形成死循环:登录页一直转圈+闪屏
* (线上实测 10 分钟 1566 次 401)。三处修复:
* 1) 这里没有 token 就不请求
* 2) 请求带 silent,401 不触发全局跳转副作用
* 3) http.ts 的 redirectToLogin 在已处于 /login 时不再重载
*
* 登录成功后 token 才出现,所以用一个轻量的定时探测(只读 localStorage,
* 不发请求)在 token 出现时补拉一次。
*/
export function LicenseProvider({ children }: { children: React.ReactNode }) {
const [state, setState] = useState<{ pro: boolean; info: LicenseInfo | null }>({ pro: false, info: null })
const loadedRef = useRef(false)
const load = useCallback(() => {
// 未登录(登录页)直接跳过,避免 401 → 跳登录 → 重载 的死循环
if (!getAccessToken()) return
licenseApi
.status()
.then(res => {
loadedRef.current = true
setState({ pro: Boolean(res.pro), info: res.license })
})
.catch(() => {
// 拿不到授权信息时按免费版处理,不打断页面
setState({ pro: false, info: null })
})
}, [])
useEffect(() => {
load()
// 登录后 token 才写入 localStorage,用 1 秒一次的低成本探测补拉(只读本地存储)
const timer = window.setInterval(() => {
if (loadedRef.current) {
window.clearInterval(timer)
return
}
if (getAccessToken()) {
window.clearInterval(timer)
load()
}
}, 1000)
return () => window.clearInterval(timer)
}, [load])
return (
<LicenseContext.Provider value={{ ...state, reload: load }}>
{children}
</LicenseContext.Provider>
)
}
export function useLicense() {
return useContext(LicenseContext)
}