上个月有个朋友跑来问我:图都传上去了,怎么网页里全是红叉?我打开他发来的图片链接,把URL粘到浏览器直接访问,竟然也给我一个403 Forbidden。其实这类问题在PicGo + 阿里云OSS的组合里非常典型,十个用OSS做图床的人,至少五个人会在某一天撞上这个报错。更坑的是,PicGo这边明明提示上传成功,OSS控制台里也能看到文件,可浏览器就是刷不出来。今天我就把这条完整的排查链路、背后的拦截机制、以及不同使用场景下的修复模板一次讲清楚。
1. 上传明明成功,图片却一片红叉:重现403现象现场
1.1 403不是上传失败,是访问被拒
很多人第一反应是“图片没传上去”,其实不是。PicGo提示上传成功,说明你的AccessKey、Bucket、Endpoint配置是对的,文件已经落到了OSS里。403出现在“客户端请求这张图片”这一步,是OSS在返回访问拒绝。
你可以把OSS当成一个高档小区:上传成功是你把快递放进了小区里的快递柜,钥匙你也有了;403则是你要进小区大门时,保安发现你没有门禁卡或者卡里没权限,直接把你拦在外面。上传链路的配置和下载/访问链路的配置,是两套逻辑。
PicGo一般只负责写入对象,而浏览器访问图片时,URL是直接请求OSS的,走的是读链路。所以当你看到403时,第一优先级不是怀疑PicGo,而是检查Bucket的访问规则、对象权限、以及是否开启了防盗链。
1.2 先分清三种现象,缩小排查范围
同样是403,细节不同,指向的原因完全不同。我习惯把现象分成三类:
- 直接浏览器访问图片URL也403:说明问题出在Bucket或Object的访问权限上,大概率是Bucket是“私有”,或者图片对象的ACL是“私有”。
- 浏览器直接访问URL能打开,但放在网页里就是403:这种情况八成是防盗链、Referer白名单限制,或者自定义域名回源校验出了问题。因为你网页打开时,请求头里带了Referer字段,OSS检查后发现不是你允许的来源。
- 本地或某些网络下能打开,外网或特定区域打不开:这种比较少见,可能与Bucket Policy的IP限制、地域限制、或者是签名URL的有效时间有关。如果你用了签名URL,时间过期或者客户端时间偏差过大,也会随机403。
把这个分类理清楚,能帮你省下很多瞎折腾的时间。
1.3 记住OSS 403的Error Code,比反复刷新更有效
阿里云OSS返回403时,响应体里通常会带一个XML格式的Error信息,里面最关键的是<Code>字段。排查的时候不要只看“403 Forbidden”这几个字,要把响应体里的Error Code记下来。
| Error Code | 大概含义 | 常见触发场景 |
|---|---|---|
| AccessDenied | 权限不足 | Bucket私有、Object私有、Referer防盗链拦截 |
| SignatureDoesNotMatch | URL签名不匹配 | AccessKey错误、URL被改动、系统时间偏差 |
| InvalidAccessKeyId | AccessKey不存在 | RAM账号配置错误或已被禁用 |
| RequestTimeTooSkewed | 请求时间与服务器差异过大 | 客户端或服务器时钟偏差超过15分钟 |
| InvalidObjectName | 对象名不合法 | 路径或文件名中包含了非法字符 |
| AccessForbidden | Bucket Policy拒绝 | 被Bucket Policy显式拒绝或防盗链拦截 |
很多人在群里问“为什么403”,我说你先把Error Code发出来,马上就能定位一半问题。所以后面所有排查步骤,我会把“如何看到Error Code”也带上。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 阿里云OSS访问403的三层拦截机制:ACL、Policy与防盗链
2.1 Bucket的“大门属性”:ACL权限分类
OSS里最容易被忽视的就是ACL。Bucket ACL有几种:私有(Private)、公共读(Public Read)、公共读写(Public Read/Write)。对应到对象(Object),还有继承Bucket、私有、公共读、公共读写这些选项。
当你把Bucket设为“私有”时,所有不带签名的URL都会被拒绝,返回403 AccessDenied。这是最常规的403来源。很多人建Bucket时图省事,直接选“私有”,然后PicGo上传时如果对象ACL写的是“继承Bucket”,那图片默认就是私有,访问自然就403。
生产环境里图床Bucket确实不应该开放写权限,但读权限必须开放。所以最省心的做法是:Bucket ACL设为“公共读”,对象ACL设为“继承Bucket”。这样所有人的匿名GET请求都能成功,但只有持有AccessKey的人才能写入或删除。
2.2 RAM子账号与临时凭证的“门禁卡”
很多人用的是主账号AccessKey,还有一部分人会为PicGo单独建一个RAM子账号,这是好习惯。但麻烦往往出在RAM子账号的权限策略上。
假设你给子账号只授权了oss:PutObject,那上传肯定成功。但如果图床需要删除或者改文件,可能就不够用。更重要的是,有时候你明明把Bucket设成了公共读,图片却还是403,这时候就要怀疑是RAM权限策略里写了Deny规则,或者子账号被附加了“只允许某些IP访问”的权限。
RAM权限、Bucket Policy、Object ACL这三者的判断逻辑有点像“小区门禁”:每个入口都要刷卡,有一道门没过,你就进不去。所以不要以为Bucket设为公共读就万事大吉,如果Bucket Policy里有一条“禁止匿名访问所有上传的图片”,照样403。
2.3 Referer防盗链和白名单的“拦路虎”
另一种高发403是防盗链。阿里云OSS控制台提供“防盗链”设置,允许你设置Referer白名单。如果你开了一个白名单,只允许https://example.com访问,那么从别的网站引用图片时,OSS会认为Referer不合法,直接拒绝,返回403。
这里有个非常隐蔽的坑:如果你在OSS防盗链设置里,把“允许空Referer”选项关闭了(或者默认没有勾选),那么用户直接在浏览器地址栏输入URL访问或者通过RSS阅读器、微信内置浏览器等不发送Referer的客户端打开图片时,也会被判为非法来源,出现403。这就是为什么经常有人“直接打开URL能看,但某些场景下又打不开”。
2.4 自定义域名/CDN与回源鉴权
如果你把OSS默认域名(如your-bucket.oss-cn-hangzhou.aliyuncs.com)换成了自己的域名,并且把自定义域名绑定到了Bucket的“域名管理”里,那访问链路上还会多一层“CNAME”校验。自定义域名绑定有问题,或者没有在HTTPS证书里上传合法证书,也可能导致请求到达OSS后被拒绝。
更常见的是你套了一层CDN。如果CDN回源时没有配置“回源鉴权”,而Bucket又是私有的,CDN回源就会拿不到内容,返回403。这时你需要在CDN里配置“阿里云OSS回源私钥”,或者开通“私有Bucket回源”功能,让CDN用固定账号的身份去OSS拉取图片。
2.5 签名URL、时间偏差和图片处理链路的隐形门槛
当Bucket是私有时,你给图片链接拼接的URL需要带签名参数(Expires、Signature等)。签名URL有时效性,过期后访问就会返回403。PicGo默认上传后生成的是普通公开链接,不会有签名;但如果你用了某些自定义图床插件或者脚本,上传后返回的可能是签名URL,那就得检查有效期。
另外,系统时钟偏差是一个非常容易被忽略的原因。OSS签名校验会对比请求的Date头或URL里的Expires参数与服务器时间,如果客户端时间比OSS服务器时间快了或者慢了超过15分钟,就会报RequestTimeTooSkewed,表现就是403。这在很多Linux服务器上尤其常见,比如Ubuntu虚拟机如果长时间没做时间同步,系统时间漂移了,fastadmin这类PHP项目上传图片后,前端再拼接签名URL访问时就会随机403。
3. 按这个顺序排查,十分钟定位403根因
3.1 第一步:确认Bucket访问权限设置
先去OSS控制台找到你的Bucket,进入“权限管理” > “Bucket ACL”,看当前是什么权限。
- 如果Bucket是“私有”,而你又用的是默认域名直接访问,那必须在URL里携带签名参数。图床场景下几乎没人这么做。
- 如果Bucket是“公共读”或“公共读写”,可以先排除Bucket级别权限问题。
我曾经遇到过用户Bucket是“公共读”,但图片还是403,最后发现是对象的权限被单独设置成了“私有”。所以这里要往下看对象权限。
3.2 第二步:确认对象(图片)上传后ACL被设为私有
在OSS控制台文件列表里,点击任一图片文件的“详情”或“权限”,看这个Object的ACL是什么。
- 如果是“继承Bucket”,而Bucket是公共读,那没问题。
- 如果上传时PicGo或自定义脚本显式设置了Object ACL为“私有”,那么即使Bucket是公共读,这个对象也会被单独拦截。
PicGo默认配置不会主动设置Object ACL,一般是继承Bucket。但如果你用了某些二次开发插件,或者通过Python脚本上传时在Header里加了x-oss-object-acl: private,那就容易踩坑。排查方法可以多传一张图,然后在OSS控制台看新文件的ACL。
3.3 第三步:检查RAM子账号权限授权范围
如果你用的是RAM子账号,去RAM控制台查看这个账号绑定的权限策略。
- 如果策略是系统自带的
AliyunOSSFullAccess,那基本没问题。 - 如果是自定义策略,重点看有没有
Allow或Deny对oss:GetObject的处理。
提示:图床场景下,RAM子账号至少需要
oss:PutObject和oss:DeleteObject的上传/管理权限。但如果Bucket本身是公共读,通常不需要给匿名用户单独的GetObject权限,因为匿名访问走的是Bucket策略,不走RAM身份验证。
不过有个例外:如果你给RAM子账号设置了“只允许特定IP段访问OSS”的条件,那从其他IP上传或访问也可能被拒,表现就是403。
3.4 第四步:检查自定义域名和CDN配置
如果你用的是自定义图片域名,先在OSS控制台“Bucket配置” > “域名管理”里确认域名已绑定,并且CNAME解析已经指向对应的OSS域名。如果绑定了CDN,检查CDN加速域名是否正确回源到OSS,同时确认是否开启了“私有Bucket回源”。
这里有个比较容易踩的坑:你在OSS控制台绑定了自定义域名,但CDN用的是另一套源站配置,回源到OSS时没带Host头,结果请求到了OSS的默认域名上,被Bucket权限策略拦下来了。
3.5 第五步:检查Referer防盗链和访问控制“网络限制”
进入OSS控制台“权限管理” > “防盗链”:
- 看是否勾选了“设置Referer白名单”。
- 如果白名单里只有你自己的域名,那从其他站点直接引用图片就会被403;如果同时关闭了“允许空Referer”,直接打开图片链接也会403。
另外,在“权限管理” > “Bucket Policy”看看有没有配置基于IP的Allow/Deny规则。如果配置了“仅允许某个IP段访问OSS”,其他IP访问就都会403。
3.6 用OssUtil和curl做最小验证
为了确定不是“浏览器缓存”等玄学问题,我一般会用命令行做最小验证。
先安装ossutil(阿里云官方命令行工具),然后用它执行:
bash复制ossutil sign oss://your-bucket/path/to/image.jpg --timeout 3600
如果命令能生成一个临时签名URL,浏览器访问这个链接能打开,说明文件本身是好的,问题出在匿名访问权限上。
再用curl直接请求默认域名:
bash复制curl -I "https://your-bucket.oss-cn-hangzhou.aliyuncs.com/path/to/image.jpg"
如果返回403,看响应Body里的Error Code。再用带Referer的方式请求一次:
bash复制curl -I -e "https://yourwebsite.com" "https://your-bucket.oss-cn-hangzhou.aliyuncs.com/path/to/image.jpg"
如果带上Referer后返回200,不带返回403,那就是防盗链的问题。这个非常直观。
4. 不同使用场景下的修复配置模板
4.1 最省心的方案:把图床Bucket设为公共读
如果图床里的图片本来就是为了对外展示,没有隐私要求,我个人推荐直接用公共读。具体配置:
- OSS控制台选择对应Bucket。
- “权限管理” > “Bucket ACL” > “公共读”。
- “权限管理” > “防盗链”:建议白名单留空,或者只禁止恶意来源,但务必勾选“允许空Referer”。
- 图片上传后Object ACL选择“继承Bucket”即可。
这个方案适合个人博客、GitHub Pages、静态网站等对外展示场景。注意:“公共读写”不要勾,因为世界上的任何人都能往你的Bucket传文件,流量和存储费不是闹着玩的。
4.2 只想允许自己网站引用图片的配置
如果你担心图片被其他网站直接盗链,想省点流量,那就开启Referer白名单。配置时注意几点:
- 在“防盗链”里添加你的网站域名,例如
https://www.example.com,可以填泛域名*.example.com。 - 务必勾选“允许空Referer”,否则微信、RSS客户端这类不携带Referer的场景会全部403。
- 如果你希望“直接输入图片链接也看不到”,那也可以不勾选“允许空Referer”,但要做好“某些正常渠道也打不开”的心理准备。
实际操作中,为了图片能被更多平台正常展示,我建议保留“允许空Referer”。盗链的网友就算偷了图片链接,因为Referer不匹配也是404或403,流量的损失并没有想象中大。
4.3 使用自有域名加HTTPS时的配置
如果你把图片域名换成了自己的域名,比如img.example.com,并希望走HTTPS,需要做两步:
- 在OSS控制台“域名管理”里绑定
img.example.com,并上传对应域名的SSL证书。 - 把
img.example.com的CNAME记录解析到Bucket域名,比如your-bucket.oss-cn-hangzhou.aliyuncs.com。
注意:如果你还套了CDN,不要直接在OSS上绑定HTTPS证书,而是在CDN上配置证书,然后把源站设置为OSS域名,开启CDN的“私有Bucket回源”或“回源鉴权Header”。
曾经有个案例,用户把CDN的源站设置成了自定义域名img.example.com,而img.example.com又CNAME到了OSS,结果形成了环路,回源请求被OSS以“Host头不匹配”拒绝,返回403。这种问题很难发现,排查时最好在CDN回源Host里显式写成Bucket默认域名。
4.4 FastAdmin、Ubuntu环境里的特殊坑
热搜词里出现频率很高的“FastAdmin上传到阿里云OSS”和“PicGo Ubuntu”,其实指向的是同一个底层问题:系统时间不同步。
FastAdmin的后台上传插件如果配置了OSS直传签名,前端的JavaScript会生成一个带expire的临时凭证。如果服务器系统时间不准,生成的签名有效期可能已经过期,OSS就会返回403。Ubuntu桌面版尤其容易出这种问题,因为很多人的主机没有开启NTP自动同步。
解决办法很简单:
bash复制sudo timedatectl set-ntp true
然后确认时间已经同步:
bash复制timedatectl
在FastAdmin的OSS配置中,也要确认Endpoint是完整的,比如oss-cn-hangzhou.aliyuncs.com,不要写成oss.aliyuncs.com这种泛域名,否则签名时Region对不上,可能也会403。
4.5 一个相对安全的方案:签名URL只在必要场景用
如果你确实需要Bucket保持私有,那就要接受“图片链接必须带签名”的现实。
- 在PicGo配置中,使用阿里云OSS的“自定义参数”或一些插件来生成带签名的URL。
- 但要注意,签名URL有时效性,默认几十分钟到几小时不等,不能用作永久图床。
如果你既要私有,又要公开访问,不如直接在Bucket权限上做“只读开放”,不要走签名方案。图床场景的核心诉求是“公开读、私有写”,签名URL完全不匹配这个场景。
5. 完整案例复盘:从控制台日志到代码修复
5.1 案例一:RAM子账号上传权限不完整导致外链403
一位用户反馈:用PicGo上传图片成功,但复制出来的Markdown链接在浏览器打开403。我让他先去OSS控制台看文件,发现文件存在,权限是“继承Bucket”,Bucket是“公共读”。奇怪,那为什么403?
后来去RAM控制台查子账号权限,发现他给子账号附加的是一个自定义策略,只允许oss:PutObject和oss:ListObjects,但策略里有一条Deny语句,禁止了所有oss:GetObject操作。虽然匿名用户访问公共读对象不需要RAM身份,但OSS的鉴权逻辑会先检查Bucket Policy、RAM Policy等身份策略,一旦匹配到显式Deny,就会拒绝。
修复方案就是删掉那条Deny,或者直接用官方自带策略AliyunOSSFullAccess。这个案例提醒我们:在OSS权限判断里,显式Deny的优先级最高,哪怕你只是“只影响RAM身份”,也可能把匿名访问连带挡了。
5.2 案例二:自定义域名没绑定,URL拼接出了问题
另一个用户把Bucket的默认域名打码后,在PicGo里设置“自定义域名”为https://cdn.example.com,但在OSS控制台并没有给Bucket绑定这个域名,也没有配置CDN。于是上传完成后,PicGo返回的URL是https://cdn.example.com/xxx.jpg,浏览器访问时根本不会命中OSS,而是指向了用户自己的服务器,服务器返回403。
这种情况并不是OSS的403,而是DNS解析到了错误地方。我建议用户在PicGo的“自定义域名”设置里,务必先确认域名已经正确绑定到OSS的“域名管理”,或者已经接入CDN。否则别瞎填。
排查方法很简单:在命令行nslookup cdn.example.com,看解析结果是不是OSS的IP或CNAME。如果解析到的是一个普通服务器的IP,那问题可能根本不在OSS上。
5.3 案例三:图片处理样式与签名冲突
还有人使用了OSS的图片处理服务,比如URL后缀加了?x-oss-process=image/resize,w_800,结果访问403。这往往是因为Bucket是私有的,而图片处理请求需要额外的签名参数,或者图片处理服务没有开通。
如果使用默认域名,并且Bucket是公共读,图片处理样式通常可以直接生效。但如果Bucket是私有,就必须使用签名URL,同时把x-oss-process参数包含在签名范围内。很多人在URL手动加参数,就会破坏签名,导致403 SignatureDoesNotMatch。
解决方法是开启“图片处理”的“原图保护”或使用官方SDK生成带处理参数的签名URL,不要手动拼URL。
5.4 复盘:如何通过OSS日志快速定级
如果你不知道怎么查Error Code,最快捷的方式是开启“OSS访问日志”。在OSS控制台“日志管理”里配置“实时日志查询”或“日志转存”,能看到每次请求的HTTP Status、Error Code、Requester、Referer等字段。
我修复完客户的403后,一般会先在日志里确认最近几个小时的403分布,从而判断是偶发性问题还是持续性权限问题。如果某个固定URL反复出现AccessDenied,那基本是权限配置问题;如果是SignatureDoesNotMatch,则重点排查签名生成工具和系统时间。
6. 修复之后的体检:用日志与OssUtil预防下次403
6.1 打开OSS访问日志,配置告警
很多人修完403就跑了,但我建议趁热打铁,把“访问日志”功能打开。OSS的“实时日志查询”可以帮你快速看到访问请求的返回状态码,配合预警功能,当403比例升高时有邮件或短信提醒。
具体操作:在OSS控制台“日志管理”里开启“访问日志”,选择一个日志Project,然后绑定投递到SLS。之后就可以用类似SQL的查询语句分析:
sql复制status: 403 | SELECT count(*) as cnt, "Error Code" GROUP BY "Error Code"
这样一旦再出现403,你能立刻知道是哪种类型,而不是等用户抱怨后才知道。
6.2 使用ossutil配合RAM权限做批量检查
如果你有大量旧图片,想确认哪些对象是私有的,可以先用ossutil批量列出对象的ACL:
bash复制ossutil ls oss://your-bucket/ --all-versions
但更实用的是用ossutil stat检查单个文件的元数据:
bash复制ossutil stat oss://your-bucket/path/to/image.jpg
输出里会有ACL字段。如果发现某个目录下所有对象都是私有,可以批量修改,但修改对象ACL需要遍历:
bash复制ossutil set-acl oss://your-bucket/path/ public-read -r
-r表示递归。注意:这种方式会改变整个目录下所有对象的权限,务必确认这些文件都是需要公开访问的图片。
6.3 写一个简单的健康检查脚本,周期访问图片URL
我在自己项目中会写一个Shell脚本,每天检查一次图床的关键图片URL,如果是403或404就发告警。
bash复制#!/bin/bash
URL="https://your-bucket.oss-cn-hangzhou.aliyuncs.com/path/to/test.jpg"
HTTP_CODE=$(curl -o /dev/null -s -w "%{http_code}" "$URL")
if [ "$HTTP_CODE" != "200" ]; then
echo "Image health check failed: $HTTP_CODE" | mail -s "OSS Image 403 Alert" you@example.com
fi
当然,你要选一张确定存在且公开的图片作为健康探针。这种方式比人工检查靠谱得多。
6.4 一个容易被忽略的操作习惯
最后说一个大家常踩的坑:很多人为了省事,直接在RAM策略里给子账号配了oss:*,然后又嫌验证麻烦,就把Bucket设成了“公共读写”。这种配置等于给全互联网开了写权限,倒不是403问题,而是安全风险。413/上传滥用、刷流量、账单爆炸都是这么来的。
我的习惯是:Bucket永远不要开公共读写,RAM用户也尽量按最小权限来给。图床场景就只给PutObject、GetObject、DeleteObject这几个Action足够。如果哪天真遇到403,按本文第三章的顺序排查,一般十分钟就能找到根因。
另外有一点蛮实际:处理这种403问题时,一定要记得在OSS的“访问控制”里看清“IP黑名单”和“Bucket Policy”,因为好几回我排查到最后,发现是客户几个月前设置了IP黑白名单,把自己公司IP之外的访问全部禁止了。修好之后,可以顺手做一次“在线检测工具”验证,比如在阿里云控制台用“网络诊断”检查一下Bucket的连通性,能省下不少来回折腾的时间。
