前言
在现代Web开发中,跨域资源共享(CORS)是一个非常重要但又常常让人困惑的概念。无论是前端开发者还是后端开发者,都需要深入理解CORS的工作原理,以便正确处理跨域请求。本文将基于《JavaScript高级程序设计(第三版)》第21.4节的内容,并结合现代Web开发实践,全面解析CORS的机制、配置和常见问题解决方案。

一、什么是跨域资源共享(CORS)?
跨源资源共享(CORS,Cross-Origin Resource Sharing)是一种基于HTTP头的机制,该机制通过允许服务器标示除了它自己以外的其他源(域、协议或端口),使得浏览器允许这些源访问加载自己的资源。
简单来说,CORS是一种安全机制,它允许一个域上的Web应用访问另一个域上的资源。这解决了浏览器同源策略的限制,使得现代Web应用可以安全地进行跨域数据交互。
1.1 同源策略与跨域请求
出于安全性考虑,浏览器实施了同源策略(Same-Origin Policy),限制了脚本内发起的跨源HTTP请求。这意味着使用XMLHttpRequest和Fetch API的Web应用程序只能从加载应用程序的同一个域请求HTTP资源,除非响应报文包含了正确的CORS响应头。
同源的定义是:协议、域名、端口都相同。以下是一些示例:
https://example.com和https://example.com- 同源https://example.com和https://api.example.com- 不同源(子域名不同)https://example.com和http://example.com- 不同源(协议不同)https://example.com和https://example.com:8080- 不同源(端口不同)
1.2 CORS的作用
CORS机制允许Web应用服务器进行跨源访问控制,从而使跨源数据传输得以安全进行。现代浏览器支持在API容器中(例如XMLHttpRequest或Fetch)使用CORS,以降低跨源HTTP请求所带来的风险。
二、CORS的工作原理
CORS的工作机制可以分为两种类型:简单请求和预检请求。
2.1 简单请求
某些请求不会触发CORS预检请求,这类请求被称为简单请求。满足以下条件的请求被视为简单请求:
-
使用以下HTTP方法之一:
- GET
- HEAD
- POST
-
除了被用户代理自动设置的标头字段外,只允许人为设置以下标头:
- Accept
- Accept-Language
- Content-Language
- Content-Type(但仅限于以下三种值)
- text/plain
- multipart/form-data
- application/x-www-form-urlencoded
- Range(只允许简单的范围标头值)
-
请求中没有使用ReadableStream对象。
我们通过一个示例来说明简单请求的工作流程:
javascript
// 客户端代码
const fetchPromise = fetch("https://api.example.com/data");
fetchPromise
.then((response) => response.json())
.then((data) => {
console.log(data);
});
这个请求会发送以下HTTP请求:
makefile
GET /data HTTP/1.1
Host: api.example.com
User-Agent: Mozilla/5.0 ...
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Origin: https://example.com
Connection: keep-alive
服务器响应:
yaml
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 00:23:53 GMT
Server: Apache/2
Access-Control-Allow-Origin: https://example.com
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: application/json
{"message": "Hello from API"}
2.2 预检请求
与简单请求不同,"需预检的请求"要求必须首先使用OPTIONS方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。
以下情况会触发预检请求:
- 使用GET、HEAD、POST以外的HTTP方法
- 使用了自定义的HTTP头
- Content-Type为application/json等非简单类型
示例代码:
javascript
// 客户端代码
const fetchPromise = fetch("https://api.example.com/data", {
method: "POST",
mode: "cors",
headers: {
"Content-Type": "application/json",
"X-Custom-Header": "value",
},
body: JSON.stringify({ name: "John", age: 30 }),
});
fetchPromise.then((response) => {
console.log(response.status);
});
CORS请求的完整流程如下图所示:

三、CORS相关的HTTP头
CORS机制通过一系列HTTP头来实现跨域访问控制。下面我们详细介绍这些HTTP头。

