存储跨域(CORS)配置与上传排查
平台的文件上传走「预签名直传」:浏览器向后端申请一个预签名地址,然后由浏览器直接把文件传到对象存储, 文件不经过后端。这一步是「页面 → 对象存储」的跨域请求,桶上没有放行跨域规则时,浏览器会在发出真实请求之前就拦下它。
这个问题的麻烦之处在于:「测试连接」是通过的。测试连接是后端服务器发起的探测,不经过浏览器, 天然测不出跨域问题。密钥、权限、桶是否存在它都能测,唯独测不出 CORS。
真实案例:2026-10 有部署使用 Cloudflare R2,在桶的 CORS 策略里把「允许请求头 / 暴露响应头」也写成了
*, 控制台保存时只报一句「此策略无效」,换浏览器、换网络排查了很久。根因是 R2 的策略校验只允许*出现在AllowedOrigins,两个头字段必须逐个列名。
现象
- 浏览器控制台报
No 'Access-Control-Allow-Origin' header is present on the requested resource(或带CORS policy字样的报错); - 上传转圈后失败,但后端日志里找不到对应请求,预检阶段就被浏览器拦下了;
- 「测试连接」通过、密钥权限都正常,上传却必失败;
- 小程序 / APP 端能传,Web 管理端不能传(只有浏览器环境受同源策略约束)。
先分清是不是 CORS
「上传失败」至少有三种成因,混在一起会把排查带偏:
| 现象 | 真实原因 | 判别方法 |
|---|---|---|
| 控制台报 CORS 字样,后端无请求日志 | 桶的跨域规则未放行 | 见下方「验证」一节 |
| 请求一直 pending / 连接超时 / 网络错误 | 预签名地址不可达(服务端点填了内网地址、防火墙拦截) | 用 curl 直接传预签名地址(服务端请求不受 CORS 限制),能通则与 CORS 无关 |
上传返回 403 AccessDenied | 密钥缺权限 / 桶策略拒绝 / 预签名已过期 | 看后端日志或 curl 的真实状态码 |
一句话判别法:curl 能传成功、浏览器报 CORS = 跨域配置问题;curl 也失败 = 与 CORS 无关,去查端点与权限。
修复
跨域规则就四个要素:来源(管理端与 H5 的地址,联调阶段可用 *)、方法(必须含 PUT)、 允许请求头(*;Cloudflare R2 需逐个列名)、暴露响应头(建议加 ETag)。各要素的取值细节、 各厂商控制台入口与 R2 可粘贴策略见下方。
通用四要素
各厂商控制台的位置与命名不同,但语义一一对应:
| 配置项 | 建议值 | 说明 |
|---|---|---|
| 来源 Origin / AllowedOrigins | 管理端与 H5 的访问地址;联调阶段可先用 * | 只填 scheme://host[:port],不带路径、不带尾斜杠,端口不能用通配符 |
| 方法 Methods / AllowedMethods | PUT, GET, HEAD, POST | 上传走 PUT,读取与回显走 GET/HEAD;只勾 GET 会让上传的预检请求失败 |
| 允许请求头 AllowedHeaders | *(Cloudflare R2 需逐个列名) | 至少覆盖浏览器实际发送的请求头,取值方法见下 |
| 暴露响应头 ExposeHeaders | ETag | 前端要做 ETag/MD5 校验必须显式列出,否则 JS 读不到该响应头 |
关于来源填 *(允许所有来源):预签名 URL 的签名在 query 参数里、不携带 Cookie,所以通配符可用; 若哪天改成携带 Cookie 的跨域调用,浏览器不允许「通配符 + 凭证」并存,必须改成具体域名。
不知道要放行哪些请求头时不要去猜:开发者工具 → Network → 上传时那条 OPTIONS 预检请求 → Access-Control-Request-Headers 头列出的名字,就是清单。
各厂商入口
| 厂商 | 位置 | 备注 |
|---|---|---|
| MinIO | 无桶级界面,由服务端变量 MINIO_API_CORS_ALLOW_ORIGIN 控制 | 自建场景通常无需配置 |
| 阿里云 OSS | 桶 → 数据安全 → 跨域设置 | * 可用于来源与两个头字段;除来源和方法外,别忘了单独加暴露响应头 ETag |
| 腾讯云 COS | 桶 → 安全管理 → 跨域访问 CORS 设置 | 同上;控制台的「暴露 Headers」即 ETag 的位置 |
| Cloudflare R2 | 桶 → Settings → CORS Policy(JSON 编辑) | * 只能写在 AllowedOrigins,两个头字段必须逐个列名 |
其余厂商(华为云 OBS、AWS S3、七牛云 Kodo 等)在各自控制台的桶跨域设置里按上述四要素配置即可,入口名称以官方文档为准。
R2 可粘贴策略
R2 的 CORS Policy 用 JSON 编辑,下面的策略可直接粘贴使用:
[
{
"AllowedOrigins": ["*"],
"AllowedMethods": ["GET", "PUT", "POST", "DELETE", "HEAD"],
"AllowedHeaders": [
"Content-Type",
"Content-Length",
"Content-MD5",
"Content-Disposition",
"Content-Encoding",
"Cache-Control",
"Authorization",
"x-amz-acl",
"x-amz-date",
"x-amz-content-sha256"
],
"ExposeHeaders": ["ETag", "Content-Length", "Content-Type", "Last-Modified"],
"MaxAgeSeconds": 3600
}
]注意 R2 只有 AllowedOrigins 支持 *,AllowedHeaders / ExposeHeaders 必须逐个列名; 需要放行更多请求头(如自定义元数据 x-amz-meta-* 的具体名字)时逐个追加到 AllowedHeaders。
控制台保存不了时,可改用官方 CLI 设置(报错会精确到字段): npx wrangler r2 bucket cors set <桶名> --file cors.json(文件格式为 {"rules":[{"allowed":{"origins":[...],"methods":[...],"headers":[...]},"exposeHeaders":[...],"maxAgeSeconds":3600}]})。
验证
分两层验证:先确认「服务端链路的预签名地址可用」,再确认「浏览器侧跨域放行」。
# 1. 服务端直接 PUT 预签名地址(不走浏览器, 不受 CORS 限制)
curl -X PUT --upload-file ./test.png "https://<预签名地址>"
# 2. 带 Origin 头观察预检响应(部分厂商对 OPTIONS 直接返回跨域响应头)
curl -sD - -o /dev/null -X OPTIONS \
-H "Origin: https://admin.example.com" \
-H "Access-Control-Request-Method: PUT" \
"https://<预签名地址>" | grep -i "access-control"- 第 1 步成功、第 2 步响应里没有
Access-Control-Allow-Origin:跨域规则未生效(或未配),回到「修复」; - 两步都失败:不是跨域问题,查服务端点是否浏览器可达、密钥权限与预签名是否过期。
改完规则仍不生效时,通常是三层延迟之一:桶规则传播(官方称极端情况要几十秒)、 浏览器预检缓存(按 MaxAgeSeconds 缓存,用无痕窗口复测)、CDN / 自定义域缓存(需刷新)。