- 修改 .env.example,优化玩家端配置说明,明确本地与线上环境的使用方式。 - 重构 API 代理逻辑,移除过时的开发代理文件,统一使用 Next.js 代理 API 请求。 - 更新 CSP 配置,简化连接来源说明,提升安全性。 - 调整 lottery-api-base.ts 中的注释,增强代码可读性。
138 lines
3.2 KiB
TypeScript
138 lines
3.2 KiB
TypeScript
/**
|
||
* 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));
|
||
|
||
function normalizeOrigin(value: string): string | null {
|
||
try {
|
||
return new URL(value).origin;
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
export function staticAllowedParentOrigins(): string[] {
|
||
return Array.from(
|
||
new Set(
|
||
ALLOWED_PARENT_ORIGINS
|
||
.map((origin) => normalizeOrigin(origin))
|
||
.filter((origin): origin is string => origin !== null),
|
||
),
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 生成 CSP 指令字符串
|
||
*/
|
||
export function generateCSP(extraParentOrigins: string[] = []): string {
|
||
const parentOrigins = Array.from(
|
||
new Set([
|
||
...staticAllowedParentOrigins(),
|
||
...extraParentOrigins
|
||
.map((origin) => normalizeOrigin(origin))
|
||
.filter((origin): origin is string => origin !== null),
|
||
]),
|
||
);
|
||
|
||
const directives: Record<string, string[]> = {
|
||
// 默认只允许同源
|
||
"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 由 Next 代理到 LOTTERY_API_UPSTREAM
|
||
"connect-src": [
|
||
"'self'",
|
||
// WebSocket 连接
|
||
"ws:",
|
||
"wss:",
|
||
].filter(Boolean),
|
||
|
||
// 媒体允许同源和 blob
|
||
"media-src": ["'self'", "blob:"],
|
||
|
||
// 对象不允许
|
||
"object-src": ["'none'"],
|
||
|
||
// 框架允许同源和指定父站
|
||
"frame-src": ["'self'", ...parentOrigins],
|
||
|
||
// 允许被嵌入到指定父站
|
||
"frame-ancestors": ["'self'", ...parentOrigins],
|
||
|
||
// 表单提交允许同源
|
||
"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 nonCspSecurityHeaders = [
|
||
{
|
||
key: "X-Content-Type-Options",
|
||
value: "nosniff",
|
||
},
|
||
{
|
||
key: "Referrer-Policy",
|
||
value: "strict-origin-when-cross-origin",
|
||
},
|
||
{
|
||
key: "Permissions-Policy",
|
||
value: "camera=(), microphone=(), geolocation=()",
|
||
},
|
||
];
|
||
|
||
export const securityHeaders = [
|
||
{
|
||
key: "Content-Security-Policy",
|
||
value: generateCSP(),
|
||
},
|
||
...nonCspSecurityHeaders,
|
||
];
|