你真的了解CORS吗

170 阅读9分钟

你真的了解CORS吗

CORS全称是"跨域资源共享"(Cross-origin resource sharing),这个对于做过前端开发的一定不陌生,比如当我们像利用AJAX 或者 Fetch 去请求非本项目的资源的时候,就会报 CORS 错误,如下图:

image-20251023221802540.png

什么是跨域

概念

在讲解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会发现什么也没有。

image-20251023224038320.png

这是为什么呢?报错不是显示请求被阻止了吗?为啥请求状态码是200呢?

其实请求已经发出了,服务端也成功返回结果了,但是浏览器不让前端 JS 读取响应结果

我们接着看 Response Headers,发现对比其他请求,少了一个access-control-allow-origin字段,而这个就是浏览器进行请求屏蔽的原因。没有这个头或者这个头不包含当前域名,浏览器就会阻止 JS 去获取请求内容。

所以只要如服务端正确返回了access-control-allow-origin字段,请求就不会被拦截,也就不会有CORS错误了。解决CORS,主要还是服务器那边的处理。

CORS 的工作流程

所有的跨域 HTTP 请求,在 CORS 机制下可以分为两大类:简单请求复杂请求(预检请求)

浏览器中跨域请求可以划分为两个阶段请求阶段响应阶段。想要理解清楚跨域请求,则需要请求的不同阶段进行分析,是请求阶段——前端请求被拦截,还是响应阶段——浏览器拦截了响应数据。

简单请求与复杂请求的划分

浏览器认为足够安全的请求,就不会发送预检请求,不发送预检请求的请求就是简单请求。相反,如果是浏览器认为有“潜在风险” 的请求,就会先发送预检请求,预检请求通过后才会发送真实的请求,这样的请求就是复杂请求

简单请求必须满足以下条件

  • 请求方 法必须是GETPOSTHEAD 这三种之一。
  • 请求头 必须是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

withCredentialscredentials 是浏览器请求阶段是否请求携带 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-TypeAuthorization

  • 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,此时浏览器直接使用原始字节流

image-20251024185250591

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-HeaderAuthorization参数

    • 默认允许读取的参数(简单响应头)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"}