feat: admin frontend - React + Vite, auth pages, user management, roles, permissions, webhooks, devices, logs
This commit is contained in:
145
frontend/admin/src/lib/http/csrf.ts
Normal file
145
frontend/admin/src/lib/http/csrf.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* 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'
|
||||
|
||||
// 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 {
|
||||
// 使用原生 fetch 避免循环依赖
|
||||
const response = await fetch(buildUrl('/auth/csrf-token'), {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
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']
|
||||
Reference in New Issue
Block a user