从“跨不过的墙”到数据自由流通:一文读懂跨域请求的本质与解决方案

247 阅读7分钟

一、问题:浏览器为什么拦了我的请求?

你有没有遇到过这样的情况?

你在本地开发一个网页,地址是 http://localhost:5173,想从 http://localhost:8080 这个地址获取一些数据。你写好了代码,点击运行,结果浏览器控制台跳出一行红色警告:

Access to fetch at 'http://localhost:8080/api/hello' from origin 'http://localhost:5173' has been blocked by CORS policy

image.png 翻译过来就是:“你不能从这个域名访问那个域名的数据,这是规定。”

这堵“墙”就是 同源策略(Same-Origin Policy)

二、什么是“同源”?为什么需要?

“同源” = 协议 + 域名 + 端口 都相同。

比如:

URL是否同源
http://localhost:5173✅ 同源
http://localhost:8000❌ 不同源(端口不同)
https://localhost:5173❌ 不同源(协议不同)
http://127.0.0.1:5173❌ 不同源(IP 虽等价,但字符串不同)

这就像你住在 A 小区,却想进 B 小区翻别人家的抽屉——门卫(浏览器)必须拦你。

确保一个网站的脚本只能访问自身域名下的资源,防止恶意网站通过脚本读取或篡改其他网站的数据,如用户会话信息或敏感数据,从而保护用户隐私和数据安全,避免跨站脚本攻击(XSS)等安全隐患。这一策略是浏览器安全的基石。

但问题是:现代 Web 应用,前端和后端本来就在不同服务器上啊!

比如:

  • 前端:https://www.myapp.com(用户访问)
  • 后端 API:https://api.myapp.com 或 http://localhost:8000(处理数据)

如果完全不能跨域,那网页岂不是“断网”了?

于是,浏览器说:“我可以让你跨,但必须双方都同意。”

这个“同意机制”,就是 CORS(Cross-Origin Resource Sharing,跨域资源共享)

三、第一代解决方案:JSONP——用“脚本”绕过检查

1. 核心思路:利用 <script> 标签不受同源策略限制

虽然浏览器禁止 fetchXMLHttpRequest 跨域请求数据,但它允许你加载外部的 JavaScript 脚本。比如你可以:

<script src="https://cdn.jsdelivr.net/npm/vue@3"></script>

这个脚本来自另一个域名,但浏览器允许加载。

小记:script脚本,Link,img都可以跨域

能不能利用这一点来“偷渡”数据呢?

答案是:能!这就是 JSONP 的诞生。

2. JSONP 是怎么工作的?

一个生活化的比喻

想象你和朋友约定好一种“暗号通信”方式:

  • 你写一封信,信封上写:

    “请把‘牛奶已送到’这句话,用‘收到货(……)’的方式告诉我。”

  • 朋友收到信后,按约定写回:

    收到货("牛奶已送到")

你一看到这行字,就知道货到了。

在技术世界里:

  • 你 = 前端
  • 朋友 = 后端
  • 信 = <script> 请求
  • “收到货” = 回调函数名(callback)
  • “牛奶已送到” = 真实数据

3. 代码实战:前后端如何配合

前端代码(发“信”)

// 创建一个叫 'show' 的全局函数
window.show = function(data) {
  console.log("服务器说:", data);
};

// 动态创建一个 script 标签
const script = document.createElement('script');
script.src = 'http://localhost:5173/say?wd=ilikeyou&callback=biaobaiCallback';
document.body.appendChild(script);

这相当于说:“请把数据用 biaobaiCallback(...) 包着发给我。”

后端代码(回“信”)

const http = require('http');
const server = http.createServer((req, res) => {
  // 匹配 GET 请求 /say
  if (req.url.startsWith('/say')) {
    const url = new URL(req.url, `http://${req.headers.host}`);
    const wd = url.searchParams.get('wd');
    const callback = url.searchParams.get('callback');
    
    console.log(url); 
    console.log(wd);      // Iloveyou
    console.log(callback); // show

    // 返回 JSONP 格式响应
    res.writeHead(200, { 'Content-Type': 'application/javascript' });
    res.end(`${callback}('我不爱你')`);
  } else {
    res.writeHead(404);
    res.end('Not Found');
  }
});

server.listen(3000, () => {
  console.log('Server running at http://localhost:3000');
});

打印结果: image.png 后端收到请求后,返回的不是普通数据,而是一段JavaScript 代码

show({"data": "我不爱你"})

浏览器一执行这段代码,就会调用你定义的 show 函数,数据就到手了!

API解析:
  • new URL(relative, base) 是 ES6+ 提供的构造 URL 对象的方法。
  • req.url 是相对路径(如 /say?wd=hello)。
  • http://${req.headers.host} 是基础 URL(例如:http://example.com),req.headers.host 包含了客户端请求的主机名和端口(如 localhost:3000)。
  • 这行代码将相对路径和基础 URL 组合,生成一个完整的、可解析的 URL 对象。
  • 有了这个对象,就可以方便地提取查询参数(query parameters)。

举个例子:如果 req.url/say?wd=你好&callback=cb,且 req.headers.hostlocalhost:8080,那么 new URL(req.url, 'http://localhost:8080') 会生成一个表示 http://localhost:8080/say?wd=你好&callback=cb 的 URL 对象。


  • url.searchParams 是一个 URLSearchParams 对象,用于操作 URL 中的查询字符串(即 ? 后面的部分)。
  • .get('wd') 方法用于获取查询参数中键为 wd 的值。
  • 例如,如果 URL 是 /say?wd=hello,那么 wd 的值就是 "hello"
  • 如果没有 wd 参数,则返回 null

