/** * Content Security Policy (CSP) 配置 * * 支持 iframe 嵌入场景,允许主站加载彩票系统 */ // 允许的主站来源 const ALLOWED_PARENT_ORIGINS: string[] = [ process.env.NEXT_PUBLIC_MAIN_SITE_URL, process.env.NEXT_PUBLIC_PARENT_ORIGIN, // 开发环境 "http://localhost:5173", "http://127.0.0.1:5173", "http://192.168.0.101:5173", "http://localhost:3801", "http://127.0.0.1:3801", "http://192.168.0.101:3801", // 生产环境应从环境变量读取 ].filter((o): o is string => Boolean(o)); /** * 生成 CSP 指令字符串 */ export function generateCSP(): string { const directives: Record = { // 默认只允许同源 "default-src": ["'self'"], // 脚本允许同源和内联(Next.js 需要) "script-src": ["'self'", "'unsafe-inline'", "'unsafe-eval'"], // 样式允许同源和内联 "style-src": ["'self'", "'unsafe-inline'"], // 图片允许同源、data URL 和 blob "img-src": ["'self'", "data:", "blob:"], // 字体允许同源 "font-src": ["'self'"], // 连接允许同源和 API 域名 "connect-src": [ "'self'", process.env.NEXT_PUBLIC_API_URL || "", // WebSocket 连接 "ws:", "wss:", ].filter(Boolean), // 媒体允许同源和 blob "media-src": ["'self'", "blob:"], // 对象不允许 "object-src": ["'none'"], // 框架允许同源和指定父站 "frame-src": ["'self'", ...ALLOWED_PARENT_ORIGINS], // 允许被嵌入到指定父站 "frame-ancestors": ["'self'", ...ALLOWED_PARENT_ORIGINS], // 表单提交允许同源 "form-action": ["'self'"], }; // 构建 CSP 字符串 return Object.entries(directives) .map(([key, values]) => { if (values.length === 0) return key; return `${key} ${values.join(" ")}`; }) .join("; "); } /** * 检测是否允许被 iframe 嵌入 * @param parentOrigin 父窗口来源 */ export function isAllowedParent(parentOrigin: string): boolean { if (ALLOWED_PARENT_ORIGINS.length === 0) return true; // 未配置时允许所有 return ALLOWED_PARENT_ORIGINS.some( (origin) => origin && parentOrigin.startsWith(origin), ); } /** * 安全头配置(用于 next.config.ts) */ export const securityHeaders = [ { key: "Content-Security-Policy", value: generateCSP(), }, { key: "X-Content-Type-Options", value: "nosniff", }, { key: "Referrer-Policy", value: "strict-origin-when-cross-origin", }, { key: "Permissions-Policy", value: "camera=(), microphone=(), geolocation=()", }, ];