上周末我们系统做上线前联调,前端同事跑过来的时候脸色不太好看:“上传附件到华为云OBS一直报错,控制台红字刷了一屏,什么has been blocked by cors policy,是不是你桶的权限给我配错了?”我看了一眼报错,心里基本就有数了——又是CORS。说实话,对接华为云OBS做附件上传,十次联调里有八次会栽在这个CORS上,剩下的两次是签名和权限问题。这东西原理不复杂,但坑是真不少,尤其是前端直传场景,配置错了连个像样的错误提示都没有,全靠自己猜。
这篇文章我就结合这次实际对接的完整过程,把华为云OBS上传附件时CORS报错的来龙去脉、配置方法、常见坑位一次说清楚。不管你是后端被前端追着问,还是前端自己排查了半天没头绪,只要你负责对接OBS上传,这篇文章应该能帮你省下至少半天时间。
1. 先搞明白CORS到底是什么,为什么上传附件会栽在这里
1.1 从一次“莫名其妙”的报错说起
先说这次的实际现象。我们的场景是浏览器端直传附件到OBS,前端用的是华为云OBS的JavaScript SDK,流程大致是:后端先通过临时凭证接口签发一个带权限的临时AK/SK和securityToken,前端拿这个临时凭证初始化OBS客户端,然后直接把文件PUT到指定桶。这个方案在本地开发环境跑得好好的,一部署到测试环境就报错,错误信息是这样的:
text复制Access to XMLHttpRequest at 'https://bucket.obs.cn-north-4.myhuaweicloud.com/xxx/file.pdf'
from origin 'https://app.example.com' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.
前端同事的第一反应是“桶的权限是不是没配”,第二反应是“是不是后端签名没签对”。我检查了一圈,权限没问题、签名没问题、网络也没问题,最后把问题定位到了桶的CORS规则上——测试环境的桶压根没配置CORS规则,而本地环境用的是别的桶,配置是齐全的,所以一直没暴露。
这个案例特别典型,因为它反映了一个普遍认知偏差:很多人以为CORS是服务端接口层面的东西,跟对象存储这种“资源型”服务没关系。实际上,浏览器把OBS的PUT请求当作一次跨域XMLHttpRequest来处理,只要你的页面域名跟桶的访问域名不一致,就必须通过CORS机制来放行,否则浏览器会直接拦截响应,哪怕OBS服务器已经成功保存了文件。
1.2 同源策略与跨域的基本概念
CORS全称是Cross-Origin Resource Sharing,跨域资源共享。要理解它,得先理解浏览器的同源策略(Same-Origin Policy)。同源的定义是协议、域名、端口三者完全一致,比如https://app.example.com和https://api.example.com不是同源,http://和https://也不是同源,域名相同但端口不同同样不是同源。
同源策略是浏览器最基础的安全机制之一。它的存在意味着,https://app.example.com页面里的JavaScript代码,默认情况下不能自由读取https://bucket.obs.cn-north-4.myhuaweicloud.com这个域名下的资源响应。这个限制对普通图片加载、<script>标签引入是没有影响的,因为那些是标签发起的请求,不受同源策略约束。但XMLHttpRequest和fetch发起的请求是严格受限的。
这里有个容易混淆的点:同源策略限制的是“读取响应”,不是“发出请求”。也就是说,你的跨域请求其实发出去了,服务器也收到了,甚至文件可能已经传上去了,但浏览器拿到响应后发现没有携带允许跨域读取的响应头,就会把响应“扔掉”,并且在控制台报CORS错误。这就是为什么有时候你看到CORS报错,但去OBS桶里一看,文件居然已经传上去了。第一次遇到这个情况的人真的会一脸懵。
1.3 简单请求和预检请求到底怎么区分
CORS机制下,跨域请求被分为两类:简单请求(Simple Request)和预检请求(Preflight Request)。
简单请求需要同时满足以下条件:
- 请求方法只能是GET、HEAD、POST
- 请求头只能包含一些安全的头部字段,比如Accept、Accept-Language、Content-Language、Last-Event-ID、Content-Type(且Content-Type的值仅限于
application/x-www-form-urlencoded、multipart/form-data、text/plain) - 不能在XMLHttpRequest中使用
XMLHttpRequestUpload对象注册事件监听器 - 请求中没有使用
ReadableStream对象
只要有一个条件不满足,浏览器就会先发送一个OPTIONS请求,也就是预检请求,去询问服务器:“我接下来要发起这样一个跨域请求,你允许吗?”服务器需要通过响应头明确告诉浏览器允许的方法、允许的来源、允许的请求头,浏览器才会真正发送后续的业务请求。
OBS的上传场景几乎全是非简单请求。为什么?因为OBS对象上传的Content-Type往往是application/octet-stream,而且签名认证需要在请求头里携带Authorization字段,加上x-amz-date、x-amz-security-token这类自定义头——这些都不在简单请求的白名单里。所以,你在浏览器Network面板里通常会看到一次OPTIONS请求,紧接着才是真正的PUT或POST请求。如果OPTIONS阶段就被拦了,后面的请求根本不会发出去;如果OPTIONS通过了但PUT阶段响应头不对,浏览器同样会拦截。
理解了这一层,很多CORS报错的排查思路就清晰了:不是笼统地看“有没有配CORS”,而是要看预检请求和实际请求这两个阶段分别有没有拿到正确的响应头。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 华为云OBS的CORS规则配置,照着做就行
2.1 控制台配置CORS规则的完整步骤
华为云OBS的CORS规则配置在桶的“权限”菜单下,不在“基本信息”里,第一次找可能要多翻一下。具体路径是:进入OBS控制台 → 找到对应的桶 → 点击桶名称进入详情页 → 左侧菜单选择“权限控制” → 找到“CORS规则”页签。
点击“创建”按钮后,会看到一个配置表单,主要字段有:
| 配置项 | 说明 | 推荐值 |
|---|---|---|
| 允许的来源(AllowedOrigin) | 允许跨域访问的来源域名,支持通配符* | 按照你的前端实际域名填写,如https://app.example.com |
| 允许的方法(AllowedMethod) | 允许的HTTP方法,支持GET、PUT、POST、DELETE、HEAD | 按实际使用勾选,上传场景至少勾选PUT、POST、GET、HEAD |
| 允许的请求头(AllowedHeader) | 允许携带的请求头,支持通配符* | 建议填写*,因为签名认证需要携带的Authorization等自定义头必须被显式允许 |
| 暴露的响应头(ExposeHeader) | 允许前端JavaScript读取的响应头 | 建议填写ETag,后面会解释为什么 |
| 缓存时间(MaxAgeSeconds) | 预检请求结果在浏览器中的缓存时间 | 建议设置3600秒 |
配置完成后点击“确定”,规则会立即生效,不需要重启任何服务。
2.2 几个关键参数的取值逻辑
这里有几个参数初看容易迷惑,实际踩过坑才知道背后的逻辑。
AllowedOrigin为什么尽量不要用*。 有些教程图省事直接填*,表示允许所有来源。但在OBS上传场景下,*有个很尴尬的问题:如果你在请求中携带了凭证信息(比如Authorization头),按照CORS规范,Access-Control-Allow-Origin不能回显为*,必须回显具体的来源域名。OBS的实际情况是,用*配置在一些严格场景下会直接导致预检失败,或者即使通过也会在前端报错。所以,如果你明确知道前端页面部署在哪个域名,就老老实实填具体域名。如果前端可能有多个环境(本地开发、测试环境、生产环境),就分别给每个环境建一条规则,OBS支持配置多条CORS规则,不用怕多。
AllowedHeader为什么建议填*。 OBS的签名机制要求请求头中携带Authorization和x-amz-*系列的自定义头。预检请求阶段,浏览器会发送一个Access-Control-Request-Headers头,列出实际请求将要携带的自定义头清单。服务器需要在Access-Control-Allow-Headers响应头中明确允许这些头。如果配置的AllowedHeader不包含这些自定义头,预检直接失败。填*是最省心的做法,OBS会把所有请求头都纳入允许范围,避免因为漏配某个头导致联调时反复踩坑。
ExposeHeader为什么要填ETag。 上传完成后,前端往往需要拿到OBS返回的ETag值,用于校验文件是否上传完整。但是CORS机制下,即使服务器返回了ETag响应头,浏览器默认也不会把非“安全响应头”(如Content-Type、Cache-Control等)暴露给前端JavaScript代码,必须通过ExposeHeader显式声明。不配置的话,前端拿到的响应对象里看不到ETag,可能会引发后续业务逻辑问题。虽然这不是报错型的坑,但属于典型的“不报错但功能不正常”问题,排查起来更隐蔽。
2.3 前端直传场景下的推荐配置
根据我们的实际使用经验,前端直传场景下,一个稳妥的CORS规则配置可以写成这样:
- 允许的来源:前端页面实际部署的域名,多个环境就配多条规则;如果没有条件限制,开发阶段可以先用
*,上线前再收敛 - 允许的方法:GET、PUT、POST、DELETE、HEAD
- 允许的请求头:
* - 暴露的响应头:
ETag - 缓存时间:3600
这个配置覆盖了最常见的上传、下载、删除、列举对象操作。如果你的业务里还需要前端直接调用OBS的其他功能,比如设置对象的Content-Type、复制对象等,方法可以继续往上加,OBS的CORS规则中的方法列表本身就是多选的。
注意:CORS规则配置是桶级别的,不是全局级别的。如果你有多个桶,每个桶都要单独配置。这个看起来是废话,但真的有人只配置了上传桶,下载桶没配,结果下载文件时又报一次CORS错,非常打击士气。
3. 对接OBS上传附件的两种常见技术方案
3.1 前端直传方案(浏览器SDK + 临时凭证)
这次我们采用的方案是前端直传,整体架构分三层:前端页面、后端服务、OBS桶。
流程是:
- 前端页面在用户选择文件后,先调用后端接口申请上传凭证
- 后端收到请求后,生成一个临时AK/SK和securityToken,设置好过期时间和权限范围(只允许PUT到指定桶的指定目录),返回给前端
- 前端用这个临时凭证初始化OBS客户端,调用
putObject方法,将文件以流的形式直接上传到OBS - 上传完成拿到ETag,再做后续业务处理
这种方案的核心价值在于,文件数据不经过应用服务器,服务器的带宽压力、磁盘压力、处理时间都被省掉了,上传速度基本等同于用户的公网带宽。而临时凭证的过期机制又保证了安全性,即使凭证在传输过程中泄露,攻击者也只能在有效期内操作指定路径下的对象,影响面可控。
前端代码的示意结构大致是这样的:
javascript复制import ObsClient from 'obs-js-sdk';
// 第一步:从后端获取临时凭证
const {
accessKeyId,
secretAccessKey,
securityToken,
bucket,
endpoint
} = await fetch('/api/obs/upload-token').then(res => res.json());
// 第二步:初始化OBS客户端
const obsClient = new ObsClient({
access_key_id: accessKeyId,
secret_access_key: secretAccessKey,
security_token: securityToken,
server: endpoint
});
// 第三步:执行上传
const file = document.getElementById('fileInput').files[0];
const result = await obsClient.putObject({
Bucket: bucket,
Key: `attachments/${Date.now()}_${file.name}`,
Body: file
});
console.log('上传完成,ETag:', result.ETag);
后端生成临时凭证时,实际用的是华为云的STS服务,核心是调用AssumeTemporaryCredential接口,代码大致如下:
javavascript复制// 这里用Node.js示意,其他语言SDK逻辑类似
const hcobs = require('@obs/esdk-obs-nodejs');
const obsClient = new hcobs.ObsClient({
access_key_id: '长期AK',
secret_access_key: '长期SK',
server: 'https://obs.cn-north-4.myhuaweicloud.com'
});
const result = await obsClient.createTemporaryCredential({
DurationSeconds: 900,
AccessKeyId: '长期AK',
SecretAccessKey: '长期SK'
});
生成临时凭证后,后端需要把返回的AccessKeyId、SecretAccessKey、SecurityToken和桶名、endpoint一起封装成接口响应返回给前端。endpoint的格式是https://obs.cn-north-4.myhuaweicloud.com,不同区域后缀不同,要注意和桶的实际区域匹配。
3.2 后端代理上传方案
与前端直传对应的,是后端代理上传方案。流程是:前端把文件上传到自己的应用服务器,应用服务器再通过OBS的SDK将文件转存到OBS。
这个方案的代码相对简单,前端就是一次普通的POST请求,后端用OBS SDK上传。整个过程发生在服务端到服务端之间,不涉及浏览器跨域问题,自然也不会触发CORS报错。这是它在“省心”维度上的最大优势。
但代价也很明显。文件先到应用服务器,再从应用服务器到OBS,相当于多了一次网络传输。如果文件比较大,或者用户量上来,服务器的带宽和磁盘会很快成为瓶颈。一台带宽只有10Mbps的ECS,同时被几十个人传文件,基本就卡死了。
3.3 两种方案如何选择
从CORS排查的角度看,我的建议是:如果你的场景允许,优先做前端直传,然后认真配置CORS规则。因为CORS的坑是有限的,配置一次摸透了,后面基本不会再出问题。而带宽和服务器压力的坑是持续的,随着业务增长会越来越明显。
不过也有例外。如果你们的内网环境有严格的安全策略,不允许前端直连对象存储的公网域名,或者文件需要经过病毒扫描、内容审核、格式转换等前置处理,那后端代理方案更合适。这种情况下CORS问题自然不存在,但你要为服务器带宽和磁盘预留足够的余量。
4. 常见问题与排查技巧实录
4.1 配置了CORS还是报错?先看这一张速查表
我把这次联调过程中遇到的和之前听过的问题整理了成一张速查表,遇到CORS报错可以按图索骥:
| 报错信息 | 可能原因 | 排查方向 |
|---|---|---|
| No 'Access-Control-Allow-Origin' header is present | 桶未配置CORS规则,或规则中的AllowedOrigin未包含当前域名 | 检查桶的CORS规则,确认来源域名精确匹配 |
| Response to preflight request doesn't pass access control check | 预检请求被拒绝 | 检查AllowedMethod是否包含实际使用的HTTP方法,AllowedHeader是否包含*或具体的自定义头 |
| No 'Access-Control-Allow-Headers' header is present | 预检请求中携带的自定义头未被允许 | 将AllowedHeader配置为* |
| Permission was denied for this request | OBS签名或权限问题,可能不是CORS本身 | 检查临时凭证的Policy是否授权了对应操作和资源路径 |
| 请求正常但前端拿不到ETag | ExposeHeader未配置ETag | 在CORS规则中补充ExposeHeader为ETag |
这个表格不是万能的,但覆盖了80%以上的问题。尤其是“Permission was denied”这条,很多人误以为是CORS问题,因为在浏览器控制台里它长的样子确实跟CORS报错很像,但本质上可能是签名过期、Policy不允许该操作,或者前端用了错误的region endpoint。排查时要先确认网络请求本身是否到达了OBS服务器、OBS服务器返回了什么状态码。如果返回的是403,大概率是权限问题;如果返回的是200但浏览器报CORS错,那才是CORS配置问题。
4.2 用curl和浏览器开发者工具一步步排查
CORS是浏览器层面的机制,它的报错信息只有在浏览器环境里才会出现。但我们可以用curl模拟浏览器的请求行为,绕过浏览器的拦截,直接看服务端的响应头。
排查的第一步是打开浏览器的开发者工具,切到Network面板,找到那条红色的请求,点开看Response Headers。如果响应头里有access-control-allow-origin,说明CORS规则生效了,那问题可能出在别的地方,比如预检请求没通过,或者请求本身没发出去。如果响应头里没有这个字段,基本可以确定桶的CORS规则没生效或没配置对。
第二步,用curl模拟一个预检请求,直接看服务端是怎么响应的:
bash复制curl -i -X OPTIONS \
'https://bucket.obs.cn-north-4.myhuaweicloud.com/attachments/test.txt' \
-H 'Origin: https://app.example.com' \
-H 'Access-Control-Request-Method: PUT' \
-H 'Access-Control-Request-Headers: authorization,content-type,x-amz-date,x-amz-security-token'
重点看响应头里有没有这三项:
text复制access-control-allow-origin: https://app.example.com
access-control-allow-methods: GET, PUT, POST, DELETE, HEAD
access-control-allow-headers: authorization,content-type,x-amz-date,x-amz-security-token
如果access-control-allow-origin的值跟你传的Origin不一致,说明AllowedOrigin里的域名和实际请求域名有差异,比如协议不同(http被跳到了https)、端口不同、或者域名带了多余的路径。如果access-control-allow-headers缺失,或者没有包含你实际携带的自定义头,就把AllowedHeader改成*再试一次。
第三步,确认预检请求通过后,再模拟实际PUT请求:
bash复制curl -i -X PUT \
'https://bucket.obs.cn-north-4.myhuaweicloud.com/attachments/test.txt' \
-H 'Origin: https://app.example.com' \
-H 'Authorization: OBS 你的签名信息' \
-H 'Content-Type: application/octet-stream' \
--data-binary 'test content'
这个请求如果返回200或204,并且响应头里有access-control-allow-origin和etag,那就说明服务器侧一切正常,问题基本出在前端代码或者SDK版本上。
4.3 几个容易忽略的坑
第一个坑是浏览器缓存。 CORS预检请求的结果会被浏览器缓存,缓存时间由MaxAgeSeconds决定。如果你改了OBS上的CORS规则,但浏览器还在用旧的缓存结果,就会出现“明明配置对了还是报错”的假象。遇到这种情况,打开一个无痕窗口测试,或者按Ctrl+Shift+Delete清掉浏览器缓存,基本就能验证是不是缓存问题。
第二个坑是Nginx或网关层二次转发。 如果前端不是直连OBS域名,而是先请求你们自己的Nginx,再由Nginx反向代理到OBS,那CORS规则就不仅要配在OBS桶上,还要在Nginx层面对OPTIONS请求做处理。因为浏览器看到的是你的Nginx域名,预检请求也会先到Nginx,OBS的CORS规则根本没机会生效。Nginx层需要在location配置里返回必要的CORS头,同时放行OPTIONS请求:
nginx复制location /obs/ {
if ($request_method = 'OPTIONS') {
add_header Access-Control-Allow-Origin '*';
add_header Access-Control-Allow-Methods 'GET, PUT, POST, DELETE, HEAD';
add_header Access-Control-Allow-Headers '*';
add_header Access-Control-Max-Age 3600;
return 204;
}
proxy_pass https://bucket.obs.cn-north-4.myhuaweicloud.com/;
}
这种方案其实是“双层的CORS配置”:一层在Nginx,另一层在OBS。Nginx返回的CORS头负责让浏览器信任你的网关域名,OBS的CORS头负责让Nginx转发请求时不被OBS拒绝。两边的AllowedOrigin都要包含实际的前端域名。
第三个坑是“本地跑得好好的,部署到服务器就报错”。 大概率是本地环境的域名恰好跟OBS桶的某个CORS规则匹配了,比如本地用了localhost:8080,而测试环境用了https://app.example.com,桶上只配了http://localhost:8080这一个来源。解决办法很简单,把所有环境的域名都配上,或者用一个合理的通配符规则。
5. 一些实操中的体会
这次对接华为云OBS的过程,让我重新确认了一个观点:CORS不是纯技术问题,而是浏览器安全模型和分布式存储这个组合下必定会遇到的摩擦点。它不是一个“配好就万事大吉”的静态项目,因为前端域名可能会变、上传方式可能会改、Nginx层可能会调整,每一个变化都可能让CORS问题重新冒出来。
我个人的建议是,在项目初期就建立一份OBS CORS配置的“基线文档”,记录当前线上环境的精确配置、每个配置项的对应逻辑、以及排查CORS问题的操作步骤。这样以后再遇到同事来问“为什么上传又报CORS错了”,可以直接把文档甩过去,让他对照着查,至少能省掉一轮“我这边没问题啊”的拉扯。
最后再分享一个小技巧。在浏览器控制台里排查CORS问题时,不要只看Console面板的报错,一定要结合Network面板看完整链路。CORS报错在Console里看起来像是一个请求挂了,但点开Network里的具体请求,你会发现其实发起了两个请求:一个OPTIONS,一个PUT。仔细看OPTIONS的响应头和PUT的响应头分别返回了什么,问题往往就一目了然了。这个方法帮我在好几次联调里快速定位问题,希望对你有用。
