打破同源策略:前端跨域的全面解析与应对策略
跨域定义
跨域(Cross-Origin)指浏览器出于安全考虑,限制网页脚本向不同源(协议、域名、端口)的服务器发起 HTTP 请求。这是由浏览器的同源策略(Same-Origin Policy)引起的安全机制。
跨域原因
当以下任意一项不匹配时即产生跨域:
-
协议不同(http vs https)
-
域名不同(www.a.com vs www.b.com)
-
端口不同(8080 vs 3000)
跨域现象
-
控制台报错:
Access to XMLHttpRequest at 'xxx' from origin 'yyy' has been blocked by CORS policy
Access to XMLHttpRequest at 'http://api.example.com/data' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
-
请求状态码可能显示为
200
但数据无法获取 -
网络请求显示
(blocked:origin)
标记
主流解决方案及代码示例
方案 1:CORS(跨域资源共享) ★ 最常用
原理:服务端通过设置 HTTP 响应头告知浏览器允许跨域请求
Spring Boot 全局配置 ★ 推荐
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") // 匹配所有路由
.allowedOrigins("*") // 允许所有来源(生产环境建议改为具体域名)
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") // 允许的方法
.allowedHeaders("*") // 允许所有请求头
.allowCredentials(false) // 是否允许发送 Cookie(设置为 true 时 allowedOrigins 不能为 *)
.maxAge(3600); // 预检请求缓存时间(秒)
}
}
Node.js服务端代码
// 服务端代码(Node.js示例)
const http = require('http');
const server = http.createServer((req, res) => {
// 设置允许跨域的域名,* 代表允许任意域名
res.setHeader('Access-Control-Allow-Origin', '*');
// 允许的请求方法(根据需求调整)
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
// 允许携带的请求头(根据需求调整)
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
// 处理预检请求(OPTIONS)
if (req.method === 'OPTIONS') {
res.writeHead(204); // 空响应
res.end();
return;
}
// 正常业务处理
res.end('Hello CORS!');
});
server.listen(3000);
关键配置项说明
响应头 | 作用说明 |
---|---|
Access-Control-Allow-Origin | 允许的源(* 表示全部,生产环境建议指定具体域名) |
Access-Control-Allow-Methods | 允许的 HTTP 方法(GET/POST 等) |
Access-Control-Allow-Headers | 允许携带的请求头(如 Content-Type、Authorization 等自定义头) |
Access-Control-Allow-Credentials | 是否允许发送 Cookie(需与前端 withCredentials 配置配合使用) |
Access-Control-Max-Age | 预检请求的缓存时间(减少 OPTIONS 请求次数) |
注意事项
-
生产环境安全:不要长期使用
allowedOrigins("*")
,应替换为具体的前端域名 -
携带 Cookie:若需要携带 Cookie,需同时设置:
.allowCredentials(true) .allowedOrigins("http://your-frontend-domain.com") // 必须明确指定域名
-
复杂请求:对于
PUT/DELETE
或自定义请求头的请求,浏览器会先发送OPTIONS
预检请求 -
Spring Security:若使用安全框架,需要在安全配置中放行 OPTIONS 请求
完整请求流程示例
浏览器 → 发送 GET 请求 → 服务器
← 返回 CORS 头 ←
方案 2:JSONP(仅限 GET 请求,历史解决方案,可忽略)
原理:利用 <script>
标签没有跨域限制的特性
// 前端代码
function jsonpRequest(url, callbackName) {
// 创建 script 标签
const script = document.createElement('script');
// 构造带回调函数名的请求 URL
script.src = `${url}?callback=${callbackName}`;
// 定义全局回调函数
window[callbackName] = (data) => {
console.log('Received data:', data);
// 清理工作
document.body.removeChild(script);
delete window[callbackName];
};
// 发起请求
document.body.appendChild(script);
}
// 使用示例
jsonpRequest('http://api.example.com/data', 'handleData');
方案 3:代理服务器 ★ 重要
原理:前端请求同源服务器,由服务器转发请求
3.1 Vue 开发环境代理(基于 vue-cli
或 vite
)
1. vue.config.js
配置(Webpack)
// vue.config.js
module.exports = {
devServer: {
proxy: {
'/api': { // 匹配所有以 /api 开头的请求
target: 'http://backend-server.com', // 目标服务器地址
changeOrigin: true, // 修改请求头中的 Origin 为目标地址
pathRewrite: {
'^/api': '' // 将 /api 前缀替换为空字符串(可选)
},
secure: false, // 如果目标使用 HTTPS 且证书无效,需设置为 false
ws: true // 代理 WebSocket
}
}
}
}
2. Vite 配置(vite.config.js
)
// vite.config.js
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://backend-server.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '') // 路径重写
}
}
}
})
3.2 React 开发环境代理(基于 create-react-app
)
1. 快速配置(无需 eject)
在 package.json
中添加:
{
"proxy": "http://backend-server.com" // 简单代理所有非静态资源请求
}
2. 高级配置(自定义 setupProxy.js
)
// src/setupProxy.js(需安装 http-proxy-middleware)
const { createProxyMiddleware } = require('http-proxy-middleware');
module.exports = function(app) {
app.use(
'/api',
createProxyMiddleware({
target: 'http://backend-server.com',
changeOrigin: true,
pathRewrite: { '^/api': '' }, // 路径重写
onProxyRes: (proxyRes) => { // 可监听代理响应
console.log('Proxying request to:', proxyRes.req.path);
}
})
);
};
3.3 Nginx 生产环境代理配置
1. 基础反向代理配置 ★ 重要
# 定义 HTTP 服务器监听配置
server {
# 监听 80 端口(HTTP 协议默认端口)
listen 80;
# 绑定的域名(生产环境替换为实际域名,支持通配符如 *.example.com)
server_name your-domain.com;
# API 请求代理配置块(匹配所有以 /api/ 开头的请求)
location /api/ {
# 反向代理到后端服务器(注意结尾的 / 会自动去除 /api 前缀)
# 示例:请求 /api/user → 转发到 http://backend-server.com/user
proxy_pass http://backend-server.com/;
# ---------- 核心代理头配置 ----------
# 传递原始请求的 Host 头(后端可通过此头识别原始域名)
proxy_set_header Host $host;
# 传递客户端真实 IP(后端可获取用户真实 IP)
proxy_set_header X-Real-IP $remote_addr;
# 传递经过的代理服务器 IP 链(用于追踪请求路径)
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# ---------- 跨域响应头配置(若后端未处理 CORS 则必需)----------
# 允许所有来源访问(生产环境应替换为具体前端域名,如 http://your-frontend.com)
add_header Access-Control-Allow-Origin *;
# 允许的 HTTP 方法(根据实际需求调整)
add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
# 允许携带的请求头(需包含实际使用的自定义头,如 Authorization)
add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
# ---------- 处理 OPTIONS 预检请求 ----------
# 当请求方法为 OPTIONS 时(浏览器自动发起的预检请求)
常见问题处理
现象 | 解决方案 |
---|---|
前端路由刷新 404 | 确认 try_files 配置正确指向前端入口文件 |
OPTIONS 请求返回 404 | 检查 proxy_pass 地址是否可访问,确保后端服务正确处理 OPTIONS 方法 |
无法获取真实客户端 IP | 在后端服务中检查 X-Real-IP 或 X-Forwarded-For 头 |
静态资源加载失败 | 检查 root 路径权限,确认文件已正确部署到 /usr/share/nginx/html 目录 |
CORS 头未生效 | 确保未在后端重复设置 CORS 头(否则可能产生冲突) |
2. 带 WebSocket 代理的配置
location /ws/ {
proxy_pass http://websocket-server.com/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade; # 关键配置
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
}
不同场景对比
方案 | 适用阶段 | 优势 | 注意事项 |
---|---|---|---|
Vue/React 代理 | 开发环境 | 无需后端配合,热更新即时生效 | 生产环境需部署到服务器 |
Nginx 代理 | 生产环境 | 高性能、支持负载均衡、可配置性强 | 需要服务器运维知识 |
Node 中间件代理 | 全阶段 | 灵活处理请求(可添加鉴权等逻辑) | 需要额外开发中间件代码 |
关键配置说明
-
changeOrigin
的作用-
true
:将请求头中的Host
改为目标服务器地址 -
false
:保持原始Host
(可能导致目标服务器拒绝)
-
-
路径重写规则
pathRewrite: { '^/api': '' } // 请求 /api/user → 转发到 /user
-
生产环境安全建议
-
不要使用
Access-Control-Allow-Origin: *
-
限制允许的 HTTP 方法
add_header Access-Control-Allow-Origin https://your-frontend.com; add_header Access-Control-Allow-Methods 'GET, POST';
验证代理是否生效
-
浏览器控制台检查
# 原始请求 fetch('/api/data') # 实际网络请求显示 Request URL: http://localhost:8080/api/data Proxied to: http://backend-server.com/data
-
cURL 测试
curl -I http://your-domain.com/api/ping # 检查返回的 Server 头是否为 Nginx
方案 4:WebSocket (了解)
原理:WebSocket 协议不受同源策略限制
// 前端代码
const socket = new WebSocket('ws://echo.websocket.org');
socket.onopen = () => {
socket.send('Hello Server!');
};
socket.onmessage = (event) => {
console.log('Received:', event.data);
};
方案 5:postMessage (了解)
原理:跨文档通信 API
// 发送方(例如 iframe 中的页面)
window.parent.postMessage('Hello Parent!', 'http://parent-domain.com');
// 接收方(父页面)
window.addEventListener('message', (event) => {
// 验证来源
if (event.origin !== 'http://child-domain.com') return;
console.log('Received:', event.data);
});
各方案对比
方案 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
CORS | 标准方案,安全性高 | 需要服务端配合 | 主流跨域解决方案 |
JSONP | 兼容性好 | 仅支持 GET,存在安全隐患 | 老项目兼容 |
代理服务器 | 前端无需修改代码 | 需要服务器支持 | 开发环境/有服务器控制权 |
WebSocket | 双向通信,无跨域限制 | 协议切换成本高 | 实时通信场景 |
postMessage | 可跨域窗口通信 | 只能窗口间通信 | iframe/多窗口交互 |
总结
建议优先使用 CORS 方案,开发环境可配合代理服务器,老项目考虑 JSONP,特殊场景选择其他方案