视频上传,尤其是一段可能上百MB、还要求在全端都能稳定跑通的短视频上传,在 uni-app 项目里绝对是一个绕不开又容易翻车的老大难。在做短视频类 App 时,我经常看到团队把大量时间耗在“微信小程序能传、App 不能传”“H5 传上去了但播放器打不开”这类问题上。其实核心原因很简单:uni-app 帮你统一了 UI 和交互层,但 H5、微信小程序、App 三端的本地文件能力和网络能力并不完全一致,OSS 的上传机制又有自己的一套规则,“全端兼容”自然没那么容易。

这篇文章我会从实际落地的角度,把一套适用于 uni-app 项目的全端 OSS 视频上传方案拆开讲清楚。核心路线是:服务端通过 STS 签发临时上传凭证,前端拿到凭证后直接把视频传到 OSS,上传完成后再调业务接口登记视频元数据。相比把视频先传到自建服务器再转发 OSS,这套方案不占服务器带宽、链路短,而且通过临时凭证做权限管控,不会把 AccessKeySecret 暴露在客户端。如果你正在做短视频发布、课程上传、实名认证录像、用户投稿这类功能,这篇文章应该能帮你避开不少真实项目里的坑。

1. 先把需求和坑位对齐:全端兼容到底难在哪

1.1 H5、小程序、App 的上传能力本来就不是一套逻辑

很多人第一次在 uni-app 里做上传,下意识会用 uni.uploadFile ,因为官方文档上说它支持 H5、小程序、App。这个说法没错,但它解决的是“把文件从本地上传到某个 HTTP 接口”这件事,OSS 直传的场景不太一样。

先看各端的真实情况。H5 端拿到的是一个 File/Blob 对象,浏览器环境下可以走 XMLHttpRequest 或者 FormData 提交。微信小程序端提供的是 wx.uploadFile ,而且上传域名必须在小程序后台配置到合法域名列表中,否则连请求都发不出去。App 端相对自由一些, uni.uploadFile 底层会调用原生的网络上传能力,但需要处理相册权限、文件路径、Android 各机型返回路径不一致等问题。

这几端的差异叠加在 OSS 上就更明显了。OSS 官方有 Web 端 JavaScript SDK,但那个 SDK 的设计目标是浏览器环境,默认使用 Blob/File 对象;在小程序里,文件是一个本地临时路径,不能直接当成 Blob 传给 SDK。如果你硬要套用官方 SDK,可能要在小程序里折腾 buffer、crypto 等兼容层,运行起来还有体积和内存问题,尤其视频文件动辄几十 MB,处理起来很不舒服。

所以我的建议是:不要在一个上传功能里把三种端的上传姿势全都自定义实现一遍,更不要因为某个端跑不通就做成“小程序走 A 方案、App 走 B 方案、H5 再走 C 方案”,那样测试成本会非常高。最好的方式是找一种所有端都能支持的公共协议,让代码只写一套。OSS 的 POST Object 表单直传,就是一个天然满足这个条件的方案。

1.2 为什么是“服务端签名 + 前端直传 OSS”

这里要先解释一下几种常见实现路线的差别。

第一种是把 AccessKeyId 和 AccessKeySecret 放在前端,初始化 OSS 客户端后直接上传。这个方案代码写起来最简单,但安全隐患非常大,等同于把仓库钥匙直接交给每个用户。只要有人从包里翻出密钥,你整个 Bucket 的数据都有可能被读走或删除,生产环境不建议考虑。

第二种是前端把视频上传到自己的业务服务器,业务服务器再转存到 OSS。很多刚开始做上传功能的团队都会选这条路,因为技术全部在自己手里,调试方便。但视频文件很大,这种方案会白白占用业务服务器的带宽、磁盘和 CPU。用户上传一份 100MB 的视频,你的服务器就得先收 100MB,再往外推 100MB,等于每份视频流量都绕了一圈,延迟高不说,服务器带宽费用一下子就上去了。类比一下,就是你网购一个商品,商家不直接发快递,而是先送到你家,再由你跑一趟送到收货人手里,纯属浪费。

第三种是服务端生成一个受限的临时凭证,客户端拿到凭证后直接上传到 OSS,文件不经过你自己的服务器。这个凭证就是 STS 临时凭证,它有有效期,而且可以限制权限范围,比如只能往某个 Bucket 的 videos 目录下写文件,不能读、不能删、不能覆盖其他目录。这样既安全,又不需要业务服务器做中转。加上 OSS 的 POST Object 支持标准的 multipart/form-data 表单提交,而 uni.uploadFile 本身就是提交这个格式的,所以三端可以共用一套上传代码。

1.3 先建立全貌:一整个视频上传流程长什么样

在实际代码动手之前,我建议你先在脑子里有一张完整流程图。视频上传不是“选个文件、传上去”这么简单,它通常包含下面几个环节:

  1. 用户在 App/小程序/H5 中选择视频,必要时做压缩;
  2. 前端调自己的后端接口,申请一个上传凭证,凭证里包含 OSS 的 host 地址、上传目录、policy、签名、STS token;
  3. 前端把视频文件和表单字段一起提交到这个 host,OSS 验证签名合法后接收文件;
  4. 上传成功后,前端拿着 OSS 返回的文件 key 调业务接口,告知后端这条视频已上传完成,后端再记录视频地址、时长、大小、封面图等元数据;
  5. 业务侧根据视频 file key 生成播放地址或者触发转码。

