用Node版aws-sdk预签名请求

1,460 阅读3分钟

项目在使用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

最后

感谢阅读,如有任何问题,欢迎留言讨论!