这篇文章主要是帮助大家全方位理解跨源资源共享-CORS,到底是一个什么东西。
概念
跨源资源共享(Cross-Origin Resource Sharing,CORS)是一个由一系列传输的HTTP标头组成的系统。这些HTTP标头决定浏览器是否阻止前端JavaScript代码获取跨源请求的响应。
从浏览器同源策略引发的跨域问题说起
同源安全策略(浏览器安全策略:协议、域名、端口号必须一致)默认阻止"跨源"获取资源。但是CORS给了Web服务器这样的权限,即服务器可以选择允许跨源请求访问到它们的资源。
浏览器根据这些信息决定是否放行请求,核心是服务器配置响应头,无需前端修改代码,是解决跨域问题的主流方案。
以下表格列出了常见操作是否遵循同源策略:
操作类型 | 具体操作或对象 | 是否遵循同源策略 (默认行为) | 说明 |
---|---|---|---|
DOM 访问 | iframe.contentDocument |
❌ 禁止 | 无法直接访问不同源 iframe 内的 DOM 树或 JS 对象。 |
window.open 返回的窗口对象 |
❌ 禁止 | 无法访问不同源弹出窗口的 document 或 location (除 window.close 和 window.opener 外)。 |
|
数据读写 (AJAX/Fetch) | XMLHttpRequest / fetch |
❌ 禁止 | 默认无法向不同源发送 读取响应内容 的请求(GET/POST/PUT/DELETE等)。 |
WebSocket 连接 |
✅ 允许 | 例外:不受同源策略限制(但可自定义 Origin 头供服务器验证)。 | |
资源嵌入 | <script src="..."> |
✅ 允许 | 可以加载并执行不同源的 JS 脚本(如 CDN 上的库),但 脚本本身 在目标源的上下文中执行,只能访问目标源的资源。 |
<link rel="stylesheet" href="..."> |
✅ 允许 | 可以加载不同源的 CSS 样式表。 | |
<img src="..."> |
✅ 允许 | 可以加载不同源的图片。 | |
<video>/<audio src="..."> |
✅ 允许 | 可以加载不同源的媒体文件。 | |
<object> , <embed> , <applet> |
✅ 允许 | 可以加载不同源的插件内容(注意安全风险)。 | |
@font-face |
✅ 允许 | 可以加载不同源的字体(现代浏览器通常默认允许)。 | |
表单提交 | <form action="..."> |
✅ 允许 | 可以 向不同源提交表单数据(POST/GET),但 浏览器无法读取响应结果(页面会跳转或刷新到目标源)。 |
Cookie / LocalStorage | 读取/写入 Cookie | ❌ 禁止 | 浏览器只发送当前源的 Cookie,无法读写不同源的 Cookie。 |
读取/写入 localStorage /sessionStorage |
❌ 禁止 | 无法访问不同源的 Web Storage。 | |
跨窗口消息 | window.postMessage |
✅ 允许 | 例外 :专门设计的 API,允许安全地 在不同源窗口间传递消息(需目标窗口监听 message 事件)。 |
- 限制交互:对DOM访问、数据读写、客户端存储的同源限制最为严格
- 允许嵌入资源:加载不同源的脚本、样式、图片、媒体、字体、插件是允许的,这是网页能正常使用CDN等外部资源的基础(嵌入的资源本身在加载后,其执行环境受目标源的限制,只能访问自己的源)
- 允许提交表单:向不同源提交表单数据是允许的,但浏览器无法读取目标服务器返回的响应内容(除非使用CORS等机制)
- 特例:WebSocket不受同源限制,postMessage专门设计的、安全的跨源通信API
关于CORS的HTTP标头都有哪些?
下面对于标头做一些简单的讲解(语法和使用方式等), 后续会做详细讲解
Access-Control-Allow-Origin
指示响应的资源是否可以被给定的来源共享。
语法:
js
// 对于不包含凭据的请求,服务器会以"*"作为通配符,从而允许任何来源的请求代码都具有访问资源的权限。
Access-Control-Allow-Origin: *
// 指定一个来源,如果包含多个来源,必须以与指定客户端匹配的来源来响应请求
// Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Origin: <origin>
// 指定来源为null
Access-Control-Allow-Origin: null
如果服务器未使用通配符,而是指定了明确的来源,那么为了向客户端表明服务器的返回会根据Origin请求标头有所不同,必须在Vary响应标头中包含Origin
makefile
Access-Control-Allow-Origin: https://example.com
Vary: Origin
Access-Control-Allow-Credentials
语法:
yaml
Access-Control-Allow-Credentials: true
指示当请求的凭据标记为 true 时,是否可以暴露对该请求的响应给脚本。
响应标头高速浏览器服务器是否允许HTTP跨源请求携带凭据。
凭据包括cookie、TLS客户端证书,或包含用户名和密码的认证标头。默认情况下,这些凭据不会在跨源请求中发送,因为这样做可能会使站点容易受到跨站请求伪造攻击
预检请求(下文会介绍)不会包含凭据,但是如果服务器将此标头设置为true的话,那么实际请求将包含凭据
如果请求未经过预检,则请求包含凭据,就要将此标头设置为true
Access-Control-Allow-Headers
用在对预检请求的响应中,指示实际的请求中可以使用哪些 HTTP 标头。
语法:
js
// 支持的请求标头名称。此标头可以列出任意数量的请求标头,用逗号分隔
// 自定义: Access-Control-Allow-Headers: X-Custom-Header
// 多个:Access-Control-Allow-Headers: X-Custom-Header, Upgrade-Insecure-Requests
Access-Control-Allow-Headers: [<header-name>[, <header-name>]*]
// 仅在无凭证的请求(即不包含HTTP cookie或HTTP认证信息的请求)中视为特殊的通配符值。带有凭证的请求中,通配符不具有特殊语义。
Access-Control-Allow-Headers: *
Access-Control-Allow-Methods
指定对预检请求的响应中,哪些 HTTP 方法允许访问请求的资源。 响应标头指定了在响应预检请求时访问资源所允许的一个或多个方法。
语法:
js
// 一个以逗号分割,表示允许使用的HTTP请求方法的列表
// Access-Control-Allow-Methods: GET, POST
Access-Control-Allow-Methods: <method>, <method>, ...
// 仅在无凭据的请求中视为通配符,有凭据的请求中无特殊语义
Access-Control-Allow-Methods: *
Access-Control-Expose-Headers
通过列出标头的名称,指示响应中的哪些标头可以暴露给脚本。
语法:
js
// 允许客户端从响应中访问0个或多个使用逗号分隔的标头名称列表。这些标头是对列入CORS白名单的请求标头的补充
Access-Control-Expose-Headers: [<header-name>[, <header-name>]*]
//
Access-Control-Expose-Headers: *
CORS白名单标头:
- Cache-Control
- Content-Language
- Content-Length
- Content-Type
- Expires
- Last-Modified
- Pragma
可以使用此请求头扩展CORS白名单标头
Access-Control-Max-Age
指示预检请求的结果能被缓存多久。
js
// 结果可被缓存的最大秒数,以无符号非整数表示,默认为5秒,各浏览器上限时间有所不同
// Access-Control-Max-Age: 600
Access-Control-Max-Age: <delta-seconds>
Access-Control-Request-Headers
用于发起一个预检请求,告知服务器正式请求会使用哪些 HTTP 标头。服务器对应的Access-Control-Allow-Headers,表示对浏览器的请求标头的回应
语法:
js
// 在请求中包含唯一的、以逗号分隔的小写HTTP标头的排序列表
// Access-Control-Request-Headers: content-type,x-pingother
Access-Control-Request-Headers: <header-name>,<header-name>,...
Access-Control-Request-Method
请求标头会由浏览器在发出预检请求时使用,通知服务器在实际请求中发出时会采用哪种HTTP方法。此标头是必须的,因为预检请求总是采用OPTIONS方法,这与实际请求所用的方法并不相同
js
// 一种HTTP请求方法,例如GET、POST或者DELETE
// Access-Control-Request-Method: POST
Access-Control-Request-Method: <method>
Origin
表示了请求的来源(协议、主机、端口)
如果一个用户代理需要请求一个页面中包含的资源,或者执行脚本中HTTP请求(fetch),那么该页面的来源(Origin)就可能被包含在这次请求中
js
// 请求的来源是隐私敏感的,或者是HTML规范定义的不透明来源
Origin: null
// 请求所使用的协议,通常是HTTP协议或者它的安全版本
Origin: <scheme>://<hostname>
// hostname -- 源站的域名或IP地址
// port -- 可选,服务器正在监听的端口号。缺省为服务的默认端口
Origin: <scheme>://<hostname>:<port>
Origin与Referer标头类似,但Origin不会暴露URL中的path部分,而且其可以为null值,其用于为源站的请求提供安全上下文,
Timing-Allow-Origin
指定允许查看通过资源时间 API获取的属性值的来源,否则由于跨源限制,这些属性值会被报告为零。
响应头 Timing-Allow-Origin 用于指定特定站点,以允许其访问Resource Timing API提供得相关信息,否则这些信息会由于跨源限制将被报告为零
语法:
js
// 服务器可以以 * 作为通配符,从而允许所有域都具有访问定时信息的权限
// 如果需要允许任何资源都可以看到的计时信息,则设置值为通配符
Timing-Allow-Origin: *
// 指定一个可以访问资源的URI。你也可以通过逗号隔开,指定多个URI
// 如果允许指定源查看你的计时信息,可以设置为
// Timing-Allow-Origin: https://example.com
Timing-Allow-Origin: <origin>[, <origin>]*
扩展:
Resource Timing API:为网络事件生成有高分辨率时间戳的资源加载时间线,并提供了资源大小和资源类型。
通过Resource Timing API可以获取和分析应用资源加载的详细网络计时数据,应用程序可以使用时间度量标准来确定加载特定资源所需要的时间,比如 XMLHttpRequest、SVG、图片,或者脚本。
预检请求和普通请求
CORS的基本理念就是只要服务器明确表示允许,则校验通过,服务器明确拒绝或没有表示,则校验不通过
所以CORS解决跨域,必须要保证服务器是自己人
CORS将请求分为两类:简单请求 和预检请求
对不同种类的请求它的规则有所区别
所以要理解CORS,首先要理解它是如何划分请求的
简单请求
若请求满足所有下述条件,则该请求就是简单请求:
- GET、POST、HEAD请求方法
- 除了被用户代理自动设置的标头字段,允许人为设置符合Fetch规范的安全字段:Accept、Accept-Language、Content-Language、Content-Type、Range(只允许简单的范围标头值 如:bytes=256-或者bytes=127-255)
- 如果有Content-Type,则它的值必须是
text/plain
、multipart/form-data
、application/x-www-form-urlencoded
三者之一
例子:
js
// 前端 JavaScript (在 https://www.example.com 上运行)
fetch('https://api.another-site.com/data', {
method: 'POST', // 允许的方法
headers: {
'Content-Type': 'application/x-www-form-urlencoded', // 允许的 Content-Type
// 不能添加自定义头部如 'X-Custom-Header'!否则会触发预检。
},
body: 'key1=value1&key2=value2' // 格式匹配 Content-Type
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
预检请求
只要不是简单请求,均为预检请求
例子:
js
// 前端 JavaScript (在 https://www.example.com 上运行)
fetch('https://api.another-site.com/user/123', {
method: 'DELETE', // 非简单方法 -> 触发预检
headers: {
'Content-Type': 'application/json', // 非简单 Content-Type -> 触发预检
'X-Custom-Header': 'somevalue' // 自定义头部 -> 触发预检
},
body: JSON.stringify({ reason: 'inactive' }), // 数据
credentials: 'include' // 需要携带凭据(如 Cookies)
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
CORS常见的错误以及解决方案
1. 缺少 Access-Control-Allow-Origin
头
控制台报错:No 'Access-Control-Allow-Origin' header is present on the requested resource.
解决:后端使用nodejs/express示例
javascript
app.use((req, res, next) => {
res.header("Access-Control-Allow-Origin", "https://your-frontend.com"); // 明确指定源
next();
});
2. 凭证请求被拒绝
The value of the 'Access-Control-Allow-Credentials' header is '' which must be 'true' when the request's credentials mode is 'include'
解决:
js
app.use((req, res, next) => {
res.header("Access-Control-Allow-Origin", "https://your-frontend.com");
res.header("Access-Control-Allow-Credentials", "true"); // 关键设置
next();
});
3. 预检请求(OPTIONS)失败
Response to preflight request doesn't pass access control check
解决:
js
// 后端修复(处理 OPTIONS 请求):
app.use((req, res, next) => {
// ...其他头设置...
res.header("Access-Control-Allow-Headers", "X-Custom-Header, Content-Type");
// 显式处理 OPTIONS 请求
if (req.method === "OPTIONS") {
return res.status(200).end();
}
next();
});
4. 不允许的HTTP方法
Method PUT is not allowed by Access-Control-Allow-Methods
js
app.use((req, res, next) => {
res.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE"); // 明确允许的方法
next();
});
通用解决方案配置
前端解决方案 vite 为例,仅限开发环境
javascript
// vite.config.js
export default {
server: {
proxy: {
"/api": {
target: "https://api.example.com",
changeOrigin: true,
rewrite: path => path.replace(/^/api/, "")
}
}
}
};
nodejs/express 解决方案:
js
const express = require('express');
const app = express();
// 关键中间件
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', 'https://www.your-awesome-app.com'); // 替换为你的前端域名或谨慎使用 '*'
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.header('Access-Control-Allow-Credentials', 'true'); // 如果需要凭证
// 处理预检请求
if (req.method === 'OPTIONS') {
res.sendStatus(200);
} else {
next();
}
});
nginx解决方案:
shell
location /api/ {
# 核心CORS配置
add_header 'Access-Control-Allow-Origin' 'https://www.your-awesome-app.com' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
# 处理预检请求
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain; charset=utf-8';
add_header 'Content-Length' 0;
return 204;
}
# 代理到你的应用服务器
proxy_pass http://your_backend_app;
}
以上是我关于CORS的一些整理分享,希望对各位有所帮助,查漏补缺,有什么意见或不足之处可以私信我指出,感谢!