其中第 2 步是整个方案的安全核心,第 3 步是全端兼容实现的重点,第 4 步是很多项目会漏掉但非常影响数据一致性的环节。文章后面会按这个顺序逐步拆解。

2. 后端签名服务设计:安全与体验的平衡点

2.1 用 STS 临时凭证替代永久 AccessKey

我见过不少项目为了图省事,后端把主账号的 AccessKeySecret 直接拿来算签名,然后下发到前端。这相当于你把银行卡密码告诉了一个收银员,虽然收银员每次只刷固定金额,但只要密码泄露一次,整张卡就废了。正确做法是使用阿里云的 STS 服务,让后端扮演一个“临时授权中心”的角色。

STS 的核心概念有几个:RoleArn 是角色的唯一标识,RoleSessionName 是这个临时会话的名字,可以理解成“给这次授权起的备注名”,通常用用户名或用户 ID 拼接随机字符串,方便后续审计。DurationSeconds 是临时凭证的有效期,单位是秒,我一般给 900 秒,也就是 15 分钟,足够用户完成一次从选择视频到上传结束的操作。

后端通过 STS 的 AssumeRole 接口拿到临时 AccessKeyId、临时 AccessKeySecret、SecurityToken 和过期时间。拿到临时密钥之后,再去生成 OSS 表单直传需要的 policy 和签名。这里的 policy 不是角色权限策略,而是 POST Object 的上传条件约束,包括允许上传的过期时间、文件大小范围、文件 key 的前缀规则等。

很多资料讲到这里会把两个 policy 混在一起,容易把人绕晕。你可以这样理解:

  • RAM Role 的 Policy 控制“临时身份能操作哪些 OSS 资源”,是权限边界;
  • POST 表单里的 Policy 控制“本次上传文件满足什么条件才允许被接收”,是单次上传的门卫。

在实际代码里,最安全的组合就是两层都限制,不要偷懒。下面是一段 Java 端的 STS 签发示例,核心逻辑不复杂,替换成你自己的账号参数就能跑通:

DefaultProfile profile = DefaultProfile.getProfile(
    "cn-hangzhou",
    "<RAM_ACCESS_KEY_ID>",
    "<RAM_ACCESS_KEY_SECRET>");
DefaultAcsClient client = new DefaultAcsClient(profile);

AssumeRoleRequest request = new AssumeRoleRequest();
request.setRoleArn("acs:ram::1234567890:role/video-upload-role");
request.setRoleSessionName("app-video-" + System.currentTimeMillis());
request.setDurationSeconds(900L);

// 这个 policy 限制临时身份只能往指定 bucket 的 videos 目录写入文件
String rolePolicy = "{"
    + "\"Version\":\"1\","
    + "\"Statement\":[{"
    + "\"Effect\":\"Allow\","
    + "\"Action\":[\"oss:PutObject\"],"
    + "\"Resource\":[\"acs:oss:*:*:my-video-bucket/videos/*\"]"
    + "}]}";
request.setPolicy(rolePolicy);

AssumeRoleResponse response = client.getAcsResponse(request);
AssumeRoleResponse.Credentials credentials = response.getCredentials();
// credentials.getAccessKeyId()
// credentials.getAccessKeySecret()
// credentials.getSecurityToken()
// credentials.getExpiration()

这里需要提一个实践心得:如果你们项目比较小,暂时不想接 STS,也可以直接用 RAM 子账号的 AccessKeySecret 来做签名下发。这个子账号的权限需要提前配好,只允许 oss:PutObject ,并且资源范围限定到指定 Bucket 的 videos 目录。它的优点是实现简单,不必每 15 分钟跟 STS 服务打交道一次;缺点是权限粒度没有 STS 细,而且子账号密钥长期存在,一旦被拖库就需要立即轮换。两种方案都能上线,但如果你是在做对外发布的产品,我还是建议一步到位接 STS,安全底子打好,后面不用返工。

2.2 POST Object 表单直传的签名规则

拿到临时密钥之后,后端要生成一组表单直传参数。先构造一个 policy JSON:

{
  "expiration": "2026-01-01T12:00:00.000Z",
  "conditions": [
    ["content-length-range", 0, 209715200],
    ["starts-with", "$key", "videos/2026/"]
  ]
}

expiration 必须使用 UTC 时间格式,并且比当前时间晚一些。conditions 中的 content-length-range 用来限制文件大小,我这里写的 209715200 是 200MB,你可以根据产品需要调整。starts-with 限制了上传 key 必须匹配一个前缀,比如 videos/2026/,这能有效防止用户往任意路径上传文件。

然后把这个 JSON Base64 编码,编码后的字符串用临时 AccessKeySecret 做一次 HmacSHA1 签名,签名结果再 Base64 编码,就是 signature。需要注意签名对象是 base64Policy 字符串本身,不是原始 JSON,也不要画蛇添足在字符串后面加换行。

服务端最终返回给前端的结构大概是这样:

{
  "accessid": "STS 临时AccessKeyId",
  "securityToken": "STS SecurityToken",
  "policy": "Base64 之后的 policy",
  "signature": "HmacSHA1 签名结果",
  "host
Logo

火山引擎视频云技术社区,是面向 AI 音视频开发者的技术交流平台。这里汇聚源自抖音、豆包等亿级 DAU 产品的 RTC、直播、点播、AI 媒体处理、音视频互动技术,提供接入指南、最佳实践、性能调优、场景案例、Demo 代码、开源项目、白皮书和 API 文档。社区汇聚官方工程师与一线开发者,为 AI 视频通话、数字人、AI 视频处理等应用的开发与落地提供技术支持。

更多推荐