相关文章推荐
道上混的沙发  ·  云数据库 - 七牛云·  6 月前    · 

项目在使用aws的s3对象存储服务,需要后端将请求预签名后传给前端,前端拿这些签名信息跟s3交互。由于我没搞过s3的签名,需要花时间去调研官方的sdk等资料,花了一天时间终于搞出来了,碰了很多坑,在这里记录一下。

小知识,大挑战!本文正在参与“ 程序员必备小知识 ”创作活动

不要自己做签名

首先,AWS提供了签名的步骤和签名的组成信息,很多人以为不难,想要自己实现。其实非常不推荐,因为他们的文档只是做参考,让读者对签名的原理有个大体的认知,并不是严格的签名教程。

如果你真的要这么做,会发现出错率非常高,最常见的是签名不一致,你提供的签名跟AWS根据你负载计算而得到的签名不匹配,并且AWS没有帮你定位出错点,他只告诉你你错了。最终的结果就是你排查了半天也找不到原因所在。我就是在这里浪费了太多时间。

不过坑只有踩了你才知道这是坑,这也是一种收获,不是吗?

推荐用sdk来做签名,轻松又简单!

用SDK做预签名

首先需要安装一些依赖包,这里采用本文发稿时的最新版 aws-sdk

  • @aws-sdk/client-s3
  • @aws-sdk/s3-request-presigner
  • 第一个是s3的客户端sdk,第二个是预签名请求的sdk

    首先做s3客户端初始化工作:

    const {
      S3Client
    } = require("@aws-sdk/client-s3");
    const REGION = "****"; //e.g. "us-east-1"
    // Create an Amazon S3 service client object.
    const s3Client = new S3Client({
      region: REGION,
      credentials: {
        accessKeyId: "******",
        secretAccessKey: "**************",
    

    单文件上传签名示例

    写一个/upload-info的端点给前端做签名调用

    //引入相关模块
    const {
      PutObjectCommand,
    } = require("@aws-sdk/client-s3");
    const { getSignedUrl } = require("@aws-sdk/s3-request-presigner");
    app.get("/upload-info", (req, res, next) => {
      //初始化命令实体
      const putCmd = new PutObjectCommand({
        Bucket: "****",
        Key: "image.jpg"
      //获取签名
      getSignedUrl(s3Client, putCmd, { expiresIn: 3600 }).then((url) => {
          //将签名好的url回传给前台
        res.send(url);
        next();
    

    前端获取并使用签名请求

    request({
      url: `http://localhost:8080/upload-info`,
    }).then((signedUrl) => {
      request({
        url: signedUrl,
        method: "put",
        data: file,
      }).then((res) => {
        console.log(`single file upload succeed!`);
    

    分段上传签名示例

    写一个/upload-part端点给前端做签名调用

    //引入模块
    const {
      CreateMultipartUploadCommand,
      CompleteMultipartUploadCommand,
      UploadPartCommand,
    } = require("@aws-sdk/client-s3");
    const { getSignedUrl } = require("@aws-sdk/s3-request-presigner");
    app.get("/upload-part", (req, res, next) => {
      //object key of file(含文件在s3桶的目录结构,eg: `dir/book.pdf`)
      const Key = 'book.pdf'
      //count of parts
      const length = req.query.count;
      const createMultiUpload = s3Client.send(
        new CreateMultipartUploadCommand({
          Bucket: "***",
          Key,
      const getCompleteUrl = (UploadId) => {
        return getSignedUrl(
          s3Client,
          new CompleteMultipartUploadCommand({
            Bucket: "***",
            Key,
            UploadId,
          { expiresIn: 3600 }
      const prePromise = createMultiUpload.then((result) => {
        const { Key, UploadId } = result;
        return getCompleteUrl(UploadId).then((completeUrl) => {
          return {
            Key,
            UploadId,
            CompleteUrl: completeUrl,
      prePromise.then((result) => {
        const { Key, UploadId, CompleteUrl } = result;
        const signPartPromsArr = [];
        for (let PartNumber = 1; PartNumber <= length; PartNumber++) {
          const cmd = new UploadPartCommand({
            Bucket: "***",
            Key,
            PartNumber,
            UploadId,
          signPartPromsArr.push(getSignedUrl(s3Client, cmd, { expiresIn: 3600 }));
        //获取所有分片的签名URL
        Promise.all(signPartPromsArr).then((partsUrlArr) => {
          //签名全部结束,开始给前端回传
          res.send({
            partEndpoints: partsUrlArr,
            CompleteUrl,
            //below two is not necessary
            Key,
            UploadId,
          next();
    

    所以可以看到,整个签名包括三部分:

  • 通过sdk创建分段上传,获取上传Id
  • 根据桶、对象的Key、分段编号上传Id签名每一个分段端点
  • 根据上传Id签名完成分段上传的端点(通知s3进行分段合并)
  • 其中前段获得的两种签名:

  • 每一个分段的签名端点
  • 完成分段上传的签名端点
  • 至于前端的使用示例不再赘述,跟单文件使用本质一致的,只是细节多一点:

  • 最小切片size不可小于5MB(s3的硬性规定)
  • 并发数量的控制
  • 进度的提示
  • 所有分片上传完毕,需要将每段的编号和Etag(从responseHeader获取),按照从小到大的顺序拼成一个XML文本通过另一个签名端点(完成分段上传端点)发送给S3
  • 感谢阅读,如有任何问题,欢迎留言讨论!

    分类:
    前端