Fix auth security: add request timeout, safe redirects, and memory-only token support.
This commit is contained in:
@@ -1,11 +1,20 @@
|
||||
import { ApiError } from '@/types/api'
|
||||
|
||||
const DEFAULT_TIMEOUT = 15000
|
||||
|
||||
// 允许外部注入 token 获取函数,避免与 store 循环依赖
|
||||
let tokenGetter: (() => string | null) | null = null
|
||||
export function setTokenGetter(fn: () => string | null) {
|
||||
tokenGetter = fn
|
||||
}
|
||||
|
||||
interface RequestOptions extends RequestInit {
|
||||
params?: Record<string, unknown>
|
||||
timeout?: number
|
||||
}
|
||||
|
||||
async function request<T = unknown>(url: string, options: RequestOptions = {}): Promise<T> {
|
||||
const token = localStorage.getItem('access_token')
|
||||
const token = tokenGetter?.() ?? localStorage.getItem('access_token')
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -27,28 +36,54 @@ async function request<T = unknown>(url: string, options: RequestOptions = {}):
|
||||
url = `${url}${separator}${query}`
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { params: _params, ...fetchOptions } = options
|
||||
const { params: _params, timeout: _timeout, ...fetchOptions } = options
|
||||
|
||||
const response = await fetch(url, { ...fetchOptions, headers })
|
||||
// 超时控制
|
||||
const controller = new AbortController()
|
||||
const timeoutId = setTimeout(() => controller.abort(), options.timeout ?? DEFAULT_TIMEOUT)
|
||||
|
||||
// 401 未授权
|
||||
if (response.status === 401) {
|
||||
localStorage.removeItem('access_token')
|
||||
localStorage.removeItem('refresh_token')
|
||||
localStorage.removeItem('user')
|
||||
window.location.href = '/login'
|
||||
throw new ApiError('登录已过期,请重新登录', 401)
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
...fetchOptions,
|
||||
headers,
|
||||
signal: controller.signal,
|
||||
})
|
||||
|
||||
// 401 未授权:保留当前路径作为 redirect 参数
|
||||
if (response.status === 401) {
|
||||
localStorage.removeItem('access_token')
|
||||
localStorage.removeItem('refresh_token')
|
||||
localStorage.removeItem('user')
|
||||
const redirect = encodeURIComponent(
|
||||
window.location.pathname + window.location.search,
|
||||
)
|
||||
window.location.href = `/login?redirect=${redirect}`
|
||||
throw new ApiError('登录已过期,请重新登录', 401)
|
||||
}
|
||||
|
||||
// 防护非 JSON 响应(如网关 HTML 错误页)
|
||||
let result
|
||||
try {
|
||||
result = await response.json()
|
||||
} catch {
|
||||
throw new ApiError('服务器返回了非预期的响应格式', response.status)
|
||||
}
|
||||
|
||||
// 业务错误
|
||||
if (result.code !== 0) {
|
||||
throw new ApiError(result.message || '请求失败', result.code)
|
||||
}
|
||||
|
||||
return result.data
|
||||
} catch (error) {
|
||||
if (error instanceof ApiError) throw error
|
||||
if (error instanceof DOMException && error.name === 'AbortError') {
|
||||
throw new ApiError('请求超时,请检查网络连接', 0)
|
||||
}
|
||||
throw error
|
||||
} finally {
|
||||
clearTimeout(timeoutId)
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
|
||||
// 业务错误
|
||||
if (result.code !== 0) {
|
||||
throw new ApiError(result.message || '请求失败', result.code)
|
||||
}
|
||||
|
||||
return result.data
|
||||
}
|
||||
|
||||
export const api = {
|
||||
|
||||
Reference in New Issue
Block a user