3.1 请求头
Origin
Origin请求头表明了请求的来源(协议、主机、端口)。
arduino
Origin: https://example.com
Access-Control-Request-Method
在预检请求中,浏览器使用此头来告知服务器实际请求将使用哪种HTTP方法。
makefile
Access-Control-Request-Method: POST
Access-Control-Request-Headers
在预检请求中,浏览器使用此头来告知服务器实际请求将携带哪些HTTP头。
go
Access-Control-Request-Headers: content-type,x-custom-header
3.2 响应头
Access-Control-Allow-Origin
这是最重要的CORS响应头,它指定了哪些源可以访问资源。
允许所有源访问:
makefile
Access-Control-Allow-Origin: *
只允许特定源访问:
arduino
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Methods
指定允许访问资源的HTTP方法。
sql
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers
指定允许在实际请求中使用的HTTP头。
makefile
Access-Control-Allow-Headers: Content-Type, X-Custom-Header
Access-Control-Allow-Credentials
指示是否允许携带凭据(如cookies、TLS客户端证书等)。
yaml
Access-Control-Allow-Credentials: true
注意:当使用凭据时,Access-Control-Allow-Origin不能设置为"*",必须指定具体的源。
Access-Control-Expose-Headers
允许浏览器访问的响应头列表。
makefile
Access-Control-Expose-Headers: X-Custom-Header, X-Another-Header
Access-Control-Max-Age
指定预检请求的结果可以被缓存多久(以秒为单位)。
makefile
Access-Control-Max-Age: 86400
四、服务器端配置示例
4.1 Node.js/Express配置
javascript
const express = require('express');
const app = express();
// 添加CORS中间件
app.use((req, res, next) => {
// 允许所有源访问(开发环境)
res.header('Access-Control-Allow-Origin', '*');
// 或者只允许特定源访问(生产环境)
// res.header('Access-Control-Allow-Origin', 'https://example.com');
// 允许的HTTP方法
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
// 允许的HTTP头
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization, X-Custom-Header');
// 允许携带凭据
res.header('Access-Control-Allow-Credentials', 'true');
// 预检请求缓存时间
res.header('Access-Control-Max-Age', '86400');
// 处理预检请求
if (req.method === 'OPTIONS') {
res.sendStatus(200);
} else {
next();
}
});
app.get('/api/data', (req, res) => {
res.json({ message: 'Hello from API' });
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});
4.2 Nginx配置
nginx
server {
listen 80;
server_name api.example.com;
location / {
# 允许所有源访问(开发环境)
add_header 'Access-Control-Allow-Origin' '*';
# 或者只允许特定源访问(生产环境)
# add_header 'Access-Control-Allow-Origin' 'https://example.com';
# 允许的HTTP方法
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
# 允许的HTTP头
add_header 'Access-Control-Allow-Headers' 'Origin, X-Requested-With, Content-Type, Accept, Authorization, X-Custom-Header';
# 允许携带凭据
add_header 'Access-Control-Allow-Credentials' 'true';
# 预检请求缓存时间
add_header 'Access-Control-Max-Age' '86400';
# 处理预检请求
if ($request_method = 'OPTIONS') {
return 204;
}
# 其他配置...
}
}
4.3 Apache配置
apache
<IfModule mod_headers.c>
# 允许所有源访问(开发环境)
Header set Access-Control-Allow-Origin "*"
# 或者只允许特定源访问(生产环境)
# Header set Access-Control-Allow-Origin "https://example.com"
# 允许的HTTP方法
Header set Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
# 允许的HTTP头
Header set Access-Control-Allow-Headers "Origin, X-Requested-With, Content-Type, Accept, Authorization, X-Custom-Header"
# 允许携带凭据
Header set Access-Control-Allow-Credentials "true"
# 预检请求缓存时间
Header set Access-Control-Max-Age "86400"
# 处理预检请求
<If "%{REQUEST_METHOD} == 'OPTIONS'">
Header set Content-Length 0
Header set Content-Type text/plain
Redirect 204 /
</If>
</IfModule>
五、客户端使用示例
5.1 使用Fetch API
javascript
// 简单GET请求
fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
// POST请求携带自定义头
fetch('https://api.example.com/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Custom-Header': 'value'
},
body: JSON.stringify({ name: 'John' })
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
// 携带凭据的请求
fetch('https://api.example.com/data', {
method: 'GET',
credentials: 'include' // 包含cookies
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
5.2 使用XMLHttpRequest
javascript
// 创建XMLHttpRequest对象
const xhr = new XMLHttpRequest();
// 配置请求
xhr.open('GET', 'https://api.example.com/data', true);
// 设置携带凭据
xhr.withCredentials = true;
// 设置请求头
xhr.setRequestHeader('X-Custom-Header', 'value');
// 处理响应
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
const data = JSON.parse(xhr.responseText);
console.log(data);
} else {
console.error('Error:', xhr.status);
}
}
};
// 发送请求
xhr.send();
六、常见问题与解决方案
6.1 CORS错误排查
当遇到CORS错误时,浏览器控制台通常会显示类似以下的错误信息:
csharp
Access to fetch at 'https://api.example.com/data' from origin 'https://example.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
排查步骤:
- 检查请求的Origin头是否正确
- 检查服务器响应是否包含正确的Access-Control-Allow-Origin头
- 对于预检请求,检查服务器是否正确响应OPTIONS请求
- 检查是否需要携带凭据,如果需要,确保Access-Control-Allow-Origin不是通配符
6.2 凭据与通配符问题
当请求需要携带凭据(cookies、HTTP认证信息)时,服务器不能将Access-Control-Allow-Origin设置为"*"。
错误示例:
http
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
正确示例:
http
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Credentials: true
6.3 预检请求失败
预检请求失败通常是因为服务器没有正确处理OPTIONS请求,或者没有返回正确的CORS响应头。
解决方案:
- 确保服务器正确处理OPTIONS请求
- 确保返回正确的Access-Control-Allow-Methods和Access-Control-Allow-Headers头
- 设置适当的Access-Control-Max-Age以减少预检请求频率
6.4 缓存问题
CORS响应头可以被缓存,但需要注意以下几点:
-
如果服务器使用通配符"*"而不是指定具体的源,需要在Vary响应头中包含Origin:
httpAccess-Control-Allow-Origin: * Vary: Origin -
预检请求的结果可以通过Access-Control-Max-Age头进行缓存
七、安全考虑
7.1 避免过度宽松的CORS策略
虽然在开发环境中使用通配符"*"很方便,但在生产环境中应该始终指定具体的源:
javascript
// 不安全的做法
res.header('Access-Control-Allow-Origin', '*');
// 安全的做法
res.header('Access-Control-Allow-Origin', 'https://trusted-domain.com');
7.2 验证Origin头
在服务器端,可以验证Origin头以确保只允许受信任的源访问:
javascript
const allowedOrigins = [
'https://example.com',
'https://app.example.com'
];
app.use((req, res, next) => {
const origin = req.headers.origin;
if (allowedOrigins.includes(origin)) {
res.header('Access-Control-Allow-Origin', origin);
}
// 其他CORS设置...
next();
});
7.3 限制HTTP方法和头
只允许必要的HTTP方法和头,避免暴露不必要的接口:
http
Access-Control-Allow-Methods: GET, POST
Access-Control-Allow-Headers: Content-Type, Authorization
八、最佳实践
8.1 开发环境与生产环境的区别
在开发环境中,可以使用宽松的CORS策略以方便调试:
javascript
// 开发环境
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
// 其他CORS设置...
next();
});
在生产环境中,应该使用严格的CORS策略:
javascript
// 生产环境
const allowedOrigins = [
'https://yourdomain.com',
'https://app.yourdomain.com'
];
app.use((req, res, next) => {
const origin = req.headers.origin;
if (allowedOrigins.includes(origin)) {
res.header('Access-Control-Allow-Origin', origin);
}
// 其他CORS设置...
next();
});
8.2 使用CORS中间件
对于Node.js应用,可以使用专门的CORS中间件:
javascript
const cors = require('cors');
const corsOptions = {
origin: ['https://example.com', 'https://app.example.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true,
maxAge: 86400
};
app.use(cors(corsOptions));
8.3 监控和日志
记录CORS相关的请求和错误,有助于及时发现问题:
javascript
app.use((req, res, next) => {
// 记录CORS请求
if (req.headers.origin) {
console.log(`CORS request from ${req.headers.origin} to ${req.url}`);
}
// 处理CORS
// ...
next();
});
总结
CORS是现代Web开发中不可或缺的一部分,理解其工作原理对于构建安全、高效的Web应用至关重要。通过正确配置CORS策略,我们可以在保证安全的前提下实现跨域资源共享。
关键要点回顾:
- CORS通过HTTP头实现跨域访问控制
- 简单请求和预检请求有不同的处理机制
- 正确设置CORS响应头是解决跨域问题的关键
- 生产环境中应使用严格的CORS策略
- 注意凭据与通配符的使用限制
- 合理利用预检请求缓存提高性能
通过本文的介绍,希望你能够更好地理解和应用CORS,解决实际开发中的跨域问题。
最后,创作不易请允许我插播一则自己开发的"数规规-数字助手"(有各种预测分析)小程序广告,感兴趣可以微信小程序体验放松放松,程序员也要有点娱乐生活,搞不好就中个排列五了呢?
感兴趣可以搜索微信小程序"数规规数字助手"体验体验!!