☰
Uppy + AWS S3 + Node.js 实战:基于 Express 的双模式签名上传示例全解
2026/10/1 7:03:57 网站建设 项目流程
  • 前端
  • UI组件
  • 后端

【免费下载链接】uppy

The next open source file uploader for web browsers :dog:

项目地址:https://gitcode.com/gh_mirrors/up/uppy
点击查看免费下载

本篇指南围绕仓库中的 examples/aws-nodejs 示例展开,它是 Uppy 官方仓库内一个“开箱即用、完整可运行”的 AWS S3 上传参考实现。后端使用 Node.js + Express.js,前端使用 Uppy 的@uppy/aws-s3插件,演示了客户端签名(STS 临时凭证)与服务端签名(预签名 URL)两种主流模式,并集成@uppy/golden-retriever实现刷新页面后文件与分片上传状态不丢失。读完本文,你将掌握:S3 + IAM 的完整权限配置(CORS、Bucket Policy、用户策略)、两种签名模式下对象 Key 前缀的处理差异、@uppy/aws-s3的getCredentials/signRequest两个关键选项的用法,以及如何在本仓库中构建、配置并启动该示例。

示例概览:两种签名模式并存

示例通过GET /s3/sts与POST /s3/presign两个路由提供两种完全不同的签名策略,对应@uppy/aws-s3插件在客户端侧的两种配置方式:

模式服务端接口Uppy 侧选项签名位置浏览器请求流
客户端签名(STS)GET /s3/stsgetCredentials浏览器本地用 SigV4 签名浏览器 → S3(直连)
服务端签名(预签名 URL)POST /s3/presignsignRequest服务端生成预签名 URL浏览器 → 服务端签名 → 浏览器 → S3

两个 Demo 都挂载了@uppy/golden-retriever,因此已选文件以及进行中的分片(multipart)上传在页面刷新后都能继续。断点续传时会发起一次ListParts请求:服务端签名模式下经由/s3/presign,而 STS 模式下直接从浏览器打到 S3。

从源码看,@uppy/aws-s3插件在初始化时正是根据这三个选项分支选择底层 S3 客户端实现(见 packages/@uppy/aws-s3/src/index.ts):

  • 提供companionEndpoint→ 使用 Companion 服务端签名;
  • 提供getCredentials→ 使用S3mini客户端配合 SigV4 本地签名(要求s3Endpoint);
  • 提供signRequest→ 使用S3mini客户端配合自定义签名函数。

本示例覆盖的是后两种分支。

对象 Key 前缀:一处变更,两处同步

两个 Demo 上传的对象最终都落在uppy-nodejs-example/<random-uuid>-<filename>,但前缀是在不同位置应用的:

  • 服务端签名模式:POST /s3/presign在生成PutObject/CreateMultipartUpload命令时自行拼上前缀(objectKey =${directory}/${key}``,见 routes/presign.js),并把最终 key 返回给 Uppy,后者在upload-success事件中上报该 key。
  • 客户端签名模式:请求根本不经过/s3/presign,因此前缀必须在前端配置generateObjectKey时应用。示例在 public/index.html 中这样实现:
