你真的了解CORS吗
CORS全称是"跨域资源共享"(Cross-origin resource sharing),这个对于做过前端开发的一定不陌生,比如当我们像利用AJAX 或者 Fetch 去请求非本项目的资源的时候,就会报 CORS 错误,如下图:
什么是跨域
概念
在讲解CORS前,需要了解什么是跨域。
一个url是由:协议、域名、端口 三部分组成。如https://baidu.com:8080中协议就是https,域名就是baidu.com,端口是8080。
当一个请求url的协议、域名、端口三者之间的任意一个与当前页面url不同即为跨域。
产生原因
跨域实际上是浏览器保护一个页面不受外部攻击的核心策略。所谓同源(即在同一个域)就是两个页面具有相同的协议(protocol)、主机(host)和端口号(port) 。它会阻止一个域的 JS 脚本和另外一个域的内容进行交互,如果缺少了同源策略,浏览器很容易受到 XSS、CSFR 等攻击。
跨域资源共享 CORS
CORS是一个W3C标准,它规定了两个不同域之间的网站如何安全地访问对方的资源。
前面所说的fetch请求显示的跨域问题,我们继续剖析,去了解浏览器是如何运作CORS的。
打开检查器,查看网络请求状况,会发现请求实际上是成功的,请求状态字节是200,但是查看Response会发现什么也没有。
这是为什么呢?报错不是显示请求被阻止了吗?为啥请求状态码是200呢?
其实请求已经发出了,服务端也成功返回结果了,但是浏览器不让前端 JS 读取响应结果。
我们接着看 Response Headers,发现对比其他请求,少了一个access-control-allow-origin字段,而这个就是浏览器进行请求屏蔽的原因。没有这个头或者这个头不包含当前域名,浏览器就会阻止 JS 去获取请求内容。
所以只要如服务端正确返回了access-control-allow-origin字段,请求就不会被拦截,也就不会有CORS错误了。解决CORS,主要还是服务器那边的处理。
CORS 的工作流程
所有的跨域 HTTP 请求,在 CORS 机制下可以分为两大类:简单请求 和 复杂请求(预检请求) 。
浏览器中跨域请求可以划分为两个阶段:请求阶段、响应阶段。想要理解清楚跨域请求,则需要请求的不同阶段进行分析,是请求阶段——前端请求被拦截,还是响应阶段——浏览器拦截了响应数据。
简单请求与复杂请求的划分
浏览器认为足够安全的请求,就不会发送预检请求,不发送预检请求的请求就是简单请求。相反,如果是浏览器认为有“潜在风险” 的请求,就会先发送预检请求,预检请求通过后才会发送真实的请求,这样的请求就是复杂请求。
简单请求必须满足以下条件:
- 请求方 法必须是
GET、POST、HEAD这三种之一。 - 请求头 必须是
Accept|Accept-Language|Content-Language|Content-Type|Last-Event-ID(SEE相关-服务端主动推送消息给浏览器)这几种 “安全头" 之一,不能包含自定义的Header。 - 如果有
Content-Type值必须是text/plain|multipart/form-data|application/x-www-form-urlencoded这几种。
不满足上面的任意条件,就属于非简单请求——复杂请求
简单请求 CORS 的工作情况
简单请求,浏览器会直接发送,不会进行预检,只要服务端能返回对应的Access-Control-Allow-Origin即可。
如果是跨域请求,则浏览器会自动在头部信息中添加 Origin 字段,来表明本次请求的源来自哪个源(协议 + 域名 + 端口),服务端通过这个值来判断是否同意这次请求。
服务端不接受这个源,则返回的信息头没有包含Access-Control-Allow-origin字段,浏览器检测到没有字段,则执行拦截响应内容。
服务端接受这个源,则请求头中会携带access-control-allow-origin字段,如果前端设置了withCredentials: true(XHR) 或credentials: "include"(fetch),则请求头会多加一个字段Access-Control-Allow-Credentials: true。
| 条件 | 备注 |
|---|---|
| Access-Control-Allow-Origin 不存在或不匹配 | JS 拿不到响应,浏览器拦截 |
| Access-Control-Allow-Credentials: true 缺失 + withCredentials=true | 浏览器仍然拦截响应,JS 拿不到 |
withCredentials 和 credentials
withCredentials 和 credentials 是浏览器请求阶段是否请求携带 cookie 的判断依据。credentials 是对于 fetch 请求而言的,withCredentials 是对于 XMLHttpRequest而言。
| credentials 选项 | 行为 | 说明 |
|---|---|---|
omit | 不带 cookie | / |
same-origin | 仅同源请求带 cookie | 常用于前后端同域的情况——fetch 默认值 |
include | 所有请求(包括跨域)都带 cookie | 用于跨域请求携带登录状态 |
| withCredentials 选项 | 行为 | 说明 |
|---|---|---|
True | 跨域不带 cookie | 默认(XMLHttpRequest 默认值) |
False | 跨域带 cookie | 常用于前后端同域的情况 |
复杂请求 CORS 的工作情况
复杂请求相对于简单请求,会多一个预检请求(OPTIONS)去判断服务端是否允许这次请求,简单说就是先预检后请求。
OPTIONS
options请求又称预检请求,它的作用是:询问服务器“支持哪些请求方式或通信选项”,而不是获取或提交资源。 它本身不会对服务器数据造成影响。
CORS 预检请求(Preflight Request)——99% 的 OPTIONS 请求场景,不需要前端手动编写
OPTIONS请求带来的四大好处:
-
安全隔离(防止恶意跨站请求)
- 防止”恶意网页“在用户浏览器上下文中,利用用户的 Cookie/Session/Token 去访问另一个受保护域的接口。
- 仅对于浏览器有效,CORS(跨域资源共享)是一个浏览器级安全策略, 属于 前端运行环境(浏览器)实现的保护机制,不是HTTP协议本身限制。
-
明确服务端权限声明(可配置、可控) ,服务器完全掌控跨域策略,而不是由浏览器随意决定。
-
预检不会有副作用(安全“试探”),OPTIONS请求是无副作用的请求类型:不会修改服务器数据,不执行业务逻辑。
-
允许浏览器缓存安全结果(减少后续开销),例如:预检返回头携带
Access-Control-Max-Age: 600表示:“浏览器在 10 分钟内,遇到相同源 + 相同请求配置,可以跳过预检。”
总结:OPTIONS 预检请求可防止跨站滥用、保护用户数据安全、让服务端控制权限、并支持结果缓存。
OPTIONS相关请求头字段介绍
发送方携带(浏览器)
Accept: */*:用来告诉服务器,能接受什么数据类型
| 值 | 含义 |
|---|---|
text/html | 希望返回 HTML 页面 |
application/json | 希望返回 JSON 数据 |
image/png | 希望返回 PNG 图片 |
*/* | 表示可以接受任意类型(最宽松) |
Access-Control-Request-Headers是浏览器告诉服务器的 “请求意图”
示例:Access-Control-Request-Headers: Content-Type, Authorization 表示请求里会带Content-Type和Authorization
Access-Control-Request-Method告知服务器实际请求所使用的 HTTP 方法Accept-Encoding告知服务器,当前环境支持的压缩算法。
处理方(服务器)
-
Access-Control-Allow-Origin是服务器允许请求的源。一般只返回请求的源,不设置为*,虽然这个符合CORS规范定义,其目的是为了:- 防止敏感信息泄露(*表示所有源均可访问,设置意味浏览器将对所有请求不做拦截);
- 无法使用带凭证(Cookie)的请求(CORS 标准明确禁止:当
Access-Control-Allow-Origin: *时,不能同时返回Access-Control-Allow-Credentials: true。) - 不利于审计与管理(*表示完全开发,安全审计人员很难判断哪些域名能访问你的服务器)
-
Access-Control-Allow-Headers是服务器的“批准清单” ,对应Access-Control-Request-Headers的请求意图。Access-Control-Allow-Headers: *是被CORS标准明确禁止的,浏览器必须拿到明确的允许列表,才能决定是否发送请求。 -
Access-Control-Allow-Methods是服务器允许的请求方法,与**Allow的区别是当前接口请求所允许的方法**。 -
Access-Control-Max-Age是服务器告知浏览器缓存OPTIONS请求的时间,单位s -
Content-Encoding是服务端告知浏览器,当前使用的压缩方法。如果服务器没有采用任何压缩算法,则不加Content-Encoding,此时浏览器直接使用原始字节流。
CORS请求相关的头部信息
-
Access-Control-Allow-Origin:该字段是必须的。它的值要么是请求时
Origin字段的值,要么是一个*,表示接受任意域名的请求。 -
Access-Control-Allow-Credentials:如果出现这个字段,则
Access-Control-Allow-Origin不能为*,否则无法使用带凭证(Cookie)的请求 -
Access-Control-Expose-Headers:如果想要拿到响应头上的参数,且这些参数不在前端默认允许读取的参数之中,就要设置这个在参数。例如:
Access-Control-Expose-Headers: X-My-Custom-Header, Authorization表示允许读取X-My-Custom-Header和Authorization参数- 默认允许读取的参数(简单响应头) :
Cache-Control|Content-Language|Content-Type|Expires|Last-Modified|param之中
- 默认允许读取的参数(简单响应头) :
绕过CORS的常见方法
服务端允许跨域
服务端可以从根本上解决处理跨域问题,只需要正确处理跨域请求,返回合法的 CORS 响应头。
Access-Control-Allow-Origin: <前端域名> 或 *
Access-Control-Allow-Credentials: true (如果带 cookie)
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
使用代理服务器
前端部署可以通过 ngnix配置 + 构建工具实现反向代理,实现通过同源代理转发到目标服务器。
JSONP (只支持 GET - 老方法)
利用在 html 的 <script> 标签,实现跨域。这是利用了浏览器允许跨域引用JavaScript资源。
整体流程:
- 浏览器请求 ->
<script src="跨域URL?callback=foo"> - 服务器返回 -> foo({数据})
- 前端 foo 函数被调用 -> 拿到数据
前端实现:
// 定义回调函数
function handleData(data) {
console.log("JSONP 返回数据:", data);
}
// 动态创建 <script> 标签
const script = document.createElement('script');
script.src = "https://api.example.com/data?callback=handleData";
document.body.appendChild(script);
后端实现:
from flask import Flask, request
app = Flask(__name__)
@app.route("/data")
def data():
callback = request.args.get("callback")
data = {"name": "Alice", "age": 25}
# 返回 JavaScript 代码调用回调函数
return f"{callback}({data})", 200, {"Content-Type": "application/javascript"}