本文是「阿里云 CDN / OSS / DCDN / 视频点播 / 视频直播 / 边缘安全加速」实践系列的第 2 篇,按天轮流输出六大产品的实战方案。上一篇讲了 CDN,本篇回到源站侧的基石——OSS。它既是 CDN 的回源仓库,也是绝大多数"上传"需求的终点。
一、理论篇:把"存什么、存多久、谁能访问"想清楚
OSS(Object Storage Service)是对象存储:扁平的命名空间里只有 Bucket(存储桶) 和 Object(对象 = Key + 数据 + 元数据),没有目录树(目录只是 Key 的前缀约定)。所有成本问题和安全问题,几乎都能归结为三个问题:存什么、存多久、谁能访问。
1. 存储类型:按"访问频率 × 时间"选
| 存储类型 | 适用场景 | 关键机制(以官方计费文档为准) |
|---|---|---|
| 标准 | 高频访问:图片、网站静态资源 | 默认类型,无取回费用 |
| 低频访问(IA) | 每月访问 1~2 次的数据 | 单价更低,但有最低存储时长(约 30 天)与按次取回费用;不足最低时长删除/转出仍按最低时长计 |
| 归档(Archive) | 极少访问:合规留存、半年前的日志 | 取回需解冻(Restore),解冻要等待 |
| 冷归档 | 年度级留存 | 解冻耗时更长、单价更低 |
| 深度冷归档 | 极长期合规归档 | 成本最低,解冻最慢 |
选型一句话:热数据标准,温数据低频,冷数据归档,转换交给生命周期规则自动做,而不是靠人手动迁移。
2. 权限模型:默认私有,按需最小开放
- Bucket 私有 + 签名访问是默认姿势:读文件用签名 URL,上传用签名直传(下文代码);
- 只有"网站静态资源"这类确定要公开的内容才用公共读,且应开启 阻止公共访问(Block Public Access) 与防列举(不给 ListObjects 权限);
- App/小程序端推荐 STS 临时凭证(有时效、可收权),服务端到端推荐签名 URL 或 V4 签名。
3. 生命周期与版本控制
- 生命周期规则按前缀定义"多少天后转换类型 / 多少天后删除 / 碎片上传多少天后清理",由服务端自动执行;
- 版本控制开启后,同名覆盖与删除都会保留历史版本,是误删事故的最后防线(注意:历史版本也计费,配合生命周期清理 NoncurrentVersion);
- 服务端加密建议直接开启(SSE-KMS 或 SSE-OSS),对业务透明。
二、实践方案总览:用户上传 + 日志归档的治理模板
以最常见的 Web 业务为例,一套可以直接抄的治理结构:
| 前缀 | 内容 | 权限 | 生命周期 |
|---|---|---|---|
static/ |
网站 JS/CSS/图片 | 公共读(供 CDN 回源) | 不设自动删除 |
user-uploads/ |
用户上传的图片/附件 | 私有,签名直传写入 | 30 天转低频,90 天转归档,365 天删除 |
logs/ |
应用日志归档 | 私有 | 90 天转归档,730 天删除;碎片 7 天清理 |
三、代码篇
1. 用 oss2 配置生命周期规则与碎片清理
pip install oss2
# oss_lifecycle.py
import os
import oss2
auth = oss2.Auth(
os.environ["ALIBABA_CLOUD_ACCESS_KEY_ID"],
os.environ["ALIBABA_CLOUD_ACCESS_KEY_SECRET"],
)
bucket = oss2.Bucket(auth, "https://oss-cn-hangzhou.aliyuncs.com", "my-bucket")
rules = [
# 用户上传:30天转低频 → 90天转归档 → 365天删除
oss2.models.LifecycleRule(
id="user-uploads-rule",
prefix="user-uploads/",
status=oss2.models.LifecycleRule.ENABLED,
transitions=[
oss2.models.LifecycleRule.Transition(
days=30, storage_class=oss2.BUCKET_STORAGE_CLASS_IA),
oss2.models.LifecycleRule.Transition(
days=90, storage_class=oss2.BUCKET_STORAGE_CLASS_ARCHIVE),
],
expiration=oss2.models.LifecycleRule.Expiration(days=365),
),
# 日志归档:90天转归档,730天删除;碎片上传7天清理
oss2.models.LifecycleRule(
id="logs-rule",
prefix="logs/",
status=oss2.models.LifecycleRule.ENABLED,
transitions=[
oss2.models.LifecycleRule.Transition(
days=90, storage_class=oss2.BUCKET_STORAGE_CLASS_ARCHIVE),
],
expiration=oss2.models.LifecycleRule.Expiration(days=730),
abort_multipart_upload=oss2.models.LifecycleRule.AbortMultipartUpload(days=7),
),
]
bucket.put_bucket_lifecycle(rules)
print("生命周期规则已下发")
提示:碎片(未完成的分片上传)不清理会一直占存储计费,给每个有上传场景的前缀都配一条
abort_multipart_upload。
2. 服务端签名后直传(Web 上传的标准解法)
直传的价值:文件流量不经过你的应用服务器,后端只负责"发许可证",带宽和内存压力直接消失。服务端用 Post Policy 生成签名:
# sign_upload.py —— 后端接口:返回一次性的直传凭证
import base64
import hashlib
import hmac
import json
import os
import uuid
from datetime import datetime, timedelta, timezone
REGION_HOST = "oss-cn-hangzhou.aliyuncs.com"
def gen_post_signature(bucket: str, ttl: int = 3600) -> dict:
upload_dir = "user-uploads/"
max_size = 10 * 1024 * 1024 # 10MB
expiration = (datetime.now(timezone.utc) + timedelta(seconds=ttl)).strftime(
"%Y-%m-%dT%H:%M:%S.000Z"
)
policy = {
"expiration": expiration,
"conditions": [
{
"bucket": bucket},
# 限定上传前缀:调用方只能写到 user-uploads/ 下
["starts-with", "$key", upload_dir],
# 限定大小:防止超大文件刷爆存储
["content-length-range", 1, max_size],
{
"success_action_status": "200"},
],
}
policy_b64 = base64.b64encode(json.dumps(policy).encode()).decode()
signature = base64.b64encode(
hmac.new(
os.environ["ALIBABA_CLOUD_ACCESS_KEY_SECRET"].encode(),
policy_b64.encode(),
hashlib.sha1,
).digest()
).decode()
return {
"host": f"https://{bucket}.{REGION_HOST}",
"access_key_id": os.environ["ALIBABA_CLOUD_ACCESS_KEY_ID"],
"policy": policy_b64,
"signature": signature,
"dir": upload_dir,
"expire": expiration,
}
前端拿到凭证后,用表单或 FormData 直接 POST 到 OSS(key 由前端拼时间戳/UUID 保证唯一):
async function uploadToOSS(file, credential) {
const key = `${
credential.dir}${
Date.now()}-${
crypto.randomUUID()}-${
file.name}`;
const fd = new FormData();
fd.append("key", key);
fd.append("policy", credential.policy);
fd.append("OSSAccessKeyId", credential.access_key_id);
fd.append("signature", credential.signature);
fd.append("success_action_status", "200");
fd.append("file", file); // file 必须是最后一个字段
const resp = await fetch(credential.host, {
method: "POST", body: fd });
if (resp.status !== 200) throw new Error("上传失败: " + (await resp.text()));
return `https://${
new URL(credential.host).hostname}/${
key}`;
}
3. 预签名 URL(服务端/脚本间传文件更省事)
def presign_put(bucket_name: str, key: str, ttl: int = 600) -> str:
bucket = oss2.Bucket(auth, f"https://{REGION_HOST}", bucket_name)
return bucket.sign_url("PUT", key, ttl, slash_safe=True)
# 生成���可以这样验证:
# curl -X PUT -T ./report.pdf "<返回的签名URL>" -v
# 返回 200 即成功;过期则返回 403,说明时效控制生效
两条路线怎么选:浏览器表单直选用 Post Policy(一次签名可传多个文件、天然限前缀限大小);App/脚本上传选预签名 URL 或 STS。
四、踩坑清单(生产环境真实踩出来的)
- 签名直传必须限前缀和大小:Policy 里没有
starts-with和content-length-range,等于把 Bucket 的写权限整个交给前端; - AK 绝不下发前端:浏览器直传用的是"后端签好的凭证",不是 AK;需要 finer 控制时用 STS 临时凭证;
- 同名覆盖:对象 Key 重复即覆盖,上传 key 一定拼 UUID/日期;重要 Bucket 开版本控制兜底;
- 低频访问的"最低 30 天":30 天内删除或转出照样按 30 天计费,别把高频变更的对象放低频(以官方计费文档为准);
- 公共读 Bucket 被列举:公共读只放单个对象读权限,绝不开 ListObjects;优先用"阻止公共访问"总开关收口;
- 碎片上传堆积:移动端弱网下分片上传大量中断,没有碎片清理规则,存储账单会悄悄变大;
- 前端直传 403 先查 CORS:给 Bucket 配 CORS 规则(允许 POST/PUT、暴露
ETag头),十个直传报错九个是 CORS。
五、小结与下一篇预告
本篇把 OSS 的三件事落成了代码:选型交给存储类型,留存交给生命周期,访问交给签名。核心代码三段:生命周期规则下发、服务端签名直传、预签名 URL,配合治理表格可以直接套到新项目里。
下一篇是系列第 3 篇:DCDN(全站加速)实践——动态加速原理、HTTPS 与 WebSocket 全站方案(含代码),把昨天 CDN 篇的"静态加速"推进到"动静混合全站加速",欢迎关注。
声明:本文为作者个人实践笔记,存储类型计费与限制的具体数字以阿里云官方文档为准。