项目在使用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");constREGION="****";//e.g."us-east-1"//CreateanAmazonS3serviceclientobject.consts3Client=newS3Client({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)=>{//初始化命令实体constputCmd=newPutObjectCommand({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(`singlefileuploadsucceed!`);});});
分段上传签名示例
写一个/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)=>{//objectkeyoffile(含文件在s3桶的目录结构,eg:`dir/book.pdf`)constKey='book.pdf'//countofpartsconstlength=req.query.count;constcreateMultiUpload=s3Client.send(newCreateMultipartUploadCommand({Bucket:"***",Key,}));constgetCompleteUrl=(UploadId)=>{returngetSignedUrl(s3Client,newCompleteMultipartUploadCommand({Bucket:"***",Key,UploadId,}),{expiresIn:3600});};constprePromise=createMultiUpload.then((result)=>{const{Key,UploadId}=result;returngetCompleteUrl(UploadId).then((completeUrl)=>{return{Key,UploadId,CompleteUrl:completeUrl,};});});prePromise.then((result)=>{const{Key,UploadId,CompleteUrl}=result;constsignPartPromsArr=[];for(letPartNumber=1;PartNumber<=length;PartNumber++){constcmd=newUploadPartCommand({Bucket:"***",Key,PartNumber,UploadId,});signPartPromsArr.push(getSignedUrl(s3Client,cmd,{expiresIn:3600}));}//获取所有分片的签名URLPromise.all(signPartPromsArr).then((partsUrlArr)=>{//签名全部结束,开始给前端回传res.send({partEndpoints:partsUrlArr,CompleteUrl,//belowtwoisnotnecessaryKey,UploadId,});next();});});});
所以可以看到,整个签名包括三部分:
通过sdk创建分段上传,获取上传Id
根据桶、对象的Key、分段编号上传Id签名每一个分段端点
根据上传Id签名完成分段上传的端点(通知s3进行分段合并)
其中前段获得的两种签名:
每一个分段的签名端点
完成分段上传的签名端点
至于前端的使用示例不再赘述,跟单文件使用本质一致的,只是细节多一点:
最小切片size不可小于5MB(s3的硬性规定)
并发数量的控制
重试机制
进度的提示
所有分片上传完毕,需要将每段的编号和Etag
(从responseHeader
获取),按照从小到大的顺序拼成一个XML文本通过另一个签名端点(完成分段上传端点)发送给S3
最后
感谢阅读,如有任何问题,欢迎留言讨论!