4.2 图片直传:COS 临时密钥与前端上传
4.1 把文章记录的写入跑通了,但封面字段还是空的——用户选了图,总不能让图片跟着 JSON 一起 base64 塞进数据库。这一节聊发布链路的第二段:图片怎么从前端直达对象存储。核心是临时密钥方案:既不让前端拿到永久密钥,也不让图片绕道云函数多跑一趟。
场景:一张封面图的去向
需求很普通:用户在编辑器里选一张图当封面,上传完拿到地址,填进 4.1 的 cover 字段再点发布。约束有两条:图片平均一两兆,大的时候十几兆;我们的预算很薄,不想为流量反复付费。
最早想的是最省事的方案:前端用云开发 SDK 的 uploadFile,三行代码搞定,鉴权全托管:
// 最省事的 SDK 托管上传
const res = await app.uploadFile({
cloudPath: `covers/${Date.now()}.jpg`,
filePath: file,
})
const cover = res.fileID // fileID 直接存进 cover 字段
跑通是跑通了,但很快就不满足:我想按用户隔离目录、给不同目录配不同权限、还要在 COS 侧直接用图片处理参数生成缩略图,托管模式都做不到。于是换成了临时密钥直传,这条路是云开发存储的底层玩法,也最能讲清楚权限边界。
先给三套方案算笔账:
| 方案 | 上传链路 | 优点 | 缺点 |
|---|---|---|---|
| SDK 托管上传 | 前端到云开发存储 | 三行代码,零配置 | 目录权限不能精细控制 |
| 云函数中转 | 前端到函数再到 COS | 能压缩加水印 | 双倍流量,大文件易超时 |
| 临时密钥直传 | 前端直连 COS | 流量省、速度快、权限可收窄 | 签名逻辑要自己写 |
云函数中转第一个被毙掉:十几兆的图走函数,函数超时风险大不说,上行下行各算一次流量,穷项目受不了。托管上传留在小图场景兜底,主链路走临时密钥。
方案:云函数发临时密钥,前端直传 COS
思路是云开发存储底层就是 COS,云函数里用 STS 服务签一张临时凭证,凭证只允许往某个目录写文件,有效期半小时。前端拿到凭证直接和 COS 通信,云开发只过一道签名,不碰文件流。
先定目录规则,权限设计全靠它:
| 目录 | 用途 | 写权限 | 读权限 |
|---|---|---|---|
| articles/{openid}/ | 文章配图 | 仅本人 | 公开 |
| avatar/{openid}/ | 头像 | 仅本人 | 公开 |
| covers/ | 官方封面 | 仅云函数 | 公开 |
发签名的云函数:
// cloudfunctions/getUploadCredentials/index.js
const STS = require('qcloud-cos-sts')
const cloud = require('wx-server-sdk')
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })
const cos = {
secretId: process.env.TENCENTCLOUD_SECRETID,
secretKey: process.env.TENCENTCLOUD_SECRETKEY,
bucket: 'my-app-12345678',
region: 'ap-shanghai',
durationSeconds: 1800,
}
exports.main = async () => {
const { OPENID } = cloud.getWXContext()
const key = `articles/${OPENID}/${Date.now()}-${Math.random().toString(36).slice(2, 8)}.jpg`
const policy = {
version: '2.0',
statement: [{
action: ['name/cos:PutObject', 'name/cos:PostObject'],
effect: 'allow',
resource: [`qcs::cos:${cos.region}:uid/${cos.uid}:${cos.bucket}/articles/${OPENID}/*`],
}],
}
try {
const credential = await STS.getCredential({ ...cos, policy })
// cos.uid 是主账号 ID,从控制台拿,写在环境变量里
return { code: 0, credential, key, url: `https://${cos.bucket}.cos.${cos.region}.myqcloud.com/${key}` }
} catch (e) {
console.error('签发临时密钥失败', e)
return { code: 500, msg: '上传服务繁忙' }
}
}
前端用 cos-js-sdk-v5 直传,注意拿凭证是异步的:
import Cos from 'cos-js-sdk-v5'
const cos = new Cos({
getAuthorization: async (options, callback) => {
const res = await app.callFunction({ name: 'getUploadCredentials' })
const c = res.result.credential
callback({
TmpSecretId: c.tmpSecretId,
TmpSecretKey: c.tmpSecretKey,
SecurityToken: c.sessionToken,
StartTime: c.startTime,
ExpiredTime: c.expiredTime,
})
},
})
async function uploadCover(file, onProgress) {
const res = await app.callFunction({ name: 'getUploadCredentials' })
const { key, url } = res.result
await cos.putObject({
Bucket: 'my-app-12345678',
Region: 'ap-shanghai',
Key: key,
Body: file,
onProgress: (p) => onProgress(Math.round(p.percent * 100)),
})
return url // 填进 4.1 的 cover 字段
}
上传完拿到的就是一个普通 CDN 地址,想要缩略图在 URL 后面拼参数即可:?imageView2/2/w/400,列表页用小图,详情页用原图,流量又省一半。
踩坑与排查
第一个是密钥泄露。上线前 review 的时候发现一版历史代码把 SecretKey 写进了前端配置文件,理由是反正打包进去了没人看见——这是典型的负向安全感。临时密钥方案上线后,即使前端被扒,拿到的凭证也只能写自己 openid 目录下的文件,有效期半小时,危害可控。
第二个是 resource 路径写错导致整条链路 403。STS 的 resource 字段里 bucket 要带 appid 后缀,region 要和 bucket 实际地域一致,少一个冒号就全部签名失败。调试技巧是开 STS 的 debug 日志,拿返回的签名直接用 curl 验一遍,别在前端来回试。
第三个是大文件上传到一半凭证过期。前端十几兆的图在弱网下可能传十几分钟,而凭证只有半小时。后来超过 10MB 的文件改用 cos.uploadFile 的分片上传,并且每次分片前检查剩余有效期,不足五分钟就重新签一张凭证续上。
第四个是 CORS。前端直连 COS 必须在存储桶配置里加上 Web 域名来源,允许 PUT 和 POST 方法,否则浏览器直接拦截,而且报错信息非常迷惑,看起来像网络断了。本地联调时 localhost 也要加进去。
小结
直传这套方案的收益是实实在在的:函数调用次数少了、大文件不受函数超时限制、流量只算一遍。代价是多写一个签名函数和一套 CORS、分片续签的细节。这套存储与签权的组合在本书目录的存储部分有统一说明,另一个项目的对象存储实操可以看灯塔站点实战。下一节解决图片和文字进去之后的合规问题,见内容审核一节,那是应用上线前最后一道硬门槛。
延伸阅读
- Python 云开发实战目录:直传前后章节的索引
- 灯塔站点实战:对象存储配合静态站点的完整案例
- 腾讯云实践:COS 常见配置问题汇总
