每次新人入职,接手项目,联调接口,控制台里弹出那个红色的报错:
然后他做的第一件事是什么?
给后端发消息:哥,接口跨域了,帮忙加个 CORS 头。
后端回:好。
五分钟后,接口通了。两个人都没有深入想过这件事到底意味着什么。
在绝大多数前端的认知里,CORS 就是一个拦路虎——一个让开发变得麻烦的安全限制,只要后端配一下就能绕过去,配完就不用管了。
这个理解是错的,而且错得很危险!
CORS 不是在保护服务端。服务端不需要 CORS 来保护自己——任何人用 curl、Postman 或者一段后端代码就能随意调你的接口,CORS 根本拦不住。
CORS 保护的是浏览器里的用户。 它防止的是:你打开了一个恶意网页之后,这个网页偷偷用你的身份去调别的网站的接口。
这个区别搞不清楚,你就永远不会理解为什么 后端加个 Access-Control-Allow-Origin: * 就行了 这句话在某些场景下有多危险。
如果没有 CORS,会发生什么?
假设浏览器没有同源策略,也没有 CORS 机制。你打开了一个看起来很正常的网页 baidu.com。
你不知道的是,baidu.com 的页面里藏着一段 JavaScript:
// baidu.com 页面里的恶意代码
// 你刚刚登录过 bank.com,浏览器里存着 bank.com 的登录 Cookie
fetch('https://bank.com/api/transfer', {
method: 'POST',
credentials: 'include', // 带上 bank.com 的 Cookie
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
to: 'attacker-account',
amount: 50000,
}),
});
如果没有同源策略,这段代码会带着你的 bank.com 的登录 Cookie,向银行的转账接口发一个真实的请求。银行服务端收到的请求里有合法的 Cookie,它会认为这是你本人在操作——然后钱就转走了。
你什么都没做,只是打开了一个网页。
这就是浏览器的同源策略和 CORS 要防止的事情。它的安全模型很简单:
你在 A 网站的页面里,不能随便用用户的身份去调 B 网站的接口。除非 B 网站明确说我允许 A 来调。
所以限制的是浏览器里的行为,不是服务端。用 curl 直接调接口完全不受影响,因为 curl 不是浏览器,它不存在用户身份被偷用的风险。
为什么后端加个 * 就行了?
Access-Control-Allow-Origin: * 的意思是:任何网站都可以调这个接口。
对于公开的、不需要用户身份的接口,比如天气 API、汇率查询、公开的 CDN 资源,* 完全没问题。因为这些接口谁调都一样,不涉及用户隐私。
但对于需要用户身份的接口——登录、支付、个人信息、后台管理——* 会制造一个巨大的安全缺口。
浏览器有一条硬规则:Access-Control-Allow-Origin: * 和 credentials: include 不能同时使用。 如果接口返回了 *,浏览器会拒绝在请求里带上 Cookie。
这是浏览器帮你兜的底。
但很多后端为了方便,不用 *,而是把请求头里的 Origin 值原样反射回去:
# 危险的后端写法:把请求的 Origin 原样返回
@app.after_request
def add_cors(response):
origin = request.headers.get('Origin')
if origin:
response.headers['Access-Control-Allow-Origin'] = origin
response.headers['Access-Control-Allow-Credentials'] = 'true'
return response
这段代码的效果是:任何网站发来的请求,都被允许带着用户的 Cookie 访问接口。 它绕过了浏览器对 * 的限制,同时打开了和 * 一样大的安全漏洞——实际上比 * 更危险,因为它允许携带凭据。
上面那个银行转账的攻击场景,在这种配置下就完全可行了。
CORS 是浏览器在替你做安全决策
搞清楚了 保护谁 的问题之后,再来看 CORS 的机制就清晰了。
整个流程分两种情况👇
简单请求 - 浏览器先发后问
如果你的请求满足以下条件(方法是 GET / HEAD / POST,且没有自定义 Header,Content-Type 只能是 form-urlencoded / multipart/form-data / text/plain),浏览器会直接发出请求,但会检查响应头里有没有 Access-Control-Allow-Origin。
如果没有,或者值不匹配当前页面的域名,浏览器会把响应拦住,不让你的 JavaScript 读到。
注意:请求已经发出去了,服务端已经收到并处理了。 浏览器只是不让前端代码拿到响应。这就是为什么说 CORS 不保护服务端——请求已经到了服务端,该执行的都执行了。
预检请求 - 浏览器先问后发
如果你的请求不满足简单请求的条件——比如用了 PUT / DELETE,或者加了 Authorization 头,或者 Content-Type 是 application/json——浏览器会先发一个 OPTIONS 请求,问服务端我能不能这么干?
OPTIONS /api/users HTTP/1.1
Host: api.example.com
Origin: https://my-app.com
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: Authorization, Content-Type
服务端回答:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://my-app.com
Access-Control-Allow-Methods: GET, POST, DELETE
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 86400
只有服务端明确回答允许,浏览器才会发出真正的请求。否则直接拦截。
Access-Control-Max-Age: 86400 是一个很容易被忽略但很重要的字段——它告诉浏览器这个预检结果你可以缓存 24 小时。如果不设这个值,每一次非简单请求都会先发一个 OPTIONS,接口的请求量直接翻倍。
生产环境的正确配置是怎么样的?
说完原理,给一个能直接用的生产级 CORS 配置。以 Cloudflare Workers 为例:
// CORS 中间件:生产级白名单配置
const ALLOWED_ORIGINS = new Set([
'https://my-app.com',
'https://admin.my-app.com',
'https://staging.my-app.com',
]);
// 本地开发环境单独处理
function isAllowedOrigin(origin: string | null): boolean {
if (!origin) return false;
if (ALLOWED_ORIGINS.has(origin)) return true;
// 开发环境放行 localhost
if (origin.startsWith('http://localhost:')) return true;
return false;
}
function handleCORS(request: Request, response: Response): Response {
const origin = request.headers.get('Origin');
// 预检请求
if (request.method === 'OPTIONS') {
return new Response(null, {
status: 204,
headers: {
'Access-Control-Allow-Origin': isAllowedOrigin(origin) ? origin! : '',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Allow-Credentials': 'true',
'Access-Control-Max-Age': '86400', // 缓存预检结果 24 小时
},
});
}
// 正常请求:只在白名单内的 Origin 才设置 CORS 头
const headers = new Headers(response.headers);
if (isAllowedOrigin(origin)) {
headers.set('Access-Control-Allow-Origin', origin!);
headers.set('Access-Control-Allow-Credentials', 'true');
}
// 响应因 Origin 头而不同,必须分别缓存
headers.append('Vary', 'Origin');
return new Response(response.body, {
status: response.status,
headers,
});
}
几个容易犯的错误:
CORS 拦不住哪些攻击?
理解了 CORS 的保护机制之后,你还需要知道它的边界在哪里——CORS 不是万能的安全方案,它只防御跨域读取,不防御跨域写入。
<form> 提交不受 CORS 限制
CORS 只限制 XMLHttpRequest 和 fetch。原生的 <form> 表单提交完全不受同源策略限制:
<!-- 这段代码放在 baidu.com 上,用户打开后会自动提交 -->
<form action="https://bank.com/api/transfer" method="POST" id="attack-form">
<input type="hidden" name="to" value="attacker-account" />
<input type="hidden" name="amount" value="50000" />
</form>
<script>document.getElementById('attack-form').submit();</script>
这就是经典的 CSRF(跨站请求伪造)攻击。浏览器会带着 bank.com 的 Cookie 提交这个表单,服务端无法区分这是用户主动操作还是被恶意网页触发的。
CORS 不能防 CSRF。 防 CSRF 需要额外的机制👇:
// 服务端 CSRF 防御示例
function validateRequest(request: Request): boolean {
const origin = request.headers.get('Origin');
const referer = request.headers.get('Referer');
// 对于状态变更的请求(POST/PUT/DELETE),必须检查来源
if (['POST', 'PUT', 'DELETE'].includes(request.method)) {
const source = origin || (referer ? new URL(referer).origin : null);
if (!source || !ALLOWED_ORIGINS.has(source)) {
return false; // 拒绝来源不明的请求
}
}
return true;
}
<img> / <script> / <link> 也不受 CORS 限制
浏览器加载图片、脚本、样式表时,默认是跨域的,而且不受同源策略限制。这就是为什么你可以在自己的页面里引用别人 CDN 上的 jQuery。
但这也意味着:恶意网页可以通过图片标签(<img>)请求 https://your-api.com/api/user?callback=steal 的方式触发你的 GET 接口,带上用户的 Cookie。如果你的 GET 接口有副作用(比如已读标记、或者返回的数据被编码在 URL 的重定向里),就可能被利用。
所以 GET 接口永远不要做状态变更操作。 这不是约定,是安全红线。
一句话总结
CORS 不是在保护你的服务器——任何人用 curl 或后端代码都能无视它。
CORS 保护的是坐在浏览器前的那个用户。它防止的是:用户在不知情的情况下,被恶意网页利用身份去操作其他网站。
下次后端同事说 我加个 * 就行了 的时候,你应该追问一句:这个接口需要登录态吗?如果需要,* 不行,必须上白名单
这一句话的技术含量不高,但它背后的安全意识,值得每一个前端工程师记住🫡
好了,今天就分享到这里吧,喜欢请点赞收藏😃
喜欢我的文章,也欢迎关注我的微信公众号:【前端技术官】。
主要分享:前端架构 · AI 编程 · 职场认知 · 开发者成长
微信扫码关注 👆
不定期更新,不刷屏,聊点真正有用的干货。