Files
user-system/frontend/admin/src/lib/http/csrf.ts
long-agent 3f3bb82f1d fix: v6 code review P0 auth/IDOR fixes + frontend regression patches
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
2026-04-23 07:14:12 +08:00

153 lines
3.5 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.
/**
* 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']