
对象存储后台显示“上传成功”,只说明服务端已经接收并保存了对象。图片能否被网站、浏览器或 CDN 正常读取,还要经过对象键、访问地址、存储桶权限、签名校验、跨域规则和缓存配置等多层检查。任何一层不匹配,都可能出现 403、404、签名无效,或者浏览器控制台提示 CORS 错误。
这类故障最容易走弯路的地方,是看到 403 就去改 CORS,看到图片不显示就把整个存储桶改成公开读。前者可能完全改错方向,后者又会扩大公开范围。比较稳妥的做法,是先判断请求在哪一层失败,再只修改对应配置。
先确认“无法访问”具体发生在哪一步
同一张图片可能有几种访问方式,它们经过的链路并不相同:
- 在对象存储控制台里预览;
- 直接在浏览器地址栏打开对象 URL;
- 通过网页中的
<img>标签显示; - 由前端 JavaScript 使用
fetch、XHR 或 SDK 读取; - 使用带有效期的签名 URL 访问私有对象;
- 通过 CDN、自定义域名或图片处理域名访问。
排查时应记录五项信息:请求 URL、HTTP 方法、状态码、响应头、响应体中的错误代码。浏览器开发者工具的 Network 面板通常能看到这些内容。对象存储服务返回的 AccessDenied、NoSuchKey、SignatureDoesNotMatch,比页面上笼统的“图片加载失败”更有判断价值。
可以先用 curl 绕开页面代码,查看对象地址本身的响应:
curl -I "https://example-bucket.example.com/images/demo.jpg"
如果服务端不支持或限制 HEAD 请求,再使用普通 GET,但不要把二进制图片直接输出到终端:
curl -sS -D headers.txt -o image-test.bin \
"https://example-bucket.example.com/images/demo.jpg"
直接请求也返回 403 或 404,问题通常在地址、对象键、权限、签名或源站配置。直接请求成功,只有网页脚本失败,才应该重点检查 CORS、浏览器凭据和前端请求头。
第一步:核对对象是否真的存在于目标位置
“上传成功”不一定代表上传到了你正在访问的路径。多环境项目常见的情况包括:测试环境上传到一个存储桶,正式站点读取另一个存储桶;SDK 使用了默认区域;应用把日期、租户 ID 或随机目录加进对象键;数据库保存的是旧地址。
建议在控制台或 API 中逐项核对:
- 存储桶名称是否正确;
- 区域和访问端点是否匹配;
- 对象键是否与请求路径完全一致;
- 大小写、空格、中文和特殊字符是否经过正确编码;
- 对象是否上传完成,大小和内容类型是否正常;
- 自定义域名是否绑定到了预期的存储桶或 CDN 分配。
对象键通常区分大小写。Images/logo.png 与 images/logo.png 可以是两个不同对象。带空格、+、#、? 或非 ASCII 字符的键,还可能在 URL 编码环节发生变化。不要只比较浏览器地址栏里“看起来差不多”的字符串,应复制控制台显示的完整对象键和实际请求 URL 逐字符检查。
AWS S3 的 HeadObject 文档还说明了一个容易误判的现象:请求者没有对象读取权限时会得到 403;对象不存在时,如果请求者同时没有列出存储桶的权限,也可能得到 403,而不是 404。这样设计可以减少对象名称被探测的风险。因此,403 并不能单独证明对象存在,也不能单独证明只是权限配置错误。
第二步:区分私有对象、公共访问与受控公开
私有存储桶里的对象,即使已经上传成功,也不会自动获得匿名读取权限。访问者必须携带有效身份凭据、使用签名 URL,或者通过具有源站访问权限的 CDN 获取对象。
如果业务需要公开展示网站图片,常见方案有三种:
- 仅允许特定前缀或特定对象公开读取;
- 存储桶保持私有,由 CDN 使用 Origin Access Control、服务身份或同类机制读取源站;
- 存储桶保持私有,由业务服务生成短期签名 URL。
不要为了处理一张图片的 403,直接关闭账户级或存储桶级的公共访问保护。AWS S3 的 Block Public Access 会在账户、存储桶、接入点等层级共同生效,并采用限制更严格的组合。即使对象 ACL 看起来允许公开读,上层公共访问阻止设置仍可能让匿名访问失败。Cloudflare R2、Google Cloud Storage 和其他对象存储也有各自的公共访问与统一权限模型,名称不同,判断原则相同:先确认匿名访问是否属于业务设计,再决定是否开放。
如果图片只供登录用户查看,公开读并不是正确修复。应保留私有权限,让应用在鉴权后发放短期 URL,或者由后端代理读取。涉及用户上传、订单附件、证件、合同、备份文件时,更不应通过“整个存储桶公开”来换取访问成功。
第三步:CORS 只处理浏览器跨域,不会替你授权
CORS 是浏览器执行的跨域访问控制。它决定某个网页源站能否通过脚本读取对象存储的响应,不负责给匿名用户增加对象读取权限。
判断 CORS 时,先看下面几种现象:
- URL 在地址栏直接打开也返回 403:先查权限或签名,不要先查 CORS;
<img src="...">能显示,但fetch()读取失败:很可能是 CORS;- 上传时浏览器先发送
OPTIONS,预检请求失败:检查允许的方法和请求头; - 服务端返回 200,但 JavaScript 读取响应时被浏览器拦截:检查响应中的 CORS 头;
- 同一个请求在
curl或服务端代码里成功,在浏览器里失败:CORS 的可能性明显增大。
普通 <img> 跨域展示通常不要求服务器允许脚本读取响应。如果后续要把图片绘制到 Canvas 并读取像素、由前端下载二进制内容,或者通过 JavaScript SDK 上传,浏览器会应用更严格的 CORS 规则。图片“能显示”和“能被脚本读取”是两个不同结果。
一条 CORS 规则至少要与实际请求的来源、方法和请求头相匹配。例如前端页面来自 https://www.example.com,配置里只允许 https://example.com,两者不会被视为同一个 Origin。协议、主机名和端口都属于 Origin 的组成部分。
下面是一个说明结构的示例,字段名称要按具体云厂商格式调整:
[
{
"AllowedOrigins": ["https://www.example.com"],
"AllowedMethods": ["GET", "HEAD"],
"AllowedHeaders": ["*"],
"ExposeHeaders": ["ETag"],
"MaxAgeSeconds": 3600
}
]
生产环境不建议为了省事长期使用任意来源。页面请求如果携带 Cookie、Authorization 或其他凭据,浏览器也不允许把 Access-Control-Allow-Origin: * 与凭据模式随意组合。应填写确切来源,并确认服务端响应中返回了与请求匹配的 Access-Control-Allow-Origin。
可以模拟浏览器的跨域 GET:
curl -I \
-H "Origin: https://www.example.com" \
"https://static.example-cdn.com/images/demo.jpg"
需要预检的请求可以这样检查:
curl -i -X OPTIONS \
-H "Origin: https://www.example.com" \
-H "Access-Control-Request-Method: PUT" \
-H "Access-Control-Request-Headers: content-type,x-custom-header" \
"https://example-bucket.example.com/uploads/demo.jpg"
检查响应是否包含匹配的允许来源、方法和请求头。只在控制台保存了 CORS 规则,不代表 CDN 已经把这些响应头转发给浏览器。使用 CDN 时还要核对缓存键、响应头策略和源站响应转发。
第四步:签名 URL 失效通常与时间、方法或 URL 变化有关
签名 URL 把授权信息放进查询参数,让没有云账号凭据的客户端在限定时间内执行指定操作。它适合私有图片临时查看、浏览器直传和一次性下载。
签名 URL 常见故障包括:
- URL 已过期;
- 生成签名时使用 GET,客户端实际发送 HEAD 或 PUT;
- 对象键、查询参数或主机名在签名后被改写;
- 反向代理、短链服务或前端路由丢失、重排了查询参数;
- 生成签名所用的临时凭据已经失效;
- 服务器时间偏差过大;
- 区域、服务端点或签名版本不匹配;
- 上传时签入的
Content-Type等请求头与实际请求不同。
签名 URL 应当作为一个完整字符串使用。不要先生成存储服务地址,再手工替换成 CDN 域名;也不要对已经签名的对象键再次编码。需要通过 CDN 发放私有内容时,应使用 CDN 自己的签名 URL 或签名 Cookie 机制,或者确认 CDN 能按原样把签名请求转发到源站。
排查时可以重新生成一条有效期较短的新 URL,在同一台设备上立即测试。新 URL 成功、旧 URL 失败,通常可以把范围缩小到过期、凭据轮换或 URL 被修改。新旧 URL 都失败,再检查签名使用的方法、区域、对象键和请求头。
第五步:检查 CDN、缓存和自定义域名
源站 URL 正常,自定义域名或 CDN URL 失败,故障范围已经缩小到 CDN 链路。需要重点检查:
- CDN 回源地址是否指向正确的存储桶和区域;
- 私有源站是否允许 CDN 的服务身份读取;
- Host 请求头和 SNI 是否符合源站要求;
- 缓存键是否保留签名所需查询参数;
- 403、404 等错误响应是否被 CDN 缓存;
- 防盗链 Referer、WAF 或访问规则是否拒绝了请求;
- 自定义域名证书是否有效,域名是否包含在证书中;
- CORS 响应头是否随缓存对象返回。
CDN 可能缓存源站返回的错误响应。即使源站权限已经修好,边缘节点仍可能在错误缓存有效期内继续返回旧的 403 或 404。修复后可以先用带唯一查询参数的 URL 验证,再按范围刷新缓存。不要在未确认缓存路径时清空整个站点缓存,以免造成不必要的回源压力。
如果开启了防盗链,还要确认规则是否允许空 Referer、搜索引擎、移动应用或你的正式域名。浏览器隐私策略、HTTPS 到 HTTP 的跳转以及部分客户端都可能让 Referer 与预期不同。
一条可复用的排查顺序
实际处理时,可以按下面的顺序缩小范围:
- 在存储控制台确认对象键、大小、内容类型和所在区域;
- 使用原始源站 URL 测试 GET 或 HEAD,记录状态码和错误代码;
- 私有对象使用一条刚生成的签名 URL 测试;
- 对照实际业务确认应采用公开读、签名 URL,还是 CDN 私有回源;
- 只有浏览器脚本失败时,再带
Origin测试 CORS; - 源站正常后测试 CDN URL,检查错误缓存、回源身份和查询参数;
- 修复后分别验证地址栏访问、网页图片、前端脚本和移动端等真实入口。
这套顺序的好处,是每一步只验证一层。原始源站还没成功时,继续刷新 CDN 或调整页面代码只会增加变量。
修复后的验收清单
修复完成后,至少验证以下项目:
- 新上传对象和历史对象都能按预期访问;
- 大小写、中文文件名和带空格的对象键处理正常;
- 匿名用户无法访问本应私有的对象;
- 签名 URL 过期后确实失效,新签名可以立即使用;
- 正式域名的 GET、HEAD、PUT 等业务方法符合 CORS 规则;
- 未授权 Origin 不能通过浏览器脚本读取响应;
- CDN 命中与回源时都返回正确内容和响应头;
- 403、404 修复后没有继续被错误缓存;
- 监控或日志能够区分源站拒绝、签名失败和 CDN 拦截。
对象存储图片访问问题通常不需要大范围放开权限。先把对象地址、权限、CORS、签名和 CDN 五层拆开,找到第一个失败点,再做最小修改。这样既能恢复图片,也能避免把原本私有的文件暴露出去。
参考资料
资料核验日期:2026 年 9 月 18 日。
- AWS S3:Blocking public access to your Amazon S3 storage,https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-control-block-public-access.html
- AWS S3:Configuring cross-origin resource sharing (CORS),https://docs.aws.amazon.com/AmazonS3/latest/userguide/cors.html
- AWS S3:Download and upload objects with presigned URLs,https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-presigned-url.html
- AWS S3 API:HeadObject,https://docs.aws.amazon.com/AmazonS3/latest/API/API_HeadObject.html
- AWS S3:Naming Amazon S3 objects,https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-keys.html
- Cloudflare R2:CORS,https://developers.cloudflare.com/r2/buckets/cors/
- Cloudflare R2:Presigned URLs,https://developers.cloudflare.com/r2/api/s3/presigned-urls/
- Cloudflare R2:Public buckets,https://developers.cloudflare.com/r2/buckets/public-buckets/
- Google Cloud Storage:Cross-origin resource sharing (CORS),https://cloud.google.com/storage/docs/cross-origin
- Google Cloud Storage:Signed URLs,https://cloud.google.com/storage/docs/access-control/signed-urls
- MDN Web Docs:Cross-Origin Resource Sharing (CORS),https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS




