Claude Code CORS 配置完整指南:安全处理跨源 API
用 Claude Code 安全配置 CORS:preflight、credentials、origin allowlist、测试命令与审查提示。
用 Claude Code 正确配置 CORS
前端跑在 localhost:3000,API 跑在 localhost:8787,浏览器就可能报 CORS 错误。很多人第一反应是加上 Access-Control-Allow-Origin: *,但只要 API 使用 Cookie、Authorization header 或后台管理页面,这个做法就会变得危险。
CORS(Cross-Origin Resource Sharing,跨源资源共享)是浏览器用来限制跨源读取响应的机制。origin 由 scheme、host、port 组成。https://app.example.com、https://api.example.com、http://localhost:3000、http://localhost:5173 都是不同 origin。
本文把 CORS 配置拆成 Claude Code 也容易审查的步骤。你会看到 Express、Fastify、Cloudflare Workers、Next.js Route Handler 的可复制代码,以及 preflight、credentials、origin allowlist、测试命令和 Claude Code 审查提示。
最重要的一点是:CORS 不是认证。它只控制浏览器中的 JavaScript 能不能读取跨源响应,不能阻止 curl、服务端请求或未授权用户。API 认证、授权、CSRF、防刷和安全响应头仍然要单独设计。
sequenceDiagram
participant Browser as Browser
participant API as API server
Browser->>API: OPTIONS /api/messages<br/>Origin + Access-Control-Request-*
API-->>Browser: 204 + Access-Control-Allow-*
Browser->>API: POST /api/messages<br/>Cookie or Authorization
API-->>Browser: 200 + Access-Control-Allow-Origin
写代码前先决定什么
让 Claude Code 生成代码前,先明确下面四件事。需求越含糊,越容易生成只适合 demo 的宽松配置。
| 项目 | 示例 | 注意点 |
|---|---|---|
| 允许的 origin | https://app.example.com, https://admin.example.com | 不包含路径,也不要末尾斜杠 |
| 是否发送凭证 | Cookie, Authorization header | Cookie 场景还要确认 SameSite=None; Secure |
| 允许的方法 | GET,POST,PUT,PATCH,DELETE,OPTIONS | 只开放实际使用的方法 |
| 允许的请求头 | Content-Type,Authorization,X-Request-ID | 必须与 preflight 请求匹配 |
preflight 是浏览器在真正请求前发出的权限确认。JSON POST、Authorization、PUT、DELETE 或自定义 header 通常会先触发 OPTIONS。如果响应中没有匹配的 Access-Control-Allow-Methods 和 Access-Control-Allow-Headers,浏览器不会发送真正的请求。
Express 配置
下面的例子假设使用 Node.js 20 或以上。Express 官方的 cors middleware 允许给 origin 传入函数,因此可以按请求检查 allowlist。因为这里支持 credentials,所以只反射允许的 origin,并启用 credentials: true。
npm init -y
npm install express cors
node server.mjs
// server.mjs
import express from "express";
import cors from "cors";
const app = express();
const allowedOrigins = new Set([
"https://app.example.com",
"https://admin.example.com",
"http://localhost:3000",
"http://localhost:5173",
]);
function isAllowedOrigin(origin) {
if (!origin) return true;
if (allowedOrigins.has(origin)) return true;
return process.env.NODE_ENV !== "production" && /^http:\/\/localhost:\d+$/.test(origin);
}
const corsOptions = {
origin(origin, callback) {
callback(null, isAllowedOrigin(origin));
},
credentials: true,
methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
allowedHeaders: ["Content-Type", "Authorization", "X-Request-ID"],
exposedHeaders: ["X-Request-ID"],
maxAge: 86400,
optionsSuccessStatus: 204,
};
app.use((req, res, next) => {
const origin = req.headers.origin;
if (origin && !isAllowedOrigin(origin)) {
return res.status(403).json({ error: "Origin not allowed" });
}
next();
});
app.use(cors(corsOptions));
app.use(express.json());
app.get("/api/health", (_req, res) => {
res.setHeader("X-Request-ID", crypto.randomUUID());
res.json({ ok: true });
});
app.post("/api/messages", (req, res) => {
res.setHeader("X-Request-ID", crypto.randomUUID());
res.json({ ok: true, received: req.body });
});
app.listen(8787, () => {
console.log("API listening on http://localhost:8787");
});
生产环境请设置 NODE_ENV=production,并且只在 allowedOrigins 中保留真实业务域名。没有 Origin header 的请求通常不是浏览器 CORS 请求,这里允许通过;但 API key、JWT 和用户权限仍然要由认证中间件检查。
Fastify 配置
Fastify 使用 @fastify/cors。官方 README 支持用布尔值、字符串、数组、正则或函数配置 origin,但 Set 完全匹配更容易审查。除非有强理由,不要用过宽的正则。
npm init -y
npm install fastify @fastify/cors
node server.mjs
// server.mjs
import Fastify from "fastify";
import cors from "@fastify/cors";
const app = Fastify({ logger: true });
const allowedOrigins = new Set([
"https://app.example.com",
"https://admin.example.com",
"http://localhost:3000",
"http://localhost:5173",
]);
function isAllowedOrigin(origin) {
if (!origin) return true;
if (allowedOrigins.has(origin)) return true;
return process.env.NODE_ENV !== "production" && /^http:\/\/localhost:\d+$/.test(origin);
}
app.addHook("onRequest", async (request, reply) => {
const origin = request.headers.origin;
if (origin && !isAllowedOrigin(origin)) {
return reply.code(403).send({ error: "Origin not allowed" });
}
});
await app.register(cors, {
origin(origin, callback) {
callback(null, isAllowedOrigin(origin));
},
credentials: true,
methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
allowedHeaders: ["Content-Type", "Authorization", "X-Request-ID"],
exposedHeaders: ["X-Request-ID"],
maxAge: 86400,
strictPreflight: true,
});
app.get("/api/health", async () => ({ ok: true }));
app.post("/api/messages", async (request) => {
return { ok: true, received: request.body };
});
await app.listen({ port: 8787, host: "0.0.0.0" });
Fastify 中插件和 hook 的顺序很关键。如果认证 hook 在 CORS 插件之前拒绝 OPTIONS,浏览器就不会发送真正请求。让 Claude Code 审查时,要明确要求它检查插件注册顺序。
Cloudflare Workers 配置
Cloudflare Workers 直接使用标准 Fetch API。需要显式处理 OPTIONS,并在成功和错误响应中都返回 CORS header。响应会因 origin 不同而变化时,加上 Vary: Origin。
// src/index.ts
const allowedOrigins = new Set([
"https://app.example.com",
"https://admin.example.com",
"http://localhost:3000",
]);
function getCorsHeaders(request: Request): HeadersInit | null {
const origin = request.headers.get("Origin");
if (!origin) return {};
if (!allowedOrigins.has(origin)) return null;
return {
"Access-Control-Allow-Origin": origin,
"Access-Control-Allow-Credentials": "true",
"Access-Control-Allow-Methods": "GET,POST,OPTIONS",
"Access-Control-Allow-Headers": "Content-Type,Authorization,X-Request-ID",
"Access-Control-Max-Age": "86400",
"Vary": "Origin",
};
}
export default {
async fetch(request: Request): Promise<Response> {
const corsHeaders = getCorsHeaders(request);
if (corsHeaders === null) {
return Response.json({ error: "Origin not allowed" }, { status: 403 });
}
if (request.method === "OPTIONS") {
return new Response(null, { status: 204, headers: corsHeaders });
}
const url = new URL(request.url);
if (url.pathname === "/api/messages" && request.method === "POST") {
const body = await request.json().catch(() => ({}));
return Response.json({ ok: true, received: body }, { headers: corsHeaders });
}
return Response.json({ error: "Not found" }, { status: 404, headers: corsHeaders });
},
};
Workers 最常见的错误是只给成功响应加 header。OPTIONS、401、403、500 忘记加 CORS header 时,DevTools 可能只显示 CORS 失败,而真正的应用错误被隐藏。
Next.js Route Handler 配置
App Router 中的 app/api/.../route.ts 使用 Web 标准的 Request 和 Response。Next.js 文档展示了如何给响应添加 CORS header;如果 API 使用 credentials,就不要用 *,而要用 allowlist。
// app/api/messages/route.ts
const allowedOrigins = new Set([
"https://app.example.com",
"https://admin.example.com",
"http://localhost:3000",
]);
function getCorsHeaders(request: Request): HeadersInit | null {
const origin = request.headers.get("Origin");
if (!origin) return {};
if (!allowedOrigins.has(origin)) return null;
return {
"Access-Control-Allow-Origin": origin,
"Access-Control-Allow-Credentials": "true",
"Access-Control-Allow-Methods": "POST,OPTIONS",
"Access-Control-Allow-Headers": "Content-Type,Authorization,X-Request-ID",
"Access-Control-Max-Age": "86400",
"Vary": "Origin",
};
}
export async function OPTIONS(request: Request) {
const headers = getCorsHeaders(request);
if (headers === null) {
return Response.json({ error: "Origin not allowed" }, { status: 403 });
}
return new Response(null, { status: 204, headers });
}
export async function POST(request: Request) {
const headers = getCorsHeaders(request);
if (headers === null) {
return Response.json({ error: "Origin not allowed" }, { status: 403 });
}
const body = await request.json().catch(() => ({}));
return Response.json({ ok: true, received: body }, { headers });
}
next.config.js 的 headers() 适合静态、公开的 API header。需要按 origin 判断的认证 API,更推荐在 Route Handler 内处理,审查起来也更直接。
测试命令
先用 curl 把 preflight 和真正请求分开验证。重点看 Access-Control-Allow-Origin 是否与请求的 Origin 完全一致,以及 Access-Control-Allow-Credentials: true 是否只对允许的 origin 返回。
curl -i -X OPTIONS http://localhost:8787/api/messages \
-H "Origin: http://localhost:3000" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: Content-Type, Authorization"
curl -i -X POST http://localhost:8787/api/messages \
-H "Origin: http://localhost:3000" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer dev-token" \
--data '{"text":"hello"}'
curl -i -X OPTIONS http://localhost:8787/api/messages \
-H "Origin: https://evil.example" \
-H "Access-Control-Request-Method: POST"
浏览器侧可以这样检查 credentials。只要使用 credentials: "include",通配符 CORS 响应就会被浏览器拒绝。
await fetch("http://localhost:8787/api/messages", {
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer dev-token",
},
body: JSON.stringify({ text: "hello" }),
});
实际用例
第一个用例是 SPA 与 API 分域部署。React 在 https://app.example.com,API 在 https://api.example.com 时,需要明确 allowlist。如果使用登录 Cookie,要同时检查 credentials、Cookie 属性和 CSRF。
第二个用例是后台管理页面。可以把 https://admin.example.com 加入 allowlist,但不要把 CORS 当成管理员权限判断。权限仍然必须在 API 代码中实现。
第三个用例是把 Cloudflare Worker 当作 BFF 或轻量代理。浏览器访问 Worker,Worker 再访问上游 API。最终返回给浏览器的 Worker 响应仍然需要正确的 CORS header。
第四个用例是公开只读 API。如果没有 Cookie、没有 Authorization、没有私有数据,Access-Control-Allow-Origin: * 可以接受。若未来可能加入认证,从一开始就使用 allowlist 更稳。
具体踩坑点
| 踩坑点 | 结果 | 修正 |
|---|---|---|
* 与 credentials: true 同时使用 | 浏览器阻止响应 | 返回明确的 origin |
注册 https://app.example.com/ | 末尾斜杠导致不匹配 | 只保存 https://app.example.com |
只允许 localhost | 端口不同会失败 | 写成 http://localhost:3000 |
OPTIONS 也要求认证 | preflight 在 401/403 停住 | 在认证前处理 preflight |
| 错误响应没有 CORS | DevTools 看不到真正错误 | 4xx/5xx 也加 CORS header |
| CDN 缓存了按 origin 变化的 header | 不同 origin header 混用 | 加 Vary: Origin |
| 把 CORS 当授权 | 非浏览器客户端仍可调用 | 单独实现认证、授权和 CSRF |
MDN 明确说明,带 credentials 的 CORS 请求不能使用 Access-Control-Allow-Origin: *。如果 Claude Code 生成了这个组合,就应视为需要修正的安全问题。
Claude Code 审查提示
Review this repository's CORS configuration.
Check:
- No Access-Control-Allow-Origin: * when credentials are enabled
- Allowlist uses exact scheme/host/port matching
- OPTIONS preflight runs before authentication middleware
- 4xx/5xx responses include the required CORS headers
- Vary: Origin is present when responses vary by origin
If changes are needed, propose the smallest safe diff.
Diagnose this CORS error by cause.
Browser error:
<paste the full DevTools Console message>
curl preflight:
<paste curl -i -X OPTIONS output>
Expected origin:
https://app.example.com
Read the relevant API files and return reproduction steps, root cause, fix, and tests.
Review the Express/Fastify/Next.js/Workers CORS implementation as a security reviewer.
Focus on:
- Whether request origins are blindly reflected
- Whether localhost remains enabled in production
- Whether Authorization is allowed without proper authorization checks
- Whether cookie flows mention SameSite=None; Secure and CSRF protection
- Whether test commands separate preflight and the real request
Group findings as Critical, Must fix, and Improvement.
官方资料与站内链接
规格层面建议以 MDN CORS 指南 为基准。实现层面可参考 Express cors middleware、@fastify/cors、Cloudflare Workers CORS 示例 和 Next.js Route Handlers。Claude Code 的复用工作流可看 Claude Code commands。
建议搭配阅读 API 开发指南、Web 安全响应头、Cloudflare Workers 指南 和 代码审查流程清单。
下一步
把示例域名替换成自己的域名后,用上面的审查提示跑一遍仓库,再用 Claude Code 安全最佳实践 一起检查 Cookie、CSRF、授权和安全响应头。如果你在做客户项目或内部平台,这份清单可以直接变成评审资料,也方便后续引入付费实施支持或复用模板。
实际试用结果
Masa 在本地把 Express 和 Fastify 示例跑在 localhost:8787,确认 Origin: http://localhost:3000 的 preflight 与 POST 成功,而 https://evil.example 返回 403。最容易漏掉的是错误响应的 CORS header,以及 Workers 中对 OPTIONS 的显式处理。最稳定的流程是先实现 allowlist,再跑 curl 检查,最后让 Claude Code 确认没有 * 加 credentials、需要的地方有 Vary: Origin,并且 localhost 不会进入生产环境。
免费 PDF: Claude Code 速查表
输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。
我们会妥善保护你的信息,不发送垃圾邮件。
把 Claude Code 变成真正能带来结果的工作流
先领取中文说明的免费 PDF,再进入英文商品页选择合适的教材。如果你需要团队落地、流程设计或内容变现支持,也可以直接咨询。
关于作者
Masa
专注 Claude Code 实务流程、团队导入和内容转化的工程师。
相关文章
从Obsidian到CLAUDE.md的Claude Code流程:不再反复解释上下文
把 Obsidian 工作笔记整理成 CLAUDE.md 运行说明,让 Claude Code 每次都带着正确上下文开始。
Claude Code 收入 CTA 路由:从文章分流到 PDF、Gumroad 与咨询
用 Claude Code 按读者意图把文章流量分到免费 PDF、Gumroad 教材或咨询入口。
Claude Code 团队交接规则: 把审查证据、权限、回滚和收入路径一起交付
面向团队的 Claude Code 交接格式: 证据、权限、回滚、免费 PDF、Gumroad 与咨询路径都要可审查。