ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Vue项目前端直传OSS实践:ali-oss、STS临时凭证与断点续传全解析

2026/9/19 3:33:10 拓冰建站 浏览量
Vue项目前端直传OSS实践:ali-oss、STS临时凭证与断点续传全解析 前端直传这个东西我是被逼出来的。项目上线后用户开始传大文件几十 MB 的设计稿、视频素材后端接一条传一条服务器带宽很快被打满上传接口和业务接口互相抢资源线上时不时报 504。后来我在 Vue 项目里通过 npm 接入阿里云对象存储 ali-oss把上传链路从浏览器 → 服务器 → OSS改成了浏览器 → OSS后端带宽压力瞬间归零上传速度反而更快用户体感也好了不少。这篇教程就是完整记录那次改造环境准备、npm 安装 ali-oss、RAM 权限与 STS 临时凭证、前端直传核心代码、断点续传以及我在实测中踩过的坑。适合正在做 Vue 项目、需要实现文件上传又不想靠堆服务器带宽解决问题的同学参考。1. 直传的账要算清楚为什么浏览器直连 OSS比后端中转划算1.1 带宽、延迟与成本的对比很多第一次接触直传的人会问后端转发不是也能用吗能但成本完全不一样。传统方案里文件先到你的 ECS再按你的程序逻辑二次转发到 OSS。这意味着文件流量要过服务器的公网带宽而一台小带宽的云服务器并发传几个大文件就满了。表面上你买的是服务器带宽实际上你在为文件传输这个本不该属于服务器的流量买单。直接上传的方案文件从浏览器直接进入 OSS 的存储节点服务器的出口带宽完全不参与剩下要做的只是在上传前向后端要一个临时凭证。OSS 本身有海量的带宽资源吞吐量远不是一台 ECS 能比的所以直传在延迟和成功率上通常都优于中转。两者的差异可以简单列一下对比项后端中转上传前端直传服务器带宽占用每传一个文件都占用几乎为零上传耗时多一跳转发延迟高直连存储节点延迟低大文件并发容易被带宽打满基本不受服务器限制后端复杂度要处理文件流、超时、重试只需提供 STS 下发接口断点续传依赖后端实现ali-oss 原生支持做这个改造最重要的收获是上传不再挤占业务接口的资源。以前上传接口一堵整个接口跟着卡现在服务器只处理一个响应很快的凭证接口业务接口稳如老狗。1.2 直传不等于把密钥放前端STS 是安全底线这是直传方案里最容易被误解的一点。网上有些古早教程直接把AccessKeyId和AccessKeySecret写在前端代码里然后告诉你可以上传了。这种做法我只能用埋雷两个字形容。AccessKey 是阿里云账号级别的长期凭证一旦从浏览器里被扒出来等于把整个账号的权限交了出去别人可以随意操作你的存储空间账单也能被刷爆。直传的正确姿势是使用 STS 临时凭证。流程是浏览器向后端请求 → 后端用长期 AccessKey 去阿里云 STS 服务换一个有时效、有权限范围的临时凭证 → 浏览器拿这个临时凭证初始化 OSS 客户端再上传。临时凭证通常只活 1 到 2 小时而且可以在 RAM 角色里限定它只能执行PutObject只能往指定 bucket 的指定目录下传文件即使被拿走危害范围也被压缩到很小。所以整个直传的安全模型可以理解为后端手里拿的是万能钥匙前端每次上传拿到的是一张指定房间、指定时间的门卡。钥匙绝不能出厂门卡随便给。2. 环境准备Vue 项目创建与 npm 的三个经典老坑2.1 用 Vite 快速创建 Vue 项目先说环境基线我推荐 Node.js 18 以上版本npm 9 以上。现在创建 Vue 项目我基本都用 Vite 脚手架比 Vue CLI 更轻更快命令也很简单npm create vitelatest oss-upload-demo -- --template vue cd oss-upload-demo npm install如果你的网络环境下载依赖很慢先配置 npm 使用国内镜像源这一步能省掉大量等待时间npm config set registry https://registry.npmmirror.com配完之后可以执行npm config get registry确认一下看到刚设置的地址就说明生效了。镜像源只在npm install下载包时起作用对你的项目代码没有影响放心用。2.2 npm 不识别、PowerShell 禁止脚本、镜像源配置新装 Node.js 的同学经常会在这里卡住我挑三个出现频率最高的环境问题说一下。第一个是命令行提示npm 不是内部或外部命令或者无法将 npm 项识别为 cmdlet、函数、脚本文件或可运行程序的名称。这说明 Node.js 安装目录没有加入系统 PATH。Windows 下在系统属性 → 环境变量 → Path里把 Node.js 的安装目录比如C:\Program Files\nodejs\加进去重新打开终端就好。这个问题的本质是所有命令行的可执行文件搜索路径里没有 npm跟 Vue、跟 OSS 都没关系。第二个是 PowerShell 下执行 npm 报无法加载文件 ...npm.ps1因为在此系统上禁止运行脚本。这是 Windows 默认的执行策略Restricted不允许运行.ps1脚本导致的。解决办法是以管理员身份打开 PowerShell执行Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser或者换到 CMD 下运行 npm也能绕过去。这个报错跟 npm 本身无关是 PowerShell 的安全策略在拦脚本。第三个是安装依赖时出现大量npm warn deprecated警告。比如node-domexception这类包提示 use your platforms native dome。看到 deprecated 先别慌它只是告诉你这个包不推荐继续用了不代表安装失败。npm 的退出码是 0、依赖能正常 require 就没问题,可以继续往下走。2.3 安装 ali-oss 依赖环境就绪后在项目里安装阿里云 OSS 的官方 Node.js SDKnpm install ali-oss装完之后注意看 package.json 的 dependencies 里有没有多出ali-oss同时确认npm run dev能正常启动。有一个小坑是 Vite 在打包某些 CommonJS 依赖时会报兼容性错误如果遇到ali-oss相关的构建报错可以改用它的浏览器版本引入import OSS from ali-oss/dist/aliyun-oss-sdk.min.js绝大多数情况下直接用import OSS from ali-oss就够了这条兜底方案先记在心里。3. 打通 OSS 权限链路RAM 子账号、STS 角色与 CORS 配置3.1 创建 RAM 子账号并授权开始写代码之前我建议先把云端的权限铺好。在阿里云控制台找到 RAM 访问控制创建一个子账号比如叫oss-uploader然后给这个子账号绑定一个最小权限的自定义策略。核心策略包含两块一个是oss:PutObject限定到指定 bucket 的指定目录另一个是sts:AssumeRole允许它去扮演为上传准备的 RAM 角色。策略 JSON 大致长这样{ Version: 1, Statement: [ { Effect: Allow, Action: [ oss:PutObject ], Resource: [ acs:oss:*:*:oss-upload-demo/uploads/* ] }, { Effect: Allow, Action: sts:AssumeRole, Resource: acs:ram::*:role/oss-frontend-role } ] }这里最关键的是把Resource写到uploads/*这一级而不是整个 bucket。这样即使临时凭证泄露别人也只能往 uploads 目录里写读不了你的其他数据更删不了文件。子账号创建好后一定要生成 AccessKey ID 和 AccessKey Secret。这份 AccessKey 只放在后端环境变量里比如 Spring Boot 的application.yml、Node.js 的.env永远不要出现在 Vue 源码里。后端语言无所谓前端只关心它最终返回的 JSON 结构。3.2 后端下发 STS 临时凭证我以 Node.js 后端为例说明一下实测过的 STS 下发接口。先安装官方包npm install alicloud/sts-sdk然后实现一个/api/oss/sts接口const express require(express) const StsClient require(alicloud/sts-sdk) const router express.Router() const sts new StsClient({ endpoint: https://sts.aliyuncs.com, accessKeyId: process.env.ALIYUN_AK_ID, accessKeySecret: process.env.ALIYUN_AK_SECRET }) router.get(/api/oss/sts, async (req, res) { try { const result await sts.assumeRole( acs:ram::1234567890123456:role/oss-frontend-role, frontend-upload, 3600 ) res.json({ code: 0, data: result.credentials }) } catch (error) { res.status(500).json({ code: error.code, message: error.message }) } }) module.exports router如果你后端是 Spring Boot思路完全一样只是用 Java SDK 调AssumeRole返回结构保持{ accessKeyId, accessKeySecret, securityToken, expiration }四个字段就行。前端不关心后端语言只按约定解析 JSON。3 600 秒过期时间是我比较习惯的值上传一个几百 MB 的文件绰绰有余同时泄露窗口也控制在 1 小时以内。生产环境建议把凭证有效期做成可配置配合前端在上传前判断过期时间快过期就重新拉。3.3 控制台里的 CORS 规则直传绕不开跨域浏览器里发起的上传请求天然带有 OriginOSS 默认不认。需要在 OSS 控制台的传输管理 → 跨域设置里加一条规则来源填你的前端域名比如https://admin.example.com调试阶段也可以填*允许 MethodsGET、PUT、POST、HEAD允许 Headers*暴露 HeadersETag、x-oss-request-id、x-oss-hash-crc64ecma暴露 Headers 很多人会漏但断点续传、进度回调、校验文件完整性都需要拿到这些响应头。尤其是x-oss-request-id排查问题时候服报错全靠它。4. 前端直传核心代码初始化客户端、拿凭证、传文件4.1 封装 OSS 客户端模块我把直传相关逻辑单独抽成一个模块放在src/utils/oss.js里所有组件共用。先看代码import OSS from ali-oss const REGION import.meta.env.VITE_OSS_REGION const BUCKET import.meta.env.VITE_OSS_BUCKET let client null let expireTime 0 async function getStsToken() { const res await fetch(/api/oss/sts, { credentials: include }) const result await res.json() if (result.code ! 0) { throw new Error(result.message || 获取STS凭证失败) } return result.data } export async function getOssClient() { // 本地缓存的client未过期就直接复用避免每次上传都请求STS if (client Date.now() expireTime) { return client } const token await getStsToken() expireTime new Date(token.expiration).getTime() - 60 * 1000 client new OSS({ region: REGION, accessKeyId: token.accessKeyId, accessKeySecret: token.accessKeySecret, stsToken: token.securityToken, bucket: BUCKET, secure: true }) return client }环境变量在项目根目录的.env里配置VITE_OSS_REGIONoss-cn-hangzhou VITE_OSS_BUCKEToss-upload-demo注意region不要带https://格式是oss-cn-hangzhou这种。secure: true会让 SDK 走 HTTPS生产环境必须开避免文件内容在传输中被劫持。这个模块里做了一层缓存同一个凭证有效期内的多次上传不会反复请求后端性能上更干净。同时留了 60 秒的提前量避免临界点上传到一半凭证失效。4.2 上传组件与进度展示在 Vue 组件里使用这个模块我一般搭配 Element Plus 的el-upload或者直接用原生input typefile。为了不引入太多框架依赖这里演示最直接的方式script setup import { ref } from vue import { getOssClient } from ../utils/oss const fileList ref([]) const uploading ref(false) const progress ref(0) async function handleFileChange(event) { const file event.target.files[0] if (!file) return uploading.value true progress.value 0 const objectName uploads/${Date.now()}-${file.name} const client await getOssClient() const result await client.put(objectName, file) if (result.res.status 200) { fileList.value.push({ name: file.name, url: https://${import.meta.env.VITE_OSS_BUCKET}.${import.meta.env.VITE_OSS_REGION}.aliyuncs.com/${encodeURIComponent(objectName)} }) } uploading.value false } /script template div input typefile :disableduploading changehandleFileChange / div v-ifuploading上传中.../div ul li v-foritem in fileList :keyitem.url a :hrefitem.url target_blank{{ item.name }}/a /li /ul /div /templateclient.put适用于 100 MB 以内的文件代码简单返回结果里有res.status可以判断是否成功。如果文件较大我会直接用下一节的multipartUpload它在内部按分片上传天然支持进度回调。进度展示推荐用multipartUpload的progress参数实现这个放到第 5 节详细展开。4.3 文件名、目录结构与访问地址设计直传最容易忽略的是对象命名规范。直接用用户原始文件名上传会有两个隐患一个是不同用户上传同名文件互相覆盖另一个是文件路径里带上/、中文、特殊字符会引发签名和访问问题。我的习惯是用业务前缀 时间戳 随机数 原始后缀来生成 objectName比如function buildObjectName(prefix, file) { const ext file.name.includes(.) ? file.name.split(.).pop() : const random Math.random().toString(36).slice(2, 8) return ${prefix}/${Date.now()}-${random}.${ext} }目录按业务维度划分比如用户头像放avatar/商品图片放goods/订单凭证放order/。这样后续做生命周期管理非常方便OSS 支持按目录前缀配置过期删除规则比如订单凭证一年后自动清除。访问地址拼接也有讲究。如果 bucket 是公共读直接用https://bucket.region.aliyuncs.com/objectName就能访问如果 bucket 是私有读这个 URL 访问会 403需要走签名 URL我留到下一节说。5. 进阶断点续传、并发控制、取消上传与加密校验5.1 multipartUpload 断点续传的用法当文件超过 100 MBput就不太合适了我改用 SDK 自带的分片上传multipartUpload。它的第一个好处是大文件不会因为一次性读取而撑爆内存第二个好处是支持断点续传。const client await getOssClient() await client.multipartUpload(objectName, file, { partSize: 1024 * 1024, // 每个分片 1 MB parallel: 4, // 并发分片数 progress: (percent, checkpoint) { progress.value Math.floor(percent * 100) // checkpoint 里记录了已经完成的分片列表 // 如果需要断点续传可以把它缓存到 localStorage 或 IndexedDB } })partSize我通常设置成 1 MB 到 5 MB 之间parallel设置成 4 到 8。这两个参数不是越大越好分片太小会导致请求数爆炸分片太大又起不到分片的意义。实测 1 MB 分片 4 并发在普通家宽和 4G 网络下表现比较稳定。如果在progress回调里把checkpoint存下来下次断网或者刷新页面后可以用uploadPart从断点继续不需要重新传整个文件。完整的断点恢复逻辑涉及从本地恢复 checkpoint 的分片列表代码量大一些但思路就是记住传了哪些分片再传剩下的。5.2 并发限制与取消上传用户经常会在上传大文件时后悔这时候如果没有取消机制文件会一直占着网络和后台队列。SDK 提供了abortMultipartUpload方法const client await getOssClient() const uploadId xxx // 需要从 multipartUpload 的返回值或 checkpoint 里拿到 await client.abortMultipartUpload(objectName, uploadId)更常用的做法是在上传过程中引入一个取消标记配合AbortController取消put或者直接调用一次 abort。我的经验是给上传任务包一个类管理任务状态pending、uploading、paused、done、errorUI 上按钮可以切换上传和暂停。这里的实现不复杂关键是 SDK 的回调要足够细让 UI 能及时感知每个状态。5.3 私有读桶的访问地址生成如果 bucket 的访问权限是私有前面拼出来的公开 URL 是打不开的。此时需要临时签名 URLconst client await getOssClient() const url client.signatureUrl(objectName, { expires: 3600 })这个方法会生成一个带签名的临时链接1 小时内有效。适合用户上传完成后预览自己的文件或者后端把签名链接下发给有权限的人。签名 URL 的过期时间同样不要太长避免被复制出去后长期有效。对于图片类文件如果你在 OSS 上开了图片处理服务还可以在 objectName 后面拼?x-oss-processimage/resize,w_200之类的参数生成缩略图签名 URL 同样支持。6. 实测报错与排查链路实录6.1 403 AccessDenied上传时遇到 403先别急着怀疑代码按下面链路排查第一步看响应体里的Code。最常见的两个一个是AccessDenied对应 RAM 策略里没有授权另一个是InvalidAccessKeyId对应 STS 凭证过期或者字段名传错。第二步确认 STS 凭证的securityToken是不是放在了stsToken字段。第三步检查 RAM 角色策略里Resource的uploads/*是否和 objectName 匹配——如果 objectName 是avatars/xxx.jpg而策略只允许uploads/*必定 403。还有一次我遇到的 403 比较隐蔽是 region 配错了。bucket 在杭州代码里写的oss-cn-shanghai报错提示 The bucket you are attempting to access must be addressed using the specified endpoint。这种问题最快的方法是去 OSS 控制台看 bucket 概览里的 Endpoint抄下来对着改。6.2 跨域 CORS 报错浏览器控制台出现 has been blocked by CORS policy 或者 Cross origin 字样基本可以锁定 CORS 配置问题。但 CORS 配置是分生效时间的改完控制台的跨域规则后通常需要等几分钟甚至十几分钟。我踩过一次改完规则立即刷新页面试还是报跨域一度以为是配置没保存后来等了十分钟再试就好了。如果配置没问题还是报跨域检查请求是不是带上了自定义 Header。SDK 默认会带x-oss-*系列请求头所以在允许 Headers里填*最省事。暴露 Headers 如果不配getResponseHeader(ETag)会拿到 null上传成功回调里要读响应头做校验时就会踩这个坑。6.3 npm 阶段的一系列环境报错这个教程走下来很多同学其实不是卡在上传代码而是卡在 npm 环境。我把高频报错和对应解法整理成一张表照着处理就好报错或警告原因处理方式无法将npm项识别为...Node.js 未加入 PATH配置系统环境变量 Path 指向 Node 安装目录npm.ps1 禁止运行脚本PowerShell 执行策略限制执行Set-ExecutionPolicy RemoteSigned或用 CMD安装慢、超时默认源在国外npm config set registry https://registry.npmmirror.comnpm WARN deprecated依赖包的弃用提示非致命确认退出码为 0 即可继续Cannot find native binding原生模块与 Node 版本不匹配删除 node_modules 和 lock 文件后重新 installnpm run build 报错Vite 打包兼容问题改用ali-oss/dist/aliyun-oss-sdk.min.js引入最后提醒一点npm run build打包后如果上传失败优先看打包产物里有没有把后端接口地址打进去。.env里的变量加了VITE_前缀才能在浏览器端代码里用如果接口地址放到生产环境应该指向线上后端记得用import.meta.env.VITE_API_BASE这类变量统一管理别让开发环境的地址残留在产物里。这套链路跑通之后后面再有上传需求基本就是换个目录前缀、改下策略权限的事不用再为一两个大文件焦头烂额了。