.use(AwsS3, { s3Endpoint: `https://${window.UPPY_S3_BUCKET}.s3.${window.UPPY_S3_REGION}.amazonaws.com`, region: window.UPPY_S3_REGION, generateObjectKey: (file) => `uppy-nodejs-example/${crypto.randomUUID()}-${file.name}`, getCredentials: async ({ signal }) => { // fetch('/s3/sts') → 返回 credentials + region }, })

需要特别警惕:STS 临时凭证的 IAM 策略被限定在该前缀下(arn:aws:s3:::${COMPANION_AWS_BUCKET}/uppy-nodejs-example/*,见 routes/sts.js),所以如果只改了一处而没同步另一处,上传必然以AccessDenied失败。

从插件源码看,generateObjectKey的默认行为是${crypto.randomUUID()}-${file.name}(见 packages/@uppy/aws-s3/src/index.ts),即示例中的写法其实就是在默认策略基础上加前缀。另外在companionEndpoint模式下,插件会直接使用file.name作为请求 key,把“决定最终存储位置”的权力完全交给服务端——这正是两种模式职责划分的体现。

分片上传的触发阈值与权限边界

@uppy/aws-s3默认只为大于100 MiB的文件启用分片上传(shouldUseMultipart的默认实现是(file.size || 0) > 100 * MB,见 packages/@uppy/aws-s3/src/index.ts)。低于该阈值时,每个文件都是单次PUT请求,因此/s3/presign中的 multipart 相关分支(POST创建、GETListParts、DELETE中止)以及对应的 S3 权限根本不会被触发。该选项也支持布尔值或自定义函数,例如始终分片或按文件类型判断。

对应地,服务端的预签名路由覆盖了全部 S3 操作(见 routes/presign.js):

请求体特征(method / uploadId / partNumber)生成的 AWS SDK 命令说明
PUT+ 无 uploadId/partNumberPutObjectCommand简单上传,可改写 key(拼前缀)
POST+ 无 uploadIdCreateMultipartUploadCommand创建分片任务,唯一可改 key 的 multipart 请求
PUT+ uploadId + partNumberUploadPartCommand上传单个分片
POST+ uploadIdCompleteMultipartUploadCommand合并分片
GET+ uploadIdListPartsCommandGolden Retriever 续传时列出已传分片
DELETE+ uploadIdAbortMultipartUploadCommand取消/中止上传

需要留意的是,除创建任务(POST+ 无 uploadId)外,其余 multipart 请求必须原样使用创建时返回的 key 进行签名,不能再次改动。所有预签名 URL 的有效期为 900 秒(expiresIn = 900)。

AWS 配置:CORS、Bucket Policy 与 IAM 用户策略

本节假设你熟悉 AWS 的 S3 与 IAM,且这些指引仅用于开发环境,不适用于生产——收紧安全策略超出了示例范围。示例以用户MY-UPPY-USER向桶MY-UPPY-BUCKET上传文件为场景。

1. 桶的 CORS 设置

对MY-UPPY-BUCKET配置如下 CORS 规则:

[ { "AllowedHeaders": ["*"], "AllowedMethods": ["GET", "PUT", "POST", "DELETE"], "AllowedOrigins": ["*"], "ExposeHeaders": ["ETag"] } ]
  • ETag必须暴露,否则分片上传无法完成(客户端依赖响应头中的 ETag 记录已传分片,S3mini.uploadPart会校验 ETag,见 S3mini.ts);
  • GET与DELETE仅用于 multipart 的 list-parts 与 abort 调用。

2. 桶策略(Bucket Policy)

将下面策略附加到MY-UPPY-BUCKET,并把ACCOUNT-ID替换成你的 12 位 AWS 账户 ID(S3 不接受 Principal ARN 中使用通配符):

{ "Version": "2012-10-17", "Statement": [ { "Sid": "MyMultipartPolicyStatement1", "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::ACCOUNT-ID:user/MY-UPPY-USER" }, "Action": [ "s3:PutObject", "s3:ListMultipartUploadParts", "s3:AbortMultipartUpload" ], "Resource": "arn:aws:s3:::MY-UPPY-BUCKET/*" } ] }

3. 用户策略(IAM Policy)

将以下策略附加到MY-UPPY-USER(客户端签名 / STS 端点必需):

{ "Version": "2012-10-17", "Statement": [ { "Sid": "MyStsPolicyStatement1", "Effect": "Allow", "Action": ["sts:GetFederationToken"], "Resource": ["arn:aws:sts::*:federated-user/*"] }, { "Sid": "MyStsPolicyStatement2", "Effect": "Allow", "Action": [ "s3:PutObject", "s3:ListMultipartUploadParts", "s3:AbortMultipartUpload" ], "Resource": "arn:aws:s3:::MY-UPPY-BUCKET/uppy-nodejs-example/*" } ] }

关键原理:S3 语句才是真正授权两个 Demo 上传的部分——GetFederationToken会话获得的是“该用户基于身份的策略”与“routes/sts.js调用时传入的会话策略”的交集。如果这里没有 S3 动作,交集为空,所有客户端上传都会以AccessDenied失败。而第 2 步的桶策略不参与这个交集——它指向 IAM 用户本身(arn:aws:iam::...:user/MY-UPPY-USER),而不是联邦会话(arn:aws:sts::ACCOUNT-ID:federated-user/123user,其中123user来自routes/sts.js中GetFederationTokenCommand的Name参数)。同账户场景下,第 2 步的桶策略是冗余的;只有当桶位于其他账户时才需要保留它。

补充:STS 端点的最小权限设计

示例中的/s3/sts是未认证端点,源码注释特别强调了权限范围要尽量收窄(见 routes/sts.js):AbortMultipartUpload具有破坏性,如果授权到整个桶,任何能访问该端点的调用者都能中止无关的上传任务。因此会话策略被限定在uppy-nodejs-example/*前缀下,expiresIn为 900 秒(15 分钟),响应还设置了匹配的Cache-Control: public,max-age=900,方便浏览器复用。

AWS 凭据的读取方式

你可以沿用已有的 AWS 凭据,也可在 IAM 页面新建用户,关键是正确配置并记录 Access Key ID 与 Secret Access Key。

本示例只从COMPANION_AWS_KEY与COMPANION_AWS_SECRET两个环境变量读取凭据(见 routes/sts.js 与 routes/presign.js)。两个客户端都显式构造了credentials对象,这会绕过 AWS SDK 的默认凭据提供链,因此AWS_ACCESS_KEY_ID、AWS_PROFILE与~/.aws/credentials均被忽略。如果想改回默认链,把两个文件中的credentials块删掉即可。

此外,routes/presign.js还会读取COMPANION_AWS_FORCE_PATH_STYLE环境变量(=== 'true'时启用),用于兼容 MinIO、LocalStack 等 S3 兼容存储的 path-style 寻址。

前置条件与依赖

运行该示例需要:

  • Node.js 22 或更新版本;
  • 整个uppy仓库的克隆。该示例是 Yarn workspace 的一部分,同时要从仓库根目录读取.env文件和 Uppy 浏览器 bundle,不能独立使用。

后端依赖见 package.json:@aws-sdk/client-s3、@aws-sdk/client-sts、@aws-sdk/s3-request-presigner(均为^3.338.0)、express(^5.2.1)、body-parser(^1.20.4)与dotenv(^17.4.2)。提供了start(node index.js)与dev(node --watch index.js)两个脚本。

安装、构建与配置

在仓库根目录执行依赖安装与浏览器 bundle 构建:

corepack yarn install corepack yarn build

构建步骤是必需的。服务端通过GET /uppy.min.mjs与GET /uppy.min.css优先服务本地构建产物 packages/uppy/dist/uppy.min.mjs;如果缺失,index.js会打印警告并回退到 CDN 上的旧版 Uppy(见 index.js),而该旧版的@uppy/aws-s3早于getCredentials与signRequest两个 API,Dashboard 仍能渲染,但问题只会在上传时报错时才暴露。

随后在仓库根目录(与package.json、.env.example同级,而不是examples/aws-nodejs/目录内)添加.env文件——index.js通过path.join(__dirname, '..', '..', '.env')加载它。可直接从模板开始:cp .env.example .env。本示例所需的最小配置如下:

COMPANION_AWS_BUCKET=MY-UPPY-BUCKET COMPANION_AWS_REGION=… COMPANION_AWS_KEY=… COMPANION_AWS_SECRET=… PORT=8080 # Optional, server-side signing only: path-style addressing for # S3-compatible endpoints such as MinIO or LocalStack. # COMPANION_AWS_FORCE_PATH_STYLE=true

注意:示例使用COMPANION_AWS_前缀的环境变量,是为了便于与本仓库其他示例集成,但它本身完全没有使用 Companion。仓库根目录的 .env.example 中还包含更多COMPANION_AWS_*可选变量(如COMPANION_AWS_PREFIX、COMPANION_AWS_USE_ACCELERATE_ENDPOINT、COMPANION_AWS_ACL、COMPANION_AWS_SSE等),供后续扩展参考,本示例并不读取它们。

启动与验证

在仓库根目录启动应用:

corepack yarn workspace example-aws-nodejs start

Dashboard Demo 随后可通过 http://localhost:8080 访问。页面上(见 public/index.html)包含两个并排的 Dashboard:“Sign on the client (STS credentials)”与“Sign on the server (presigned URLs)”。index.js在渲染首页时会把COMPANION_AWS_BUCKET与COMPANION_AWS_REGION注入为window.UPPY_S3_BUCKET/window.UPPY_S3_REGION全局变量供客户端读取。

两个 Demo 都注册了complete、upload-success、upload-error事件用于控制台输出。你可以选中一个小于 100 MiB 的文件和一个大于 100 MiB 的文件分别上传,观察控制台中upload-success上报的最终 key(应带有uppy-nodejs-example/前缀)以及分片任务是否触发;也可以上传到一半刷新页面,验证 Golden Retriever 的断点续传(ListParts请求会出现在服务端签名模式的/s3/presign调用中,或 STS 模式的浏览器直连 S3 请求中)。

深入理解:浏览器端 S3 客户端的底层行为

示例的前端配置背后,是@uppy/aws-s3内部S3mini客户端(源自 s3mini 项目,见 S3mini.ts)在承担请求发送与签名协调:

  • getCredentials模式:_createCredentialBasedSigner会先调用getCredentials获取临时凭证,再用createSigV4Signer构造 SigV4 签名器为每个请求生成预签名 URL;凭证带缓存(cachedCredentials),多个并发请求共享同一次 fetch(cachedCredentialsPromise)。
  • 令牌过期自愈:当 S3 返回ExpiredToken或InvalidAccessKeyId错误时,客户端会清空凭证缓存并以新凭证自动重试一次(request方法中的shouldRetryCredentials逻辑),这对 15 分钟有效期的 STS 凭证在实际使用中非常重要。
  • 上传实现:上传通过 XHR 进行以支持进度回调,内置 3 次指数退避重试、离线检测(等待online事件后自动恢复)以及停滞检测(ProgressTimeout)。resolvedKey的规则是:签名器返回的 key 非空则采用签名器 key,否则回退到请求 key——这正是服务端签名模式能改写对象 Key 的机制。

前端两个 Dashboard 的完整配置(STS 模式的getCredentials与预签名模式的signRequest)都在 public/index.html,是接入自有后端时最值得直接复用的参考代码。

小结

examples/aws-nodejs是一个麻雀虽小、五脏俱全的参考实现:它用两个 Express 路由完整覆盖了 Uppy 直连 S3 的两种主流签名方案,并把 IAM 权限、CORS、对象前缀、分片阈值与断点续传这些容易踩坑的细节都摆在了明面上。无论你是要在自己的 Node.js 后端接入 Uppy + S3,还是想理解@uppy/aws-s3的getCredentials/signRequest到底如何运作,这份示例都是极佳的起点。

  • 前端
  • UI组件
  • 后端

【免费下载链接】uppy

The next open source file uploader for web browsers :dog:

项目地址:https://gitcode.com/gh_mirrors/up/uppy
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询