不知道你有没有遇到过这种场景:开发环境里图片显示得好好的,一上生产就翻车——前端拿到的图片地址要么打不开,要么直接变成下载,要么干脆给你一个 403。我自己做独立项目那会儿轮流试过好几个图床方案,最后兜兜转转还是回到 Amazon S3。这篇就把我从零开始,把一张本地图片通过 S3 上传、配置成 Public,最后拿到一个能在浏览器里直接打开的 URL 的完整过程记录下来。
你要是以为“这不就是控制台里点两下的事”,那后半段你会很意外。光是 Block Public Access 这一个默认开关,就能卡住一大批人。文章会覆盖两种公开访问路线的选择、控制台和 CLI 两种上传方式、图片打开变成“下载”的坑、以及 403 报错的完整排查链路。适合第一次用 AWS 的朋友,也适合那些之前总在 S3 权限上栽跟头、这次想一次性搞明白的开发者和运维。
1. 先把思路理清:Public 公开访问的两种主要路线
很多人一上来就奔着“让链接能直接打开”去,结果把 Bucket 权限开到最大,最后又担心数据裸奔。S3 的公开访问其实不是只有“全公开”一种玩法,动手之前先得选对路线。
1.1 整个 Bucket 打开公读:适合当静态资源仓库
路线一就是让整个 Bucket 里的对象都允许匿名 GET 请求。任何人在浏览器输入 Object URL,就能直接看到图片。适合头像、商品图、文章配图、分享页素材这类本来就要给全网用户看的内容。
实现这种效果最推荐的办法,是往 Bucket 里写一条 Bucket Policy。你用不着在上面勾什么“公开读写”,只需要声明一条 s3:GetObject 允许所有人访问的策略,S3 就会把对象以“公开可读”的方式返回。
这种路线的优点是简单、直接,控制台上传完文件,外面立刻能访问;缺点是你得接受“这个 Bucket 里的任何文件都是公开的”这个事实。所以 Bucket 本身要专桶专用,别把私密资料和数据备份往里塞。
1.2 私有 Bucket + 预签名 URL:适合受限场景
路线二是反过来,Bucket 保持全私有,只有拿到有效链接的人才能访问。这靠的是 预签名 URL(Presigned URL)。你指定一个对象和过期时间,AWS 会生成一串带签名参数的临时链接,链接到期后自动失效,外面再打开就是 403。
预签名 URL 适合哪些场景?临时交付文件、给客户发几张订单截图、用户之间的隐私图片分享,还有付费内容的短时预览。它比全公开更安全,因为你可以控制有效期、只能访问指定对象,而且不需要改 Bucket 的任何权限。
这两种路线可以同时存在:同一个 Bucket 里,你想公开的图片用公有读策略,想私密的对象照样可以通过预签名链接单独分享。
| 对比项 | 整个 Bucket 公读 | 私有 Bucket + 预签名 URL |
|---|---|---|
| 适用内容 | 静态资源、公开图片 | 隐私数据、临时文件 |
| 链接有效期 | 永久有效 | 可自定义到期时间 |
| 访问控制粒度 | 整个 Bucket 匿名只读 | 指定对象、指定时长 |
| 实现成本 | 一条 Policy 搞定 | 每次生成链接需调用 API |
| 防误伤风险 | 高,别乱放文件 | 低,可控性强 |
如果你要做的是“上传图片并通过链接直接打开”,先问自己一句:这些图片是不是永远不需要收回来?如果答案是“是”,走路线一;答案不确定,走路线二更稳妥。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备:账号、Bucket 创建与权限模型中的三个关键设置
路线确定后,开始搭环境。这部分的坑密度最高,我几乎每次给朋友远程排查 S3 问题,最后都能回到这三个地方。
2.1 不要用根账号操作,先建一个 IAM 用户
登录 AWS 之后,第一件事不是创建 Bucket,而是去 IAM 控制台建一个专用用户。我见过不少人在根账号下顺手点完了所有配置,后来项目交接、权限回收全都说不清。
建议创建一个 IAM 用户,分配 AmazonS3FullAccess 权限就够了。如果是做生产环境,更精确的做法是只给指定 Bucket 的读写权限。I AM 策略大概长这样:
json复制{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject"
],
"Resource": "arn:aws:s3:::your-bucket-name/*"
}
]
}
这个用户创建好之后,会有一对 Access Key ID 和 Secret Access Key。后面用 AWS CLI 上传图片时要用到,下载 CSV 或自己记好,别随手发群里。
2.2 创建 Bucket 时连续踩中的三个坑
打开 S3 控制台,点 Create bucket,第一步就有一堆选项。新手最容易在这三个地方翻车。
坑一:Bucket 名称必须全局唯一。
S3 的 Bucket 名是全世界唯一的,你没法起一个叫 images 的 Bucket,因为它早被别人注册了。命名建议带上项目名和用途,比如 blog-image-public-2025。名称只能用小写字母、数字、点号和短横线。
坑二:区域选择会影响访问延迟和 URL 写法。
同理,你会想选离目标用户最近的区域。面向国内用户,东京 ap-northeast-1 或新加坡 ap-southeast-1 比较常见;面向北美,就选 us-east-1。区域确定之后,对象的 URL 会带上这个区域标识,后面会讲到。
坑三:Block All Public Access 默认是勾上的。
这是最大的一个坑。AWS 从安全角度考虑,新创建的 Bucket 默认开启 “Block all public access”。也就是说,就算你后面上传文件时勾了 “Public Read”,或者手动写了公开权限,S3 也会直接无视,访问一律 403。
控制台创建 Bucket 时,你需要把 “Block all public access” 这一栏的勾全部取消,才能给后续公开访问留出余地。取消时 AWS 会弹一段警告,大意是“你正在允许公开访问”,确认就行。
坑四:Object Ownership 决定你能不能配 ACL。
在 Bucket 创建页面,你会看到 “Object Ownership” 选项。默认是 “Amazon S3 object ownership: Bucket owner enforced”,这表示该 Bucket 的 ACL 处于禁用状态,你只能通过 Bucket Policy 来设置公共访问。
如果你更习惯用老一套的 ACL(把单个对象权限设为 public-read),那就得把 Object Ownership 勾成 “ACLs enabled”,然后选择 “Bucket owner preferred”。不过今天我更推荐统一用 Bucket Policy,两个机制混用容易把自己绕晕。
2.3 权限模型不要混着配
S3 的权限可以从四个层级去理解:IAM 用户权限、Bucket Policy、ACL、Block Public Access。很多权限异常问题,根源就在于这几个层级之间互相冲突。
我的实践原则是:公开访问只靠 Bucket Policy 控制,IAM 只控制谁能上传操作,ACL 干脆不用。 这样权限链路单一,排错时只需要排查 Policy 和互联网侧的访问效果。Block Public Access 则作为总开关,需要公读时取消阻止,不需要时保持开启。
3. 上传图片并生成公开链接:控制台操作全流程
环境搭好后,用控制台上传一张图是最直观的验证方式。哪怕你后面准备全走 CLI,也建议先在控制台完整走一遍,因为你能看到每一个配置项的位置,后面命令行里遇到概念才不会懵。
3.1 修改 Bucket 的 Block Public Access 设置
先进入你刚创建的 Bucket,找到左侧菜单 Permissions(权限),最上方就是 “Block public access (bucket settings)”。点 Edit,把 “Block all public access” 的勾去掉,保存。
这一步之后,Bucket 页面右侧会出现一个 Access 状态标签,显示 “Objects can be public”。如果这里仍然显示 “This bucket is not public”,说明总开关还没取消成功,回去重新检查。
这里有个反直觉的点:即使你取消了 Block Public Access,S3 控制台页面上依然会把这个 Bucket 称作 “public”,这是指“允许公开”,而不是“已经公开”。真正的公开取决于 Bucket Policy 有没有把 GetObject 放开。
3.2 写入一条公读 Bucket Policy
进入同一个 Permissions 页面,往下拉找到 “Bucket policy”,点 Edit,粘贴下面这段 JSON。注意把 your-bucket-name 替换成你自己的 Bucket 名。
json复制{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicReadGetObject",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::your-bucket-name/*"
}
]
}
这段策略的含义非常直白:允许所有人(Principal: "*")对 Bucket 下所有对象(Resource: "bucket/*")执行读取操作。保存后,AWS 会展示策略被激活的结果。
这里补充一点:为什么不是勾选 ACL 的 “public-read” 来完成公读?ACL 在 S3 里属于较老一套的授权方式,粒度和表达力都不如 Bucket Policy。尤其是 Bucket owner enforced 模式下 ACL 直接被禁用,你再纠结 ACL 只会多绕路。用 Policy 一锤定音,以后维护也只是改一段 JSON。
3.3 上传文件和获取 Object URL 的几种形式
回到 Bucket 的 Objects 标签页,点 Upload,把本地图片拖进去,直接点上传即可。因为前面已经写好了公读策略,这里不需要再单独设置对象的公共权限。
上传完成后,点击对象名,进入对象详情页。右侧或下方的 “Object URL” 就是那个可以直接打开的链接。点击它,浏览器会开新标签页并把图片渲染出来。
不过你可能注意到,控制台给的 Object URL 是这种格式:
code复制https://your-bucket-name.s3.ap-northeast-1.amazonaws.com/path/to/image.jpg
S3 对象 URL 其实是分场景的,主要有三种:
| URL 类型 | 格式示例 | 适用场景 |
|---|---|---|
| 默认 Object URL | https://<bucket>.s3.<region>.amazonaws.com/<key> |
泛域名访问 |
| 虚拟托管风格 | https://<bucket>.s3.amazonaws.com/<key> |
旧格式,部分工具兼容 |
| 静态网站托管端点 | http://<bucket>.s3-website-<region>.amazonaws.com |
启用静态网站托管后可用 |
如果发现默认 Object URL 能访问,但自己在浏览器拼的地址不对,多半是 region 或者 Bucket 名拼错了。可以直接从控制台的 Object URL 复制,省去手敲出错的概率。
3.4 图片打开却变成“下载”的根因:Content-Type 元数据
很多时候,链接放浏览器里能打开,但打开之后弹出来的不是图片预览,而是直接下载一个文件到本地。这个问题的根子不在权限,而在对象的 Content-Type 元数据。
S3 本身不知道你上传的文件应该按什么格式返回,它得靠对象的 Content-Type 元数据来判断。如果你上传时没有显式指定,S3 可能把它默认为 application/octet-stream,浏览器收到这个 Content-Type 只会当成二进制流下载,不会渲染成图片。
而控制台上传的图片,S3 通常会根据扩展名自动识别成 image/jpeg 或 image/png。如果你用的是 SDK 或 CLI 上传,传的时候不指定 --content-type,就很容易触发“下载”而不是“打开”。
如果不小心传错了,一种补救办法是用 CLI 把对象的元数据刷掉:
bash复制aws s3 cp s3://your-bucket-name/path/to/image.jpg s3://your-bucket-name/path/to/image.jpg \
--content-type "image/jpeg" \
--metadata-directive REPLACE \
--acl bucket-owner-full-control
这条命令本质上是用对象自己覆盖自己,但强制刷新了 Content-Type。执行完后,浏览器再打开链接就能直接预览。
4. 用 CLI 脚本化上传:批量、可控、可复用
控制台体验完之后,日常使用还是得切到 AWS CLI。尤其是一次传几十张、几百张图,或者需要在上传时统一指定 Content-Type、Cache-Control,控制台一个个点能点到手抽筋。
4.1 安装和配置 AWS CLI
先把 CLI 装上。macOS 上用 Homebrew:
bash复制brew install awscli
Ubuntu/Debian 上:
bash复制sudo apt update && sudo apt install awscli
装完后执行 aws configure,输入 IAM 用户的 Access Key ID、Secret Access Key,然后指定默认区域,比如 ap-northeast-1。输出格式建议填 json。
4.2 上传单个文件并保持公开属性
如果 Bucket 已经通过 Policy 全公开,上传时其实不需要额外加 --acl public-read。但如果你将来可能把某个 Bucket 从“全公开”改成“仅 ACL 公开”,那么上传时显式声明 --acl public-read 会多一层保险。
一条完整的上传命令长这样:
bash复制aws s3 cp ./image.jpg s3://your-bucket-name/images/image.jpg \
--acl public-read \
--content-type "image/jpeg" \
--cache-control "public, max-age=31536000"
拆开解释一下:
--acl public-read:给对象打上公共读标记。在 Bucket Policy 公读环境中,这行可加可不加;但在未配置 Policy 只想单文件公读时,这行就是关键。--content-type:显式告诉浏览器该对象是图片,避免出现上一节说的“变成下载”。--cache-control:设置浏览器缓存,图片这类静态资源长期缓存可以减少回源请求,省流量也省请求费用。
4.3 批量上传与同步
传整个目录:
bash复制aws s3 cp ./images/ s3://your-bucket-name/images/ \
--recursive \
--acl public-read \
--content-type "image/jpeg"
注意,--content-type "image/jpeg" 会把目录下所有对象都标成 JPEG。如果目录里有 PNG,就得分开处理,或者干脆让 aws 自动识别。
更常用的一条命令是 aws s3 sync,它会对比本地和 S3 的差异,只上传新增或改动的文件,非常适合持续构建场景:
bash复制aws s3 sync ./images/ s3://your-bucket-name/images/ \
--acl public-read \
--exclude "*.tmp"
--exclude 可以顺手排除临时文件。用 sync 还有个好处:如果本地删了某个文件,同步时加个 --delete 参数,S3 上对应的对象也会被删掉。
这里有个我踩过的细节:很多文件的 Content-Type 会因为扩展名不同而自动变化,sync 并不能 100% 保证每类文件都识别正确。对于特殊格式(比如 SVG、WebP),建议在同步前先自己确认一遍,或者用 --include / --exclude 组合分别指定类型。
4.4 一条条验证链接是否真的通
上传完后,别急着把链接甩给别人。用 curl 验证状态码是最快的:
bash复制curl -I "https://your-bucket-name.s3.ap-northeast-1.amazonaws.com/images/image.jpg"
返回 200 OK,说明对象可访问;返回 403 Forbidden,说明权限设置有问题;返回 404 Not Found,说明对象路径不对。后面排错章节会细说这三个状态码的坑。
也可以查询单个对象的 ACL,确认权限确实已经放开:
bash复制aws s3api get-object-acl --bucket your-bucket-name --key images/image.jpg
如果输出里有 "URI": "http://acs.amazonaws.com/groups/global/AllUsers" 且权限是 READ,代表这个对象已经是公开可读状态了。
5. 打开链接后却被 403:一次从报错到修复的完整排查链路
每次写 S3 权限相关的内容,这个问题必定会出现:“我按网上的教程一步步做了,为什么其他人打开链接还是 403?” 这里就完整复现一下排查思路,照着走基本都能解决。
5.1 先看状态码,区分三种常见情况
遇到访问异常,先看浏览器里返回的是什么状态码。
| 状态码 | 含义 | 最常见根因 |
|---|---|---|
| 403 Forbidden | 无权访问 | Block Public Access 仍开启 / Policy 没生效 / IAM 限制 |
| 404 Not Found | 对象不存在 | 路径写错 / 对象没上传成功 / 区域不对 |
| 405 Method Not Allowed | 请求方法不允许 | 试图对 S3 对象做非 GET 的匿名操作 |
403 是重灾区。你上传后能通过控制台看到对象,不代表外面的匿名用户能访问,因为 S3 的权限链路是层层判断的。
5.2 从“总开关”到“对象路径”的递进排查法
我的排查顺序是从外到内,从总开关到细节,不要一上来就怀疑 Policy。
第一步:检查 Block Public Access。 进入 Bucket 的 Permissions 页面,看 “Block public access (bucket settings)” 是不是全部关闭。如果这里还开着,无论 Policy 写成什么样,外面访问都是 403。这是最常见的状况。
第二步:确认 Bucket Policy 里的 Resource 是否正确。 很多人把 Policy 里的 Bucket 名写错,或者把 Resource 少写了 /*。注意,arn:aws:s3:::your-bucket-name 只匹配 Bucket 本身,arn:aws:s3:::your-bucket-name/* 才匹配下面的所有对象。公读策略必须加 /*。
第三步:确认对象路径确实存在。 用 CLI 列一下:
bash复制aws s3 ls s3://your-bucket-name/images/
确认你要访问的对象确实在 images/ 目录下,而不是在别的前缀下面。有时候控制台上传时按了“创建文件夹”,对象 Key 会带上额外的前缀,URL 里少写一层目录就会变成 404。
第四步:检查对象是否被别人单独设了“拒绝”。 虽然少见,但如果你用到过 ACL 并把一个对象设成了 private,Bucket Policy 的公读也无法覆盖它。处理方式就是回到对象权限里,把 ACL 改成 public-read 或直接删掉 ACL 改用 Policy。
第五步:直接请求一下,用返回体判断问题。 浏览器访问 403,返回的 XML 里通常会有一行 <Code>AccessDenied</Code>。把这个报错直接复制到搜索引擎,往往能立刻找到对应原因。
5.3 一个返工案例:我把 ACL 和 Policy 混用后遇到的怪问题
有次我给一个客户配图床,Bucket 取消了 Block Public Access,也写了公读 Policy,上传时也没忘加 --acl public-read。结果客户反馈:有些图片能打开,有些图片打开是 403。
我挨个对比了能打开和不能打开的图片,发现一个规律:凡是上传命令里带了 --acl public-read 的关键字段都对,但上传完之后我又批量执行过一次覆盖操作,那条命令没带 --acl 参数,而 Bucket 又开了 Bucket owner enforced。最后覆盖出来的对象,ACL 里的公共读可能被重置了,Policy 又因为某些条件没覆盖到,于是出现了“有的能开有的不能开”的怪异现象。
经过这个案例,我彻底统一了方案:公读只靠 Bucket Policy,上传时不依赖 ACL,覆盖操作必须携带原有元数据和权限,不再混用两套体系。这样之后,再没出现过类似问题。
这里给一条硬经验:做权限变更排查时,一次只改一个变量。 你同时动 Policy、动 ACL、动 Block Public Access,出了问题完全说不清是哪一个引起的。先把总开关关掉再打开 Policy,每改一步就访问一次链接验证,能省下大量返工时间。
6. 让公开链接更稳定、更省钱的几个进阶操作
到这一步,你的图片已经能通过 S3 的原始域名直接打开了。但如果这是正经线上项目,还有三件事值得尽快做。
6.1 挂一层 CloudFront 分发,而不是把 S3 域名直接暴露出去
直接开放 S3 公读没问题,但从性能和成本角度看,我更推荐在 S3 前面加一层 CloudFront CDN。
原因有两个:一是缓存。CloudFront 会把图片缓存到边缘节点,用户访问走的是最近节点,加载速度快很多,同时 S3 的请求量大幅下降,GET 费用也跟着下来。二是安全。CloudFront 支持 Origin Access Control(OAC),可以让 S3 Bucket 保持私有,只有 CloudFront 能通过 OAC 拉取文件,这样既能实现公开访问,又不用把 S3 本身裸露在公网上,降低被枚举和刷量的风险。
配置步骤很简单:
- 在 CloudFront 控制台创建分配,Origin domain 选择你的 S3 Bucket。
- Origin access 选择
Origin access control settings,创建一个新的 OAC。 - 保存后控制台会提示要不要自动更新 S3 的 Bucket Policy,选择“是”。
- 缓存策略保持默认的 CachingOptimized,行为策略里的 Viewer protocol policy 改成
Redirect HTTP to HTTPS。 - 如果需要自定义域名,先在 ACM 申请证书(注意证书要申请在
us-east-1),然后在分配里设置 Alternate domain name,再把 DNS 的 CNAME 指到 CloudFront 分配的域名。
6.2 用生命周期策略自动清理过期图片
很多人把图片传上去之后就不管了,结果存储成本每个月都在悄悄增加。可以给 S3 配上生命周期规则,按前缀自动降冷或删除。
比如,images/ 下的图片如果超过 180 天没人访问,转成 Standard-IA(低频访问)能省不少存储费;超过 365 天的旧图,直接过期删除。配置位置在 Bucket 的 Management → Lifecycle rules,创建规则时指定前缀、过渡动作和过期时间。
这里要提醒一句:生命周期规则是不可逆的,一旦执行删除,文件就没了。如果你删了又后悔,没有版本控制备份的话,只能靠本地文件恢复。建议重要图片至少保留一份在其他存储里,或者开启版本控制再配“删除非当前版本”的策略。
6.3 防盗刷与预算告警:公读链接也必须盯紧
公读链接相当于把一扇门敞开在公网上,门里面虽然只放图片,但防不住有人恶意刷流量。图片访问量一大,你的 S3 账单也会跟着跳。我见过的案例里,有人夜里被刷了几十万次 GET 请求,第二天一看账单直接傻眼。
所以,配完公读之后,有两件事必须立刻做:
第一,开启成本预算告警。 在 AWS Cost Explorer 里设置月度预算,比如 10 美元,超过 80% 就发邮件和短信通知。这样就算真被刷了,你也能第一时间知道。
第二,限制来源或请求频率。
如果是自有网站,可以在 CloudFront 的行为里绑定 AWS WAF 的 Rate Limit 规则,限制单个 IP 每秒的请求次数。这有点复杂,但对稳定运行很重要。
如果只是简单防一下,也可以在 S3 Bucket Policy 里加 Referer 条件,只允许特定域名过来的请求访问图片。比如:
json复制{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::your-bucket-name/*",
"Condition": {
"StringLike": {
"aws:Referer": "https://yourdomain.com/*"
}
}
}
]
}
不过这只是一个“防君子不防小人”的办法,因为 Referer 头可以被伪造。真要严谨防护,还是得上 CloudFront + WAF。
说到这,最后分享一点个人体会:公开读和私有读之间,不是判断题,而是选择题。如果只是临时给合作伙伴发几张图,预签名 URL 更省心;如果图片本来就是要展示给全网用户的,大大方方开 public-read,然后记得挂 CDN、写生命周期、配预算告警。
我自己现在做新项目,默认所有 Bucket 都是私有的,只有图床这类特定 Bucket 才开公读,而且命名里就带上 public- 前缀,防止哪天自己都被权限绕晕。毕竟 S3 权限这套体系,说简单也简单,说复杂真能让人怀疑人生。保持单一权限来源、一次性只改一个变量、每改一步就验证一次,这三个习惯能帮你避开绝大多数坑。
