同源策略与跨域:CORS 全解
本节目标
- 理解「同源策略」到底在防什么
- 用一个可运行的 Node 小服务器,实测「没配 CORS 头 vs 配了」的请求差异
- 搞清楚「预检(Preflight)」什么时候发生
同源策略(SOP,Same-Origin Policy):协议 + 域名 + 端口三者相同才算「同源」。不同源时,浏览器禁止前端 JS 读取跨域响应(注意:请求其实可能发出了,只是结果被拦),且默认不带跨域 cookie。
CORS(跨域资源共享):服务器通过响应头声明「允许谁访问」,浏览器据此放行。
简单请求(GET/POST + 简单头,且非自定义 Content-Type):
响应头:
Access-Control-Allow-Origin: https://a.com (或 * 表示任意)
Access-Control-Allow-Credentials: true (需要带 cookie 时)预检请求(Preflight):非简单请求(如 PUT/DELETE、application/json、自定义头)会先发一个 OPTIONS 探测:
请求(OPTIONS):
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: content-type
响应:
Access-Control-Allow-Methods: PUT, DELETE
Access-Control-Allow-Headers: content-type
Access-Control-Max-Age: 86400 (预检结果缓存一天)下面这段 Node 代码真实演示 CORS 的三种情况:同源成功、跨域配了 Access-Control-Allow-Origin: *(放行)、跨域没配 CORS(被浏览器拦截)。用浏览器打开页面即可见差异(Node 端只负责「服务器行为」,拦截由浏览器执行):
// 运行环境:Node.js 14+(仅用内置 http)
// 步骤:node brw-l12-server.js 启动后,用浏览器打开 http://localhost:3000/ 看控制台
// 你会看到三种结果:① 同源成功;② 跨域+配了 CORS 成功;③ 跨域+没配 CORS 被浏览器拦截。
const http = require('node:http')
// 服务器 3000:返回一个含「跨域请求测试」的页面,并给自身接口配好 CORS
http.createServer((req, res) => {
if (req.url === '/api/same') {
res.writeHead(200, { 'Access-Control-Allow-Origin': '*' })
return res.end('来自同源 3000 的接口')
}
// 返回测试页
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
res.end(`<h3>打开控制台看 CORS 效果</h3>
<script>
// ① 同源请求:成功(同源策略不限制自己)
fetch('/api/same').then(r=>r.text()).then(t=>console.log('同源 OK:', t))
// ② 跨域到 4000,但该接口配了 CORS 头:成功(服务器明确放行)
fetch('http://localhost:4000/api/ok').then(r=>r.text())
.then(t=>console.log('跨域+CORS OK:', t))
.catch(e=>console.log('跨域+CORS 失败(意外):', e.message))
// ③ 跨域到 4000,该接口没配 CORS 头:被浏览器拦截
fetch('http://localhost:4000/api/other').then(r=>r.text())
.then(t=>console.log('跨域无CORS OK:', t))
.catch(e=>console.log('跨域无CORS 被拦截(预期):', e.message))
</script>`)
}).listen(3000, () => console.log('页面+同源接口: http://localhost:3000'))
// 服务器 4000:模拟「另一个源」,两个接口一个配 CORS、一个没配
http.createServer((req, res) => {
if (req.url === '/api/ok') {
// ✅ 配了 CORS 头:跨域也放行
res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8', 'Access-Control-Allow-Origin': '*' })
return res.end('来自 4000 的接口(配了 CORS,跨域也成功)')
}
// ❌ 没配 CORS 头:跨域会被浏览器拦截
res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' })
res.end('来自 4000 的接口(但没配 CORS)')
}).listen(4000, () => console.log('跨域接口: http://localhost:4000(/api/ok 配了CORS,/api/other 没配)'))带 cookie 的注意点:credentials: 'include' 时,Allow-Origin 不能是 *,必须明确写域名,且服务器要开 Access-Control-Allow-Credentials: true。
其他跨域方案:JSONP(<script> 不受同源限制,只支持 GET,已过时)、反向代理(开发期 /api → 后端,最省心)、postMessage(iframe 跨窗口)、document.domain(同主域子域旧方案,边缘化)。
排查口诀:看到 CORS 报错,先看「是不是预检没过(OPTIONS 404/缺响应头)」,再看 Allow-Origin 是否匹配、credentials 是否冲突。
名词解释
同源策略(SOP):浏览器的安全基线,规定「不同源(协议/域名/端口任一不同)的页面,不能互相读取数据」。它的「能力」是:拦截跨源读取响应、限制跨源写 cookie。你可以把它想成「隔壁小区的人不能进你家翻抽屉」——但能站在门口喊话(请求可能发出)。它防的是「恶意网站偷读你已登录的别的网站数据」。
CORS(跨域资源共享,Cross-Origin Resource Sharing):服务器用响应头主动声明「允许哪些源访问我」。方法/响应头:Access-Control-Allow-Origin(允许谁)、Access-Control-Allow-Methods(允许哪些方法)、Access-Control-Allow-Headers(允许哪些请求头)、Access-Control-Allow-Credentials(是否允许带 cookie)。它把「是否放行」的决定权交还给服务器。
预检(Preflight):浏览器在发「非简单跨域请求」前,先自动发一个 OPTIONS 探测,问服务器「我打算用 PUT + 这个头,你允许吗」。方法:服务器回 Access-Control-Allow-Methods/Headers 表示同意,Access-Control-Max-Age 表示「这次探测结果我缓存多久,期间别再问」。它是 CORS 的「先问后做」安全机制。
课后练习
练习 1:前端用 fetch('https://api.b.com/data', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({a:1}) }) 跨域请求,为什么浏览器会先发一个 OPTIONS?
答案:因为
Content-Type: application/json不属于「简单请求头」(简单头只限text/plain、application/x-www-form-urlencoded、multipart/form-data等),于是该请求被判定为「非简单请求」,浏览器必须先发OPTIONS预检,确认服务器允许application/json和POST后,才发真正的 POST。
练习 2:前端带 credentials: 'include' 跨域,服务器返回 Access-Control-Allow-Origin: *,能拿到响应吗?为什么?
答案:不能。带凭据(cookie)的跨域请求,浏览器要求
Access-Control-Allow-Origin必须是具体的源(不能是通配符*),同时服务器还要返回Access-Control-Allow-Credentials: true。用*会被浏览器直接拒绝,报 CORS 错误。这是为了防止「任意网站都能带着用户的 cookie 去访问目标接口」。
总结
跨域是前端新手最易卡住、也最容易被「前端背锅」的话题。我想澄清一个关键事实:CORS 不是前端能「解决」的问题,而是后端该「配置」的响应头。浏览器之所以拦你,恰恰是因为它在忠实地执行同源策略——这层保护挡住了「恶意网站偷读你已登录的银行数据」这类攻击。所以当你遇到 CORS 报错,正确动作是「找后端加响应头」,而不是在前端用什么奇技淫巧绕过(那往往意味着你在破坏安全模型)。唯一前端能独立搞定的跨域是「开发期反向代理」。记住这条边界:同源策略是浏览器给用户的铠甲,CORS 是服务器递出的「访客通行证」——你只能申请,不能自己造。