鸿蒙端云一体:手写 OBS Header 签名,做一个个人云相册 App
鸿蒙端云一体:手写 OBS Header 签名,做一个个人云相册 App
手机相册越攒越满,云备份又贵又不放心。这个案例用 HarmonyOS 的 ArkTS + 华为云 OBS,从零做一个自己的云相册:本地选图 → 逐文件上传 → 云端列表预览,全程只用对象存储,不写一行后端服务。
关键难点只有一个:OBS 的 Header 签名。官方 SDK 在鸿蒙侧不一定顺手,手写一遍反而更清楚。这篇把签名算法、逐文件上传、幂等与断点三件事拆开讲。
一、为什么选 OBS 直传,而不是自建后端
传统做法是端侧传后端、后端再转 OBS。多一层服务意味着多一台 ECS、多一份鉴权代码、多一个故障点。
OBS 支持客户端直接签名访问:端侧持有 AK/SK,对每个请求现算一个 Authorization 头,直接打对象存储的 REST 接口。省掉后端,代价是 AK/SK 落在端侧,所以这个方案只适合个人自用 App,不适合分发给他人——真要分发,必须换成 STS 临时凭证或后端代理。
这点先说清楚,因为它决定了整个架构。
二、OBS Header 签名到底怎么算
OBS 的签名走的是 HMAC-SHA1,构造一条规范化字符串(CanonicalizedResource),再用 SK 做两次 HMAC,最后 Base64。四步:
1. 拼 CanonicalizedResource
格式是 /{bucket}/{objectKey},如果请求带子资源(如 ?acl、?uploads)还要按字典序追加。上传单个对象时最简单:
/photo-album-bucket/photo_backup/v2/2026/11/xxx.jpg
2. 拼 StringToSign
Verb + 换行 + Content-MD5 + 换行 + Content-Type + 换行 + Date + 换行 + CanonicalizedResource
Date 必须是 RFC 1123 格式的 GMT 时间,和服务器时间偏差超过 15 分钟会直接 403——这是新手最常踩的坑,鸿蒙设备时间不准就全军覆没。
3. HMAC-SHA1 两轮
Signature = Base64( HMAC-SHA1( SK, HMAC-SHA1( SK, StringToSign ) ) )
注意是两轮,且两轮都用 SK 做密钥,不是先 SK 后 AK。
4. 组 Authorization 头
Authorization: OBS {AK}:{Signature}
三、ArkTS 里怎么算 HMAC-SHA1
鸿蒙的 CryptoArchitectureKit 提供 cryptoFramework.createMac(‘SHA1’)。核心片段:
import { cryptoFramework } from '@kit.CryptoArchitectureKit';
import { util } from '@kit.ArkTS';
async function hmacSha1(key: string, data: string): Promise<Uint8Array> {
const mac = cryptoFramework.createMac('SHA1');
const enc = new util.TextEncoder();
await mac.init({ algName: 'HMAC', key: { data: enc.encodeInto(key) } as cryptoFramework.SymKey });
const out = await mac.doFinal({ data: enc.encodeInto(data) });
return out.data;
}
async function obsSignature(sk: string, stringToSign: string): Promise<string> {
const first = await hmacSha1(sk, stringToSign);
const mac2 = cryptoFramework.createMac('SHA1');
const enc = new util.TextEncoder();
await mac2.init({ algName: 'HMAC', key: { data: enc.encodeInto(sk) } as cryptoFramework.SymKey });
const second = await mac2.doFinal({ data: first });
return new util.Base64Helper().encodeToStringSync(second.data);
}
Base64 用 util.Base64Helper,不要自己手写查表。
四、逐文件上传:为什么不做批量
相册备份动辄几百个文件,一口气 Promise.all 会把内存和网络同时打满,且任何一个失败都难定位。改成串行队列加单文件进度:
for (const file of pendingFiles) {
if (cancelled) break;
const md5 = await calcFileMd5(file.uri);
const key = buildObjectKey(file, md5);
await putObject(bucket, key, file, md5);
await recordDone(key, md5);
}
好处很直接:进度条是真实的,取消能立刻停,失败能精确知道是哪个文件。
五、对象键设计:让重复备份自动变幂等
如果键直接用文件名,同名照片会互相覆盖;如果用时间戳,同一张图每次备份都会重传。这里的做法是内容寻址:把本地 URI、文件大小、修改时间、MD5 一起参与生成键,前缀固定 photo_backup/v2/:
function buildObjectKey(f: MediaFile, md5: string): string {
const seed = f.uri + '|' + f.size + '|' + f.mtime + '|' + md5;
return 'photo_backup/v2/' + sha256Hex(seed).slice(0, 32) + extOf(f.uri);
}
同一张照片内容不变,键就不变,重复备份时先查本地记录,命中就直接跳过。MD5 同时塞进 Content-MD5 头,OBS 服务端会校验,传坏了立刻报错而不是留个坏对象。
需要说明的是:取消的当前文件要整文件重传,OBS 单次 PUT 不支持字节级断点续传;想要断点得走分段上传(Multipart Upload),那是另一个量级的复杂度,个人相册场景不必上。
六、云端预览:签名 GET 拿临时可访问链接
私有桶的对象不能直接给 Image 组件用,每个 GET 也要签名。做法是给预览单独算一个带 Expires 的链接:
GET /{bucket}/{key}?Expires={unix秒}
这时 StringToSign 里把 Expires 放在 Date 的位置。这样生成的链接在一段时间内有效,可以直接丢给图片组件加载,不用每次渲染都重新签。
七、踩过的坑
- 时间偏差:设备时间不准导致签名 403。上传前先拿一次服务端时间校准。
- Content-Type 不一致:签名时用了 image/jpeg,实际发请求却是 application/octet-stream,直接 403。签名用的头必须和真实请求完全一致。
- 对象键里的中文:URI 编码要在签名前统一做一次,签名后不要再编一次,否则重复编码。
- 并发太高:鸿蒙端并发 4 到 6 路比较稳,再多容易触发限流。
八、这个案例还能往哪扩
- 分段上传:超过 64 MiB 的视频走 Multipart,支持真正断点续传
- 时间轴视图:按对象最后修改时间分组,注意时区统一用 UTC 存、本地展示
- 跨设备恢复:云端 GET 拉回本地相册,重名不覆盖
- 文档归档:用系统文件选择器把 PDF、TXT 也纳入备份
小结
这个案例的技术含量不在 UI,而在把云厂商的签名协议手写对。一旦签名跑通,端侧直连云存储就是一条极短的通路:没有后端、没有中转、没有额外费用。
对鸿蒙开发者来说,这也是个不错的练手项目——ArkTS 的异步、文件 API、加密框架、网络请求全都会用到,而且成果是能天天用的东西。
案例仓库在鸿蒙开发实践中心可以找到,配套的建桶、取访问密钥、客户端配置步骤都在仓库 README 里。
- 点赞
- 收藏
- 关注作者
评论(0)