3.3 跨域、防盗链与临时密钥:图片安全三道门
有一天晚上我收到一条流量告警:CDN 回源流量在一小时内冲高了好几个 GB。我爬起来一查,发现不是社区的流量,而是一篇站外论坛的帖子直接嵌了我的图片地址。那张图存在一个公有读的存储桶里,任何人拿到链接就能无限引用,流量费全算我头上。帖子被转了几百次,一晚上烧掉的钱够我交一个月的存储费。
止血动作是第一时间把那个桶改成私有读写,社内引用全部改走签名 URL。但这件事让我意识到,安全配置不能出事再补,得有一套成体系的防线。事后我花了一个周末,给 COS 补上三道门,从此再没被刷过。
本篇讲的就是这三道门:防盗链、跨域 CORS 配置、STS 临时密钥。排序有讲究,从最基本的成本防盗,到前端直传必须跨过的技术门槛,再到密钥安全的终极方案。每一道我都会给出配置位置、我的踩坑过程和效果验证,你可以照着自己的项目一步步配。
第一道门:防盗链与 Referer 白名单
最直接的止血方式是设置 Referer 黑白名单。开启之后,COS 会检查请求头里的 Referer 字段,判断请求来源页面是不是我自己的域名,不是就拒绝。
配置在控制台就能完成:存储桶的安全管理里有防盗链设置,选白名单模式,把社区域名填进去。这里只有一个开关要小心:是否允许空 Referer。空 Referer 就是浏览器地址栏直接打开图片、或者某些客户端不带上来源字段的情况。图床的做法一般是允许空 Referer,因为用户体验是直接打开链接要能看到图;但如果你更在意防刷,关掉它会拦住一大批爬虫脚本。我自己选了允许空 Referer,真被刷了再说——事后证明这个决定救了我一次,但也仅限于一次。
为什么说 Referer 只能算第一道门?因为它能伪造。Referer 头是客户端自己发的,想改也就是几行代码的事。对付随手复制链接的人它够用了,对付有经验的爬虫不够。真正能防住刷量的,是把桶设成私有读写,用签名 URL 和 CDN 回源鉴权控制访问。Referer 白名单可以继续留着当一层低成本过滤,两者不冲突。
另外强调一点:防盗链规则是存储桶级别的,配一次全桶生效。同时对多个桶一一配过去不现实,所以一开始就规划好图床收敛到少数几个桶,后面所有安全策略都好维护。这部分涉及不少服务器侧的配置管理经验,想系统了解面板化运维的可以看1Panel 运维面板实战。
第二道门:跨域 CORS
第二道坎是技术性的。社区改版之后我想让网页直接上传图片到 COS,不经过后端中转,结果页面里上传组件一跑就报跨域错误。浏览器抛出的提示很明确:请求被 CORS 策略拦截。
跨域问题的根源是浏览器的同源策略:网页域名和对象存储域名不一致时,浏览器会先问 COS 这个存储桶允不允许我这个来源访问。没人回答它,它就默认拒绝。所以要在存储桶上主动声明 CORS 规则,等于告诉浏览器哪些来源、哪些操作方法是允许的。
对于会修改数据的写操作,浏览器在真正发送之前还会先发一个预检请求探路,确认服务器允许之后才上传正式数据。所以配规则时,预检涉及的 OPTIONS 方法被拒绝往往是第一道报错来源,看报错信息时注意区分是预检没过还是正式请求没过。
配置位置同样在存储桶的安全管理:跨域访问 CORS 设置。我加的规则大致是这样,建议照着自己前端的实际请求一一对应:
| 配置项 | 我的设置 | 说明 |
|---|---|---|
| 来源 Origin | 社区域名 + 本地调试域名 | 测试环境别忘了加,否则本地调试报同样错 |
| 操作 Methods | PUT、POST、GET | 直传需要写操作,读取也需要 |
| Headers | 允许的请求头 | 按前端框架需要配 |
| 超时 Max-Age | 600 秒 | 浏览器缓存预检结果,减少 OPTIONS 请求 |
我踩过的坑值得记一下:第一次我只配了 GET,想着先让读取通,结果前端直传用的 PUT 方法继续报错,报错信息里还是 CORS,我一度以为是缓存没刷新,清了半小时浏览器。配 CORS 的关键就是把前端实际用到的方法和来源都覆盖到,少一个都通过不了。另外本地开发域名(比如 localhost 加端口)一定要加进来源,不然下班前调试能多花你一个小时。配完之后清一遍浏览器缓存再验证,否则旧的预检结果可能继续误导你。
第三道门:STS 临时密钥,密钥绝不放前端
走完前两道门,最后一个问题也是最要命的一个:前端直传 COS,上传凭证从哪来?
最蠢的做法是把永久 SecretId 和 SecretKey 写进前端代码。密钥一旦随网页下发,等于把存储桶的钥匙交给了所有用户,任何人按 F12 都能看到,想删库只是愿不愿意的问题。这条红线绝对不能碰。
正确做法是后端签发 STS 临时密钥:后端调 COS 的 STS 接口,拿到一组带有效期的临时凭证(SecretId、SecretKey、Token 三个字段),下发给前端,前端拿着它在有效期内直传。临时密钥有两个好处:第一会过期,一般设 30 分钟以内;第二可以限定权限范围,只允许往指定目录上传,干不了别的事。
后端用官方 STS SDK 生成临时凭证的代码大致如下:
from qcloud_cos import CosConfig, CosS3Client
from sts.sts import Sts
config = CosConfig(
Region="ap-shanghai",
SecretId="COS_SECRETID",
SecretKey="COS_SECRETKEY",
Scheme="https",
)
sts = Sts(config)
response = sts.get_credential(
{
"duration_seconds": 1800,
"bucket": "pic-community-1250000000",
"region": "ap-shanghai",
"allow_prefix": "pics/2026/*",
"allow_actions": [
"name/cos:PutObject",
"name/cos:PostObject",
],
}
)
注意 allow_prefix 的妙处:它把前端的写权限锁死在当天目录里,前端的凭证永远碰不到历史数据。duration_seconds 控制有效期,我设的是 1800 秒,够上传完成就好,不用心疼——过期了前端再请求一次就是。这套思路也通用,灌到其他工具链里很方便,可以在DeepSeek 工具链实战里看到更多后端服务串接的实践。
临时密钥的整个运转流程串一遍是这样的:用户在页面点上传 → 前端调后端接口申请凭证 → 后端用永久密钥向 STS 换取临时凭证 → 前端拿凭证直接把图传进 COS → 前端把对象 Key 回传后端存库。全程永久密钥只待在后端,前端拿到的凭证过期快、权限窄,泄露了也只在允许的目录里能写文件,风险被压到了最低。
三道门的关系
| 门 | 挡住什么 | 怎么配 | 局限 |
|---|---|---|---|
| 防盗链 Referer | 站外页面直接嵌图 | 安全管理里设白名单 | 可被伪造,拦君子不拦小人 |
| 跨域 CORS | 无关站点的 JS 读写 | 声明允许的来源与方法 | 只是浏览器策略规则声明 |
| STS 临时密钥 | 永久密钥泄露 | 后端签发带时效的限权凭证 | 需要后端配合,多一次交互 |
三道门的定位完全不同:Referer 是低成本的过滤网,CORS 是浏览器规则规定的声明项,STS 临时密钥才是权限体系的根本。生产环境建议三层都配上,纵深防御比单点保险稳得多。
还有一条经验属于事后总结:安全配置再齐全,也得配上监控才算闭环。那次盗链事故我最庆幸的不是配了哪道门,而是云监控的流量曲线第一时间把我叫醒了。怎么给 COS 和 CDN 配流量告警,我在 5.3 篇云监控里详细写过,这里先埋个伏笔。
配完这三道门,我的图片存储才算真正能放心睡觉。不过安全只是花钱的故事的一半,另一半是怎么少花钱——旧图月月占着存储位,是时候上生命周期规则了。
延伸阅读
- COS 快速入门:对象存储官方文档,防盗链与 CORS 的配置入口都在这里。
- DeepSeek 工具链实战:后端服务密钥管理与脚手架实践。
- 1Panel 运维面板实战:服务器面板里统一管理配置与密钥的思路。
