CORS跨域问题详解
在前后端分离开发中,我们常常遇到一个令人头疼的问题——跨域(CORS,Cross-Origin Resource Sharing)。明明接口地址、请求方法都没问题,却无法访问后端服务。
什么是跨域
跨域(Cross-Origin)指的是当前网页向与其协议、域名或端口三者中任意一个不同的目标发起的请求。
如果请求的资源不属于同源范围(协议、域名、端口三者中有任意一个不同),浏览器会根据同源策略限制该跨域请求。需要注意的是:
- 同源策略是浏览器的安全机制,不是服务器的限制
- 不同浏览器对同源策略的具体实施可能存在细微差异
- 该问题的解决需要后端服务器配置CORS响应头来允许跨域请求,这涉及前后端的协作
为什么会有同源策略
浏览器为了防止恶意网页利用用户的浏览器身份获取其他网站的敏感数据,实施了同源策略。主要防护以下几类威胁:
- CSRF(跨站请求伪造):恶意网站通过浏览器自动携带的 Cookie,以用户身份向已登录网站发起未经授权的请求。
- 数据泄露:恶意网页尝试读取用户在其他网站的敏感信息,如 Cookie、LocalStorage、SessionStorage 等。
- 内网扫描:当用户在公司内网访问外部恶意网站时,该网站可能尝试扫描和攻击内网资源。
注意:同源策略是浏览器的安全机制,不是服务器端的限制;不同浏览器对同源策略的实现可能存在细微差异。跨域问题需要后端在响应头中正确设置 CORS,以允许浏览器跨域访问。
浏览器如何处理跨域请求
当浏览器检测到当前页面发起的请求与页面源不同,会根据请求类型采取不同的处理方式。
CORS请求分类:简单请求与预检请求
简单请求的定义
满足以下全部条件的请求被视为“简单请求”:
- 方法:仅限
GET
、POST
、HEAD
。 - Content-Type:仅限
application/x-www-form-urlencoded
、multipart/form-data
、text/plain
。 - 请求头:仅限
Accept
、Accept-Language
、Content-Language
、Content-Type
(前述三种值),不允许自定义头。
简单请求的处理流程
- 浏览器直接发送请求,同时添加
Origin
头部 - 服务器处理请求并返回响应
- 浏览器检查响应中的
Access-Control-Allow-Origin
头部 - 如果允许当前源访问,则将响应交给JavaScript;否则抛出CORS错误
预检请求(Preflight Request)
当请求不满足“简单请求”条件时,浏览器会先发送一个 OPTIONS
预检请求,确认服务器是否允许实际请求。
触发预检请求的情况
- 使用非简单请求方法(如
PUT
、DELETE
、PATCH
) - 使用自定义请求头(如
Authorization
、X-API-Key
) Content-Type
为application/json
等非简单类型。
预检请求示例
1 | OPTIONS /api/data |
预检响应示例
1 | 200 OK |
Tip:
Access-Control-Max-Age
表示预检结果的缓存时间(单位:秒),可减少预检次数。
携带凭据的请求
当请求需要携带cookie或其他凭据时:
1 | fetch(url, { |
浏览器会进行额外检查:
- 服务器必须返回
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin
不能是通配符*
,必须是具体的源- 如果检查不通过,浏览器会阻止JavaScript访问响应数据,并抛出CORS错误
常见跨域场景举例
场景 | 示例 | 跨域原因 |
---|---|---|
子域名不同 | 页面:https://www.example.com 接口:https://api.example.com |
域名不同 |
端口不同(开发环境) | 页面:http://localhost:3000 接口:http://localhost:8080 |
端口不同 |
协议和域名都不同 | 页面:http://insecure.com 接口:https://secure.com |
协议、域名都不同 |
第三方 API 调用 | 页面:https://my-app.com 接口:https://api.weather.com |
域名不同 |
本地文件访问 | 页面:file:///C:/app/index.html 接口:http://localhost:8080 |
协议(file:// vs http://) |
🔄 跨域解决方案对比
方案 | 原理与适用场景 | 限制与注意事项 |
---|---|---|
1. JSONP | 利用 <script> 标签能够跨域加载脚本的特性,将接口返回的数据包装成函数调用。 |
仅支持 GET 请求,且无法处理复杂交互;已被现代 CORS 替代。 |
2. 反向代理 | 前端服务器(如 Nginx)接收前端请求,再转发到后端,浏览器只与同源服务器通信。 | 配置简单、安全性高,适用于生产环境;需维护代理服务器。 |
3. iframe + postMessage | 在不同源页面间通过 postMessage 进行数据通信。 |
实现较繁琐,主要用于嵌入第三方或多个域之间的复杂通信场景。 |
4. 后端设置 CORS | 后端在响应头中配置 Access-Control-Allow-Origin 、Access-Control-Allow-Methods 、Access-Control-Allow-Headers 等。 |
通用、标准;可细粒度控制源、方法与头部;需后端框架或中间件支持。 |
5. WebSocket | 基于 TCP 的双向通信协议,一旦建立连接后不受同源策略限制。 | 需要 WebSocket 服务端支持;不适合所有应用场景。 |
CORS与CSRF的安全关系澄清
“当后端没有设置CORS的时候,数据岂不是轻易的就被窃取了”
实际上,CORS 不是安全机制,而是浏览器的访问控制:
- 跨域请求:浏览器仍会发送请求到服务器(可抓包验证)。
- 响应处理:若未授权,浏览器会阻止前端脚本读取响应,但请求已执行。
- 真正规避风险:应对 CSRF 攻击,需使用 CSRF Token、SameSite Cookie 等机制。
CSRF攻击三要素
- 用户已登录目标网站(存在有效会话)
- 网站依赖Cookie验证身份(无额外防护)
- 用户访问恶意页面(触发跨域请求)
CSRF 防御策略
1. CSRF Token
原理:后端在用户会话中生成一个随机、不可预测的 token,并将其以隐藏字段或响应接口返回给前端;前端将此 token 附加在每次敏感请求中,后端校验请求中 token 与会话存储的值是否一致。若一致,则请求合法。
1 | # Django. views.py |
2. SameSite Cookie 属性
原理:通过设置 SameSite
属性控制浏览器是否在跨站请求时携带 Cookie。
1 | Set-Cookie: sessionid=abcd1234; SameSite=Lax; Secure; HttpOnly; Path=/ |
模式 | 行为说明 |
---|---|
Strict | 仅当前站点导航时携带 Cookie,完全阻止跨站请求发送 Cookie。 |
Lax | 允许顶级跨站 GET/HEAD 导航携带 Cookie(推荐用于登录态)。 |
None | 始终携带 Cookie,适用于复杂跨站场景,需配合 Secure 。 |
优缺点:
- 优点:无需额外前端代码,依赖浏览器实现;
- 缺点:对非导航型跨站请求(如 AJAX POST)无效;部分老旧浏览器兼容性欠佳。
3. 验证 Origin
/ Referer
头
原理:后端检查请求头中的 Origin
或 Referer
字段,仅允许来自可信域名的请求。此方式不依赖 token,但要确保所有请求都出现在同浏览器策略下。
1 | # Django |
然后在 settings.py 中添加中间件:
1 | MIDDLEWARE = [ |
注意:某些请求(如表单提交)可能没有 Origin
,建议同时检查 Referer
。
4. 🔐 双重提交验证(Double Submit Cookie)
原理:前端和后端各自持有相同的随机 token。后端将 token 写入 Cookie,前端也读取该 Cookie 并在请求头中携带;后端只需比较 Cookie 与头部中 token 是否一致,无需维护服务端会话状态。
1 | // 前端示例 |
优缺点:
- 优点:适合无状态架构,无需会话存储;
- 缺点:Token 泄露风险更高,需与 HTTPS 强绑定。
5. CAPTCHA / 二次确认
对高风险操作(如交易、密码修改等),可在操作前要求用户输入 CAPTCHA、短信验证码或重新输入密码,以二次确认用户意图。
1 | <!-- 示例:操作前弹出验证框 --> |