4. JSONP 的局限性

  • 优点:简单,兼容老浏览器

  • 缺点:

    • 只能发 GET 请求(因为 <script> 只能 GET)
    • 不安全,容易被黑客攻击(如果 callback 被恶意注入)
    • 需要后端配合,后端的输出的方式要加padding

四、现代解决方案:CORS——官方颁发的“通行证”

随着 Web 发展,W3C 推出了 CORS(Cross-Origin Resource Sharing) ,也就是“跨域资源共享”。

它的核心思想是:让服务器主动告诉浏览器:“我允许你跨域访问我。”

1. 工作原理

只要后端在响应中加上这行头:

Access-Control-Allow-Origin:  http://localhost:5173

或者允许所有来源:

Access-Control-Allow-Origin: *

浏览器看到这个头,就知道:“哦,服务器允许,那我可以把数据给你了。”

2. CORS 的两种请求:简单请求 vs 复杂请求

CORS 并不是一刀切的。它根据请求的“复杂程度”,分为两类:

1. 简单请求

满足以下所有条件,就是“简单请求”:

  • 方法GETPOSTHEAD

  • Content-Type

    • text/plain
    • multipart/form-data
    • application/x-www-form-urlencoded
  • 不能自定义其他特殊头(如 AuthorizationX-Token

👉 这种请求,浏览器直接发,不打招呼。

示例:GET 请求
fetch('http://localhost:8000/api/test') // GET 请求

后端只需设置:

res.setHeader('Access-Control-Allow-Origin', '*')
res.end(JSON.stringify({ msg: '跨域成功!' }))

浏览器收到响应后,发现有 Access-Control-Allow-Origin 头,就放行数据。

2. 复杂请求(Preflight Request)

只要不符合“简单请求”的条件,就是“复杂请求”。

比如:

  • 使用 PUTPATCHDELETE 方法
  • Content-Type 是 application/json
  • 添加了自定义请求头(如 Authorization: Bearer xxx

👉 这种请求,浏览器会先发一个预检请求(OPTIONS) ,问服务器:“我待会要这么干,你同不同意?”

image.png

只有服务器说“同意”,浏览器才会发送真正的请求。

实例:以 PATCH 为例
1. 前端代码(发起复杂请求)
fetch('http://localhost:8000/api/test', {
  method: 'PATCH', // 不是 GET/POST,属于复杂请求
})
.then(res => res.json())
.then(data => console.log(data))
2. 浏览器自动行为:先发 OPTIONS 预检

浏览器会自动先发送一个 OPTIONS 请求:

OPTIONS /api/test
Origin: http://127.0.0.1:5500
Access-Control-Request-Method: PATCH
3. 后端必须响应预检
if (req.method === 'OPTIONS') {
  // 设置允许的来源
  res.setHeader('Access-Control-Allow-Origin', 'http://127.0.0.1:5500')
  // 设置允许的方法
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PATCH, PUT, DELETE, OPTIONS')
  // 可选:允许的头
  // res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization')

  res.writeHead(200) // 返回 200,表示“同意”
  res.end()
  return
}
4. 浏览器收到预检响应后,才发真正的 PATCH 请求
PATCH /api/test
Origin: http://127.0.0.1:5500
5. 后端处理真正请求
if (req.url === '/api/test' && req.method === 'PATCH') {
  res.setHeader('Access-Control-Allow-Origin', 'http://127.0.0.1:5500')
  res.writeHead(200, { 'Content-Type': 'application/json' })
  res.end(JSON.stringify({ msg: '跨域成功!!!' }))
}

image.png

6.完整后端代码示例
const http = require('http')

const server = http.createServer((req, res) => {
  // 所有请求都是跨域请求,所有响应都带上 CORS 头
  res.setHeader('Access-Control-Allow-Origin', 'http://127.0.0.1:5500')
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PATCH, PUT, DELETE, OPTIONS')

  // 预检请求(OPTIONS)
  if (req.method === 'OPTIONS') {
    res.writeHead(200)
    res.end()
    return
  }

  // 真正的请求
  if (req.url === '/api/test' && req.method === 'PATCH') {
    res.writeHead(200, { 'Content-Type': 'application/json' })
    res.end(JSON.stringify({ msg: '跨域成功!!!' }))
  } else {
    res.writeHead(404)
    res.end('Not Found')
  }
})

server.listen(8000, () => {
  console.log('CORS Server running at http://localhost:8000')
})

五、CORS vs JSONP:时代的选择

对比项CORSJSONP
支持方法所有(GET/POST/PUT/DELETE)仅 GET
安全性高(标准头控制)低(易 XSS)
错误处理清晰(可捕获网络错误)困难(script load error)
现代性✅ 主流方案❌ 仅兼容老浏览器
使用方式原生 fetch / axios手动创建 script 标签

💡 结论:除非要兼容 IE8/9,否则一律使用 CORS。

六、总结:跨域的本质是信任问题

跨域问题的本质,不是技术难题,而是安全与信任的平衡

  • 浏览器出于安全,默认禁止跨域请求(防止恶意网站窃取你的银行数据)。
  • 但现代应用又需要跨域通信。
  • 所以我们通过 JSONP(老方法)  或 CORS(新标准)  来建立“信任链”。

🔑 记住一句话
跨域不是“不能通信”,而是“需要双方同意才能通信”。