Backend fixes: - auth_handler: P0 认证逻辑修复 - ratelimit: 限速中间件增强 + 新增单元测试 - auth_service: 认证服务逻辑完善 + 新增测试 - server: server 配置增强 + 新增测试 - handler_test: 新增 handler 层集成测试 - auth_bootstrap_test: bootstrap 路径测试 Frontend patches: - LoginPage/RegisterPage: CSRF + 表单交互修复 - BootstrapAdminPage: 引导流程修复 - DevicesPage: 设备管理页修复 - auth/social-accounts/users/webhooks services: 类型修正 - csrf.ts: CSRF token 处理修正 - E2E 脚本: CDP smoke + auth e2e 增强 Docs: - FULL_CODE_REVIEW_REPORT_2026-04-20 - report-v6 执行计划 - REAL_PROJECT_STATUS 更新 - .gitignore: 新增 .gocache-*/config.yaml 排除 验证: go build/vet 0错误, go test 42/42 PASS, 0 FAIL
153 lines
3.5 KiB
TypeScript
153 lines
3.5 KiB
TypeScript
/**
|
||
* 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'
|
||
import { getAccessToken } from './auth-session'
|
||
|
||
// 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 {
|
||
const headers: Record<string, string> = {
|
||
'Content-Type': 'application/json',
|
||
}
|
||
const accessToken = getAccessToken()
|
||
if (accessToken) {
|
||
headers.Authorization = `Bearer ${accessToken}`
|
||
}
|
||
|
||
// 使用原生 fetch 避免循环依赖
|
||
const response = await fetch(buildUrl('/auth/csrf-token'), {
|
||
method: 'GET',
|
||
credentials: 'include',
|
||
headers,
|
||
})
|
||
|
||
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']
|