同源策略与跨域:CORS 全解

本节目标

同源策略(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 是服务器递出的「访客通行证」——你只能申请,不能自己造。