引言:理解跨域问题的本质
在现代Web开发中,跨域资源共享(CORS)是一个常见且棘手的问题。浏览器的同源策略(Same-Origin Policy)是Web安全的基础,它限制了一个源(origin)的文档或脚本如何与来自另一个源的资源进行交互。同源指的是协议(protocol)、域名(domain)和端口(port)都相同。
当我们在开发Web应用时,经常会遇到需要从一个域名请求另一个域名下的API资源的情况,例如:
前端应用(
http://frontend.example.com)需要调用后端API(http://api.example.com)
微服务架构中不同服务之间的数据交互
第三方API集成(如调用支付、地图等服务)
本文将深入探讨在浏览器同源策略限制下实现跨域请求的各种解决方案,包括CORS、JSONP、代理服务器、WebSocket等方法,并提供详细的代码示例和最佳实践。
同源策略详解
什么是同源策略
同源策略是浏览器的一个核心安全功能,它限制了一个源的文档或脚本如何与来自另一个源的资源进行交互。这里的”源”由协议、域名和端口三部分组成。
例如:
http://example.com/page1
和
http://example.com/page2
同源(协议、域名、端口相同)
http://example.com
和
https://example.com
不同源(协议不同)
http://example.com
和
http://api.example.com
不同源(域名不同)
http://example.com:80
和
http://example.com:8080
不同源(端口不同)
同源策略限制的资源类型
同源策略主要限制以下类型的资源:
DOM访问
:一个源的JavaScript不能访问另一个源的DOM
AJAX请求
:XMLHttpRequest和Fetch API不能向不同源发送请求(除非目标服务器明确允许)
Web字体
(CSS中@font-face)
Canvas图像数据
(跨域图片绘制)
WebGL纹理
为什么需要跨域
尽管同源策略提供了安全保障,但在实际开发中,跨域需求非常普遍:
微服务架构
:前端需要与多个后端服务通信
第三方服务集成
:调用支付、地图、社交媒体API
CDN资源加载
:从不同域名加载静态资源
子域名分离
:主站和API子域名分离
开发环境
:本地开发服务器与API服务器分离
跨域解决方案详解
1. CORS(Cross-Origin Resource Sharing)
CORS是W3C标准,是现代浏览器支持的跨域解决方案。它允许服务器声明哪些源可以访问其资源。
CORS工作原理
CORS通过HTTP头部来实现:
简单请求
:GET、HEAD、POST方法,且使用标准Content-Type(application/x-www-form-urlencoded、multipart/form-data、text/plain)
预检请求
:非简单请求(PUT、DELETE、自定义头部、非标准Content-Type)会先发送OPTIONS请求进行预检
服务器端实现(Node.js + Express)
const express = require(‘express’);
const cors = require(‘cors’);
const app = express();
// 方法1:允许所有源(开发环境)
app.use(cors());
// 方法2:允许特定源
const allowedOrigins = [
‘http://localhost:3000’,
‘https://frontend.example.com’,
‘https://app.example.com’
];
app.use(cors({
origin: function (origin, callback) {
// 允许没有origin的请求(如同源请求或服务器端请求)
if (!origin) return callback(null, true);
if (allowedOrigins.indexOf(origin) === -1) {
const msg = ‘The CORS policy for this site does not allow access from the specified Origin.’;
return callback(new Error(msg), false);
}
return callback(null, true);
},
credentials: true, // 允许携带凭证(cookies、HTTP认证)
methods: [‘GET’, ‘POST’, ‘PUT’, ‘DELETE’, ‘OPTIONS’], // 允许的HTTP方法
allowedHeaders: [‘Content-Type’, ‘Authorization’, ‘X-Requested-With’], // 允许的请求头
exposedHeaders: [‘X-Custom-Header’], // 暴露给前端的响应头
maxAge: 600 // 预检请求缓存时间(秒)
}));
// 处理预检请求
app.options(‘*’, cors());
// API路由
app.get(‘/api/data’, (req, res) => {
res.json({ message: ‘This is CORS-enabled for all origins!’ });
});
app.listen(5000, () => {
console.log(‘CORS-enabled web server listening on port 5000’);
});
服务器端实现(Nginx配置)
server {
listen 80;
server_name api.example.com;
# CORS配置
location / {
# 允许的源
if ($http_origin ~* (https?://(localhost|frontend.example.com|app.example.com)(:[0-9]+)?$)) {
add_header ‘Access-Control-Allow-Origin’ “$http_origin” always;
}
# 允许的HTTP方法
add_header ‘Access-Control-Allow-Methods’ ‘GET, POST, PUT, DELETE, OPTIONS’ always;
# 允许的请求头
add_header ‘Access-Control-Allow-Headers’ ‘Content-Type, Authorization, X-Requested-With’ always;
# 暴露的响应头
add_header ‘Access-Control-Expose-Headers’ ‘X-Custom-Header’ always;
# 允许携带凭证
add_header ‘Access-Control-Allow-Credentials’ ‘true’ always;
# 预检请求缓存时间
add_header ‘Access-Control-Max-Age’ ‘600’ always;
# 处理预检请求
if ($request_method = ‘OPTIONS’) {
add_header ‘Access-Control-Allow-Origin’ “$http_origin”;
add_header ‘Access-Control-Allow-Methods’ ‘GET, POST, PUT, DELETE, OPTIONS’;
add_header ‘Access-Control-Allow-Headers’ ‘Content-Type, Authorization, X-Requested-With’;
add_header ‘Access-Control-Max-Age’ ‘600’;
add_header ‘Content-Type’ ‘text/plain; charset=utf-8’;
add_header ‘Content-Length’ ‘0’;
return 204;
}
proxy_pass http://backend;
}
}
客户端实现
// 使用Fetch API
async function fetchData() {
try {
const response = await fetch(‘https://api.example.com/data’, {
method: ‘GET’,
credentials: ‘include’, // 携带cookies
headers: {
‘Content-Type’: ‘application/json’,
‘Authorization’: ‘Bearer token123’
}
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log(data);
} catch (error) {
console.error(‘Fetch error:’, error);
}
}
// 使用XMLHttpRequest
function fetchWithXHR() {
const xhr = new XMLHttpRequest();
xhr.open(‘GET’, ‘https://api.example.com/data’, true);
xhr.withCredentials = true; // 携带cookies
xhr.setRequestHeader(‘Content-Type’, ‘application/json’);
xhr.setRequestHeader(‘Authorization’, ‘Bearer token123’);
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
const data = JSON.parse(xhr.responseText);
console.log(data);
} else {
console.error(‘XHR error:’, xhr.statusText);
}
}
};
xhr.send();
}
// 使用Axios库
const axios = require(‘axios’);
const api = axios.create({
baseURL: ‘https://api.example.com’,
withCredentials: true,
headers: {
‘Content-Type’: ‘application/json’,
‘Authorization’: ‘Bearer token123’
}
});
api.get(‘/data’)
.then(response => {
console.log(response.data);
})
.catch(error => {
console.error(‘Axios error:’, error);
});
CORS的优缺点
优点
:
标准化解决方案,现代浏览器都支持
安全可控,服务器可以精确控制允许的源
支持所有HTTP方法和自定义头部
支持携带凭证(cookies、HTTP认证)
缺点
:
需要服务器端支持
对于旧浏览器(IE9及以下)不支持
预检请求会增加延迟
配置复杂,容易出错
2. JSONP(JSON with Padding)
JSONP是一种利用