一、跨域问题的本质:同源策略与安全边界
1.1 什么是跨域?
跨域(Cross-Origin)的核心根源,是浏览器内置的同源策略,指的是浏览器出于安全考虑,限制一个源(Origin)的脚本访问另一个源的资源。这里的”源”由三个要素组成,所谓「同源」,要求两个 URL 的以下三个要素完全一致:
- 协议(http vs https)
- 域名(example.com vs api.example.com)
- 端口(80 vs 8080)
只要三者中任一不同,即构成跨域。举个直观的例子,以 http://www.example.com:80 为基准,同源判定结果如下:
| 请求 URL | 是否同源 | 跨域原因 |
|---|---|---|
https://www.example.com | 否 | 协议不同(http→https) |
http://api.example.com | 否 | 域名不同(主域一致,子域不同) |
http://www.example.com:3000 | 否 | 端口不同(80→3000) |
http://www.example.com/user | 是 | 仅路径不同,三要素完全一致 |
补充:
核心定义
- 主域(注册域名):是用户通过域名注册商合法申请、拥有完整所有权与管理权的最小独立域名单元,格式为「自定义主体 + 顶级域」,比如
example.com、example.com.cn。主域是域名所有权的核心载体,必须付费注册,可自主创建所有下级子域。- 子域:是主域持有者在主域基础上,免费自主配置的下级从属域名,无需额外注册。比如主域
example.com的子域包括www.example.com、api.example.com、admin.example.com等,子域完全依附于主域,可独立配置解析指向不同服务,但无独立所有权。
1.2 一个必须纠正的核心误区
跨域是浏览器的单向限制,不是服务器拒绝了你的请求
真实的请求流程是这样的:
- 前端发起跨域请求,浏览器正常将请求发送到目标服务器
- 服务器接收请求,正常处理并返回响应数据
- 浏览器收到响应后,检查响应头的 CORS 规则,判定是否允许当前源访问
- 若规则不匹配,浏览器直接拦截响应,抛出 CORS 报错,前端无法拿到任何响应数据
简单说:请求发出去了,服务器也正常返回了,只是浏览器把数据「扣下了」。这也是为什么你在 Network 面板能看到请求的状态码是 200,却拿不到响应体的核心原因。
1.3 同源策略到底限制了什么?
同源策略就像浏览器给每个页面加了一道「安全隔离墙」,主要限制三类行为:
- 数据存储访问:禁止读取 / 修改非同源页面的 Cookie、LocalStorage、IndexedDB 等存储数据
- DOM 操作:禁止获取非同源页面的 DOM 元素,防止恶意页面篡改嵌入的第三方页面
- 网络请求:限制 XMLHttpRequest、Fetch API 发起的跨域 AJAX 请求,这也是我们最常遇到的跨域场景
但有一个关键例外:HTML 的资源嵌入标签不受同源策略限制。
比如<script>、<img>、<link>、<iframe>等标签,可以正常加载跨域资源,这也是 JSONP、图片打点等方案的底层原理。
1.4 为什么会有同源策略?
同源策略是浏览器的核心安全机制,主要防范:
-
XSS 攻击:恶意脚本窃取用户数据
-
CSRF 攻击:伪造用户身份进行恶意操作
-
数据泄露:敏感信息被未授权访问
-
举两个最直观的风险场景:
-
没有同源策略,你打开的恶意钓鱼网站,可以直接读取你网银页面的 Cookie,轻松盗取你的账户信息,发起转账操作
-
没有同源策略,恶意页面可以嵌入你的电商支付页面,篡改 DOM 元素的支付金额,诱导你完成超额付款
-
同源策略的核心意义,就是隔离不同站点的资源,防止恶意网站窃取用户的敏感数据,从根源上阻断绝大多数 CSRF、XSS 衍生的攻击行为。
二、主流跨域解决方案
2.1 CORS:跨域资源共享的核心机制
2.1.1 CORS 工作原理
CORS(Cross-Origin Resource Sharing)是 W3C 标准,通过HTTP 响应头告知浏览器是否允许跨域访问。其核心流程分为两类:
简单请求(Simple Request)
满足以下条件的请求为简单请求:
- 方法仅限
GET、HEAD、POST Content-Type为application/x-www-form-urlencoded、multipart/form-data、text/plain- 无自定义请求头
简单请求直接发送,无需预检。
非简单请求(Preflight Request)
复杂请求会触发浏览器的预检机制:
-
浏览器先使用
OPTIONS方法发起一个「预检请求」,携带三个核心请求头:Origin:请求的来源域Access-Control-Request-Method:正式请求要使用的方法Access-Control-Request-Headers:正式请求要携带的自定义头
// 预检请求示例 OPTIONS /api/data HTTP/1.1 Origin: https://www.example.com Access-Control-Request-Method: PUT Access-Control-Request-Headers: Authorization, Content-Type -
服务器接收预检请求,校验后返回对应的 CORS 响应头,明确是否允许该请求
-
浏览器校验预检响应通过后,才会发起正式的 HTTP 请求;校验失败则直接拦截,正式请求永远不会发出
2.1.2 核心 CORS 响应头
服务器的 CORS 配置,本质就是正确设置以下 HTTP 响应头,每一个都有明确的作用,不可乱配:
| 响应头 | 必需性 | 核心作用 | 安全注意事项 |
|---|---|---|---|
Access-Control-Allow-Origin | 必需 | 指定允许访问的源,值可以是单个具体源、null,或通配符* | 非公开 API 严禁使用*;携带凭据时,必须为具体源,绝对不能用* |
Access-Control-Allow-Methods | 预检必需 | 指定正式请求允许的 HTTP 方法,多个用逗号分隔 | 按需开放,不要全量开放GET,POST,PUT,DELETE,PATCH |
Access-Control-Allow-Headers | 预检必需 | 指定正式请求允许携带的自定义请求头 | 按需开放,比如仅开放Content-Type,Authorization |
Access-Control-Allow-Credentials | 可选 | 指定是否允许请求携带 Cookie、HTTP 认证等身份凭证,值只能是true | 开启后,Allow-Origin不能为*,前端必须设置withCredentials: true |
Access-Control-Max-Age | 可选 | 指定预检请求的缓存时间(秒),缓存期内不会重复发起预检 | 建议设置合理值(如 86400 秒 = 1 天),减少 OPTIONS 请求开销 |
// 使用示例
Access-Control-Allow-Origin: https://www.example.com # 必须,允许的源
Access-Control-Allow-Methods: GET, POST, PUT, DELETE # 允许的HTTP方法
Access-Control-Allow-Headers: Content-Type, Authorization # 允许的请求头
Access-Control-Allow-Credentials: true # 是否允许发送Cookie
Access-Control-Max-Age: 86400 # 预检请求缓存时间(秒)
Access-Control-Expose-Headers: X-Total-Count # 允许客户端访问的响应头
2.2 服务端 CORS 配置(生产环境首选)
Node.js/Express 方案
const express = require('express');
const cors = require('cors');
const app = express();
// 方案1:使用cors中间件(推荐)
app.use(cors({
origin: 'https://www.example.com', // 精确指定源
methods: ['GET', 'POST', 'PUT', 'DELETE'],
credentials: true, // 允许携带Cookie
maxAge: 86400, // 预检缓存24小时
}));
// 方案2:手动配置(更灵活)
app.use((req, res, next) => {
const allowedOrigins = ['https://www.example.com', 'https://app.example.com'];
const origin = req.headers.origin;
if (allowedOrigins.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Access-Control-Allow-Credentials', 'true');
}
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With');
res.setHeader('Access-Control-Max-Age', '86400');
if (req.method === 'OPTIONS') {
return res.sendStatus(200);
}
next();
});
Spring Boot 4.0 方案
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://www.example.com", "https://app.example.com")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("Content-Type", "Authorization", "X-Requested-With")
.allowCredentials(true)
.maxAge(86400); // 24小时
}
// 或使用@CrossOrigin注解
@RestController
@RequestMapping("/api")
@CrossOrigin(origins = "https://www.example.com", maxAge = 86400)
public class ApiController {
// ...
}
}
FastAPI 方案
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
# 配置CORS中间件
app.add_middleware(
CORSMiddleware,
allow_origins=["https://www.example.com", "https://app.example.com"],
allow_credentials=True,
allow_methods=["*"], # 或指定具体方法
allow_headers=["*"], # 或指定具体头
max_age=86400, # 预检缓存24小时
)
2.2 代理方案
代理方案的核心原理,直击同源策略的本质:同源策略仅限制浏览器与服务器之间的通信,服务器与服务器之间的 HTTP 通信,没有任何跨域限制。
我们只需要搭建一个「代理中转服务器」,让浏览器把请求发给同源的代理服务器,再由代理服务器转发给真实的后端接口,就能彻底绕开跨域问题 —— 对浏览器来说,请求是发给同源的代理服务,不存在跨域;对后端来说,请求来自正常的服务器,无需做任何 CORS 配置。其中,代理方案分为开发环境和生产环境两类。
开发环境代理
Vite/Vue3 配置
// vite.config.js
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'https://api.example.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
secure: false, // 不验证SSL证书
ws: true, // 代理WebSocket
}
}
}
})
Webpack DevServer 配置
// webpack.config.js
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'https://api.example.com',
pathRewrite: { '^/api': '' },
changeOrigin: true,
headers: {
Connection: 'keep-alive'
}
}
}
}
}
配置完成后,前端只需把请求地址写成
/api/xxx,开发服务器会自动把请求转发到http://localhost:3000/xxx,浏览器全程认为是同源请求,不会有任何跨域问题。
Nginx 反向代理(生产环境推荐)
生产环境中,我们通常使用 Nginx 作为静态资源服务器和反向代理服务器,实现和开发代理完全一致的效果,同时还能兼顾负载均衡、静态资源缓存等能力。
server {
listen 80;
server_name www.example.com;
# 前端静态资源
location / {
root /var/www/frontend;
index index.html;
try_files $uri $uri/ /index.html;
}
# API代理,解决跨域
location /api/ {
proxy_pass https://api.example.com/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# CORS相关头
add_header 'Access-Control-Allow-Origin' 'https://www.example.com' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
# 处理预检请求
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Max-Age' 86400;
add_header 'Content-Type' 'text/plain; charset=utf-8';
add_header 'Content-Length' 0;
return 204;
}
}
}
配置完成后,用户访问
https://www.example.com加载前端页面,所有/api开头的请求都会被 Nginx 转发到后端服务,前端和接口处于完全同源的状态,从根源上彻底消除了跨域问题,无需后端做任何 CORS 配置。
2.3 JSONP:仅兼容老旧浏览器的古董方案
JSONP 是早期前端解决跨域的方案,核心原理是利用<script>标签不受同源策略限制的特性,通过动态创建 script 标签,请求后端返回一个「函数调用」,把数据作为参数传入,前端在全局定义该函数,从而拿到跨域数据。
2.4 postMessage:跨窗口 /iframe 通信专属方案
postMessage 是 HTML5 提供的 API,专门用于解决「不同源的页面之间」的通信问题,最常见的场景:主页面与嵌入的跨域 iframe 通信、同一浏览器打开的多个跨域标签页通信。
2.5 WebSocket:全双工通信无跨域限制
WebSocket 协议本身不受浏览器同源策略限制,只要服务器支持,前端可以和任意源的 WebSocket 服务建立连接,实现全双工通信,是实时通信场景的最优解。
2.3 跨域解决方案对比与选型建议
| 方案 | 适用场景 | 优点 | 缺点 | 安全性 |
|---|---|---|---|---|
| 服务端 CORS | 生产环境 | 标准化、浏览器原生支持 | 需要后端配合 | ★★★★☆ |
| Nginx 代理 | 生产环境 | 性能好、配置灵活 | 运维复杂度高 | ★★★★★ |
| 开发代理 | 开发环境 | 无需后端改动、调试方便 | 仅限开发环境 | ★★★★☆ |
| JSONP | 传统项目 | 兼容性好 | 仅支持 GET、有安全风险 | ★☆☆☆☆ |
| WebSocket | 实时通信 | 无跨域限制 | 仅适用于特定场景 | ★★★★☆ |
选型建议
- 生产环境:优先选择 Nginx 反向代理或服务端 CORS 配置
- 开发环境:使用 Webpack/Vite 内置代理
- 遗留系统:考虑 WebSocket 或服务端代理
- 移动端/小程序:服务端统一代理,避免前端跨域问题
喜欢的话,留下你的评论吧~