2026-04-02 11:20:20 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* CSRF Token 管理
|
|
|
|
|
|
*
|
|
|
|
|
|
* CSRF 保护机制:
|
|
|
|
|
|
* 1. GET 请求获取 CSRF Token(从 cookie 或 API)
|
|
|
|
|
|
* 2. POST/PUT/DELETE 请求将 Token 添加到 X-CSRF-Token 头
|
|
|
|
|
|
*
|
|
|
|
|
|
* 注意:由于使用 Bearer Token 认证(存储在内存中),
|
|
|
|
|
|
* CSRF 风险相对较低,但为增强安全性仍建议对关键操作启用。
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
// 注意:避免从 './client' 导入,防止循环依赖
|
|
|
|
|
|
// 使用原生 fetch 获取 CSRF Token
|
|
|
|
|
|
|
|
|
|
|
|
import { config } from '@/lib/config'
|
2026-04-23 07:14:12 +08:00
|
|
|
|
import { getAccessToken } from './auth-session'
|
2026-04-02 11:20:20 +08:00
|
|
|
|
|
|
|
|
|
|
// CSRF Token 存储
|
|
|
|
|
|
let csrfToken: string | null = null
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取 CSRF Token
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function getCSRFToken(): string | null {
|
|
|
|
|
|
return csrfToken
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 设置 CSRF Token
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function setCSRFToken(token: string): void {
|
|
|
|
|
|
csrfToken = token
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 从 cookie 中读取 CSRF Token
|
|
|
|
|
|
* Django/Laravel 等框架通常在 cookie 中设置 csrftoken
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function getCSRFTokenFromCookie(): string | null {
|
|
|
|
|
|
if (typeof document === 'undefined') {
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const match = document.cookie.match(/csrftoken=([^;]+)/)
|
|
|
|
|
|
return match ? match[1] : null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 解析 API 基础 URL
|
|
|
|
|
|
* 注意:此函数复制自 client.ts 以避免循环依赖
|
|
|
|
|
|
*/
|
|
|
|
|
|
function resolveApiBaseUrl(): URL {
|
|
|
|
|
|
const origin = typeof window !== 'undefined' ? window.location.origin : 'http://localhost'
|
|
|
|
|
|
const rawBaseUrl = /^https?:\/\//i.test(config.apiBaseUrl)
|
|
|
|
|
|
? config.apiBaseUrl
|
|
|
|
|
|
: config.apiBaseUrl.startsWith('/')
|
|
|
|
|
|
? config.apiBaseUrl
|
|
|
|
|
|
: `/${config.apiBaseUrl}`
|
|
|
|
|
|
|
|
|
|
|
|
const baseUrl = new URL(rawBaseUrl, origin)
|
|
|
|
|
|
|
|
|
|
|
|
if (!baseUrl.pathname.endsWith('/')) {
|
|
|
|
|
|
baseUrl.pathname = `${baseUrl.pathname}/`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return baseUrl
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 构建完整 URL
|
|
|
|
|
|
*/
|
|
|
|
|
|
function buildUrl(path: string): string {
|
|
|
|
|
|
const normalizedPath = path.replace(/^\/+/, '')
|
|
|
|
|
|
const url = new URL(normalizedPath, resolveApiBaseUrl())
|
|
|
|
|
|
return url.toString()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 初始化 CSRF Token
|
|
|
|
|
|
* 从 cookie 或 API 获取 Token 并存储
|
|
|
|
|
|
*/
|
|
|
|
|
|
export async function initCSRFToken(): Promise<string | null> {
|
|
|
|
|
|
// 优先从 cookie 获取
|
|
|
|
|
|
let token = getCSRFTokenFromCookie()
|
|
|
|
|
|
|
|
|
|
|
|
if (!token) {
|
|
|
|
|
|
try {
|
2026-04-23 07:14:12 +08:00
|
|
|
|
const headers: Record<string, string> = {
|
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
|
}
|
|
|
|
|
|
const accessToken = getAccessToken()
|
|
|
|
|
|
if (accessToken) {
|
|
|
|
|
|
headers.Authorization = `Bearer ${accessToken}`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 11:20:20 +08:00
|
|
|
|
// 使用原生 fetch 避免循环依赖
|
|
|
|
|
|
const response = await fetch(buildUrl('/auth/csrf-token'), {
|
|
|
|
|
|
method: 'GET',
|
|
|
|
|
|
credentials: 'include',
|
2026-04-23 07:14:12 +08:00
|
|
|
|
headers,
|
2026-04-02 11:20:20 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
if (response.ok) {
|
|
|
|
|
|
const result = await response.json()
|
|
|
|
|
|
// 后端返回字段名为 csrf_token
|
|
|
|
|
|
if (result.code === 0 && result.data?.csrf_token) {
|
|
|
|
|
|
token = result.data.csrf_token
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
// API 不支持,使用 cookie 中的 token(如果有)
|
|
|
|
|
|
token = getCSRFTokenFromCookie()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (token) {
|
|
|
|
|
|
setCSRFToken(token)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return token
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 清除 CSRF Token(登出时调用)
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function clearCSRFToken(): void {
|
|
|
|
|
|
csrfToken = null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* CSRF Token 头名称
|
|
|
|
|
|
*/
|
|
|
|
|
|
export const CSRF_HEADER_NAME = 'X-CSRF-Token'
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取带 CSRF Token 的请求头
|
|
|
|
|
|
* 用于 POST/PUT/DELETE 请求
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function getCSRFHeaders(): Record<string, string> {
|
|
|
|
|
|
const token = csrfToken || getCSRFTokenFromCookie()
|
|
|
|
|
|
if (!token) {
|
|
|
|
|
|
return {}
|
|
|
|
|
|
}
|
|
|
|
|
|
return {
|
|
|
|
|
|
[CSRF_HEADER_NAME]: token
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 需要 CSRF 保护的方法列表
|
|
|
|
|
|
*/
|
|
|
|
|
|
export const CSRF_PROTECTED_METHODS = ['POST', 'PUT', 'DELETE', 'PATCH']
|