跨域是什么,如何解决?
解释了浏览器“同源策略”导致的跨域问题,并介绍了CORS(标准方案)和代理服务器两种主流解决方法。
这是一个关于“跨域”非常经典和重要的问题。我会用一个清晰、易于理解的方式来解释它,并提供详细的解决方案。
核心摘要
- 是什么? 跨域(Cross-Origin)是指浏览器出于安全原因,限制一个网页去请求另一个不同“源”的资源。这个限制被称为“同源策略”(Same-Origin Policy)。
- 为什么有这个限制? 为了保护你的信息安全。如果没有这个策略,你在A网站登录后,B网站的恶意脚本就能轻易地读取你在A网站的个人信息,造成严重的安全漏洞。
- 怎么解决? 最主流、最标准的解决方案是 CORS(跨域资源共享)。其他方案包括 JSONP(已过时,不推荐)、代理服务器(非常实用)等。
第一部分:跨域是什么?
1. 理解“源”(Origin)
首先,我们需要理解什么是“源”。一个“源”由三个部分组成:
- 协议(Protocol):例如
http或https - 域名(Domain):例如
www.a.com或api.b.com - 端口(Port):例如
:80或:3000
只有这三者完全相同时,才属于“同源”。
举个例子:
假设我们的网页地址是 http://www.a.com/page.html
| 请求的URL | 是否同源? | 原因 |
|---|---|---|
http://www.a.com/other.html |
是 | 协议、域名、端口都相同 |
https://www.a.com/page.html |
否 | 协议不同 (http vs https) |
http://api.a.com/data.json |
否 | 域名不同 (子域名不同) |
http://www.b.com/page.html |
否 | 域名不同 |
http://www.a.com:8080/page.html |
否 | 端口不同 (默认80 vs 8080) |
2. 什么是“同源策略”(Same-Origin Policy)?
同源策略是浏览器的一个核心安全功能。它规定:
一个源的文档或脚本,不能与另一个源的资源进行交互。
这个策略主要限制了以下几种行为:
- 无法读取或修改另一个源的 DOM。
- 无法读取另一个源的 Cookie、LocalStorage 和 IndexedDB。
- 无法发送 AJAX 请求到另一个源(这正是我们通常所说的“跨域问题”)。
需要注意的是:
- 这个策略是由浏览器强制执行的。请求实际上已经发送到服务器了,但是浏览器在接收到响应后,会检查响应头。如果发现源不同且服务器没有明确许可,浏览器就会拦截这个响应,并在控制台报错。
- 像
<script>,<img>,<iframe>,<link>这些标签不受同源策略限制,它们可以加载跨域资源。这也是 JSONP 方案能够存在的基础。
第二部分:如何解决跨域问题?
以下是几种主流的解决方案,按照推荐顺序排列。
方案一:CORS (Cross-Origin Resource Sharing) - 跨域资源共享【标准方案】
这是 W3C 标准,也是目前解决跨域问题最主流、最优雅的方法。
核心思想:
服务器通过在响应头(Response Header)中添加一些特殊的 Access-Control-* 字段,来告诉浏览器,我允许你这个源来访问我的资源。浏览器看到这些头信息后,就不会拦截响应。
实现方式:
修改服务器端代码,添加相应的响应头。
1. 简单请求 (Simple Request)
当请求满足以下所有条件时,它被视为“简单请求”:
- 请求方法是
GET、POST或HEAD之一。 - HTTP 头信息不超出
Accept,Accept-Language,Content-Language,Content-Type等几个字段。 Content-Type的值仅限于text/plain,multipart/form-data,application/x-www-form-urlencoded。
对于简单请求,服务器只需要在响应头中添加一个字段:
Access-Control-Allow-Origin: http://www.a.com
或者,允许任何源访问(不推荐在生产环境中使用,尤其是在需要凭证的情况下):
Access-Control-Allow-Origin: *
2. 预检请求 (Preflight Request)
不满足“简单请求”条件的请求,就是“非简单请求”。例如,请求方法是 PUT、DELETE,或者 Content-Type 是 application/json。
对于这类请求,浏览器会先自动发送一个 OPTIONS 方法的“预检”请求到服务器,询问服务器是否允许接下来的实际请求。
预检请求的响应头需要包含更多信息:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: http://www.a.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400 // 预检请求的缓存时间(秒)
如果预检通过,浏览器才会发送真正的请求。
后端代码示例 (Node.js + Express):
可以使用 cors 中间件轻松实现。
npm install cors
const express = require('express');
const cors = require('cors');
const app = express();
// 简单用法:允许所有源
// app.use(cors());
// 精细化配置
app.use(cors({
origin: 'http://www.a.com', // 只允许这个源访问
methods: ['GET', 'POST', 'PUT'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true // 如果需要携带 cookie,这个必须是 true
}));
app.get('/data', (req, res) => {
res.json({ message: 'Hello from the API!' });
});
app.listen(3000, () => console.log('Server running on port 3000'));
方案二:代理服务器 (Proxy) 【非常实用】
当你无法控制目标服务器(例如,请求一个第三方的 API)时,代理是最佳选择。
核心思想:
利用“服务器之间通信没有同源策略限制”这一特点。
- 前端(例如
http://www.a.com)不直接请求目标服务器(http://api.b.com)。 - 前端请求自己的同源服务器(
http://www.a.com/api)。 - 自己的服务器收到请求后,再由它作为“代理”,去请求真正的目标服务器(
http://api.b.com)。 - 服务器拿到数据后,再返回给前端。
实现方式:
- 开发环境:现代前端框架(如 Vue CLI, Create React App, Vite)都内置了开发服务器代理功能,配置非常简单。
- Vite 示例 (
vite.config.js)javascriptexport default { server: { proxy: { // 字符串简写 '/api': 'http://api.b.com', // 选项写法 '/api': { target: 'http://api.b.com', changeOrigin: true, // 必须设置为 true rewrite: (path) => path.replace(/^\/api/, '') // 去掉路径中的 /api } } } }
- Vite 示例 (
- 生产环境:通常使用 Nginx 作为反向代理。
- Nginx 配置示例 (
nginx.conf)plaintextserver { listen 80; server_name www.a.com; location / { # 前端静态文件 root /path/to/frontend/dist; try_files $uri $uri/ /index.html; } location /api/ { # 代理所有 /api/ 开头的请求 proxy_pass http://api.b.com/; proxy_set_header Host api.b.com; proxy_set_header X-Real-IP $remote_addr; } }
- Nginx 配置示例 (
方案三:JSONP (JSON with Padding) 【已过时,不推荐】
核心思想:
利用 <script> 标签的 src 属性不受同源策略限制。
- 前端定义一个回调函数(例如
handleData)。 - 通过
<script>标签向服务器发送请求,并在 URL 中带上这个回调函数名(例如?callback=handleData)。 - 服务器收到请求后,不返回纯粹的 JSON 数据,而是返回一段 JavaScript 代码,内容是调用这个回调函数,并将 JSON 数据作为参数传入。例如
handleData({ "name": "Alice" })。 - 浏览器加载并执行这段代码,前端定义的
handleData函数就被调用了。
缺点:
- 只支持 GET 请求:因为
<script>标签只能发送 GET 请求。 - 安全性差:容易遭受 XSS 攻击,因为服务器可以返回任意的 JavaScript 代码。
- 错误处理不便。
由于 CORS 的普及,JSONP 现在已经很少使用了,不建议在新项目中使用。
总结与建议
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| CORS | W3C标准、安全、支持所有HTTP方法、功能强大 | 需要服务器端配置支持 | 首选方案,尤其是自己能控制后端API时。 |
| 代理 (Proxy) | 无需修改目标服务器、安全、可以解决任何跨域问题 | 需要额外配置一台代理服务器、增加了一次网络请求 | 当前端无法控制目标服务器API时(如调用第三方服务)。 |
| JSONP | 兼容性好(支持老旧浏览器) | 只支持GET、不安全、已过时 | 基本不再使用,仅用于兼容非常古老的项目。 |
简单来说:
- 能改后端代码? -> 用 CORS。
- 不能改后端代码? -> 用 代理。
- 忘了 JSONP 吧,除非你在维护一个古董项目。