一、前言
开门见山的说,笔者已经被AWS坑的体无完肤了,文档难找、SDK版本繁多老版本没有注释、例子不全还有误导的情况、MD5-Hex不用一定要用MD5-Base64等等各种问题导致在使用的过程中各种卡壳,不过好在最终还是把问题解决,才有了今天给大家带来了【爬坑指南】,我们先从要做一件什么事情开始说起:
资源汇总:
- AWS S3 文档:https://docs.aws.amazon.com/AmazonS3/latest/userguide/Welcome.html
- 获取文件MD5-Base64:https://the-x.cn/zh-cn/hash/MessageDigestAlgorithm.aspx
- Js-MD5:https://www.npmjs.com/package/js-md5
- 上传进度条:https://dev.to/codebeast/monitoring-upload-and-download-progress-for-amplify-storage-2akb
二、文件中心?上传文件很难吗?
首先上传文件并不难,from-data谁还不会呢?但是要把文件传好传安全很难,考虑点会非常多:
- 运维层面:
- 用什么存储资源?
- 使用了不同的云有差异怎么办?
- CDN用哪家的?能不能授权?
- 文件上传服务器无状态POD服务是否需要挂在共享磁盘
- 跨域配置
- 安全层面:
- 谁传的文件谁访问,不能越权
- 用户上传身份校验
- 文件UGC和安全检查
- 上传控制频率
- 上传文件名随机化
- 上传存储的的secret key安全保存2
- 后端校验文件大小等后缀验证以及文件头信息逻辑
- 文件一致性校验
- 功能层面:
- 缩略图 OCR
- 断点续传 or 分片上传 or 文件秒传
- 视频压缩格式转码
- 未使用的文件自动清理,节省资源
在根据业务的不同还有更多的扩展性要求。
如果当业务遇到文件上传场景的时候把这一套做了一遍,然后另外一个业务也需要使用在重来一遍就非常头疼了,所以就诞生出做一个通用的文件上传的想法了。
三、为什么要用前端直传?
如果在遇到内部通讯协议使用Grpc或者是http-json为主,还需要对于网关鉴权进行改造和单独配置,会有非常多的额外成本,所以考虑使用前端直传的场景成本最低,而且因为各家云厂商提供的SDK成熟度高各项功能都有 (这里是第一个失策阿里云和腾讯云等都有非常完善的JS-SDK,没想到AWS提供的非常的简单)
并且前端也可以封装成一个标准SDK和后端对应,遇到文件上传场景抄起来就直接用就可以了。
整个流程大家可以参考下面这张流程图,整体分为五个阶段:
- 前端获取上传凭证,服务端校验用户上传行为合法性后申请凭证返回
- 前端只传文件到对应的对象存储
- 完成上传上报服务端完成上传,服务端下载文件进行校验,没问题返回临时访问URL
- 前端完成表单填写提交到业务服务端,业务服务端通过文件资源ID告知文件中心文件被使用,保存资源ID(如果是公开访问的文件文件中心可以直接返回长授权URL)
- 用户访问资源时使用资源ID换取临时访问授权
四、AWS Go SDK 创建预签名URL × Postman模拟上传
首先我们需要确定需要使用AWS的哪些能力:
- 上传文件校验MD5完整和文件长度 AWS进行一轮校验(可选),防止申请的预签名的文件和实际的不符合
- 定义文件是否可以被公开访问 ACLPrivate
- 授权时效 上传时效和被临时访问时效
package test
import (
"context"
"fmt"
"testing"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
)
const AK = "XXXX" // AWS AK
const SK = "XXXX" // AWS SK
const BUCKET = "XXXX" // AWS BUCKET Name
type Credential struct {
Ak string
Sk string
}
func (c *Credential) Retrieve(ctx context.Context) (aws.Credentials, error) {
return aws.Credentials{
AccessKeyID: c.Ak, SecretAccessKey: c.Sk,
}, nil
}
var client *s3.Client
func init() {
cred := &Credential{
Ak: AK,
Sk: SK,
}
cfg := aws.Config{
Region: "ap-southeast-1",
Credentials: cred,
BearerAuthTokenProvider: nil,
HTTPClient: nil,
EndpointResolverWithOptions: nil,
RetryMaxAttempts: 0,
RetryMode: "",
Retryer: nil,
ConfigSources: nil,
APIOptions: nil,
Logger: nil,
ClientLogMode: 0,
DefaultsMode: "",
RuntimeEnvironment: aws.RuntimeEnvironment{},
}
client = s3.NewFromConfig(cfg)
}
func TestAWSV2PresignPutObject(t *testing.T) {
fmt.Println("Create Presign client")
presignClient := s3.NewPresignClient(client)
filemd5 := "/SX1AAfPuVitH7ZK9bNg6Q==" // 前端上传文件给到 AWS-S3 时文件MD5校验(可选)
fileLen := 5616111 // 前端上传文件给到 AWS-S3 时文件大小校验(可选)
filename := "/demo/test.jpg" // 文件存储bucket的路径和名称
presignResult, err := presignClient.PresignPutObject(context.TODO(), &s3.PutObjectInput{
Bucket: aws.String(BUCKET),
Key: aws.String(filename),
ACL: types.ObjectCannedACLPrivate,
ContentMD5: &filemd5,
ContentLength: int64(fileLen),
}, func(po *s3.PresignOptions) {
// 授权时效
po.Expires = 48 * time.Hour
})
if err != nil {
panic("Couldn't get presigned URL for GetObject")
}
fmt.Printf("上传URL: %s\n", presignResult.URL)
presignResult, err = presignClient.PresignGetObject(context.TODO(), &s3.GetObjectInput{
Bucket: aws.String(BUCKET),
Key: aws.String(filename),
}, func(po *s3.PresignOptions) {
// 授权时效
po.Expires = 48 * time.Hour
})
if err != nil {
panic("Couldn't get presigned URL for GetObject")
}
fmt.Printf("访问URL: %s\n", presignResult.URL)
}
使用上述代码我们就能得到以下内容:
上传URL: https://xxxx.s3.ap-southeast-1.amazonaws.com/demo/test.jpg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIASQSSOHDZ5AN5LXUF%2F20221021%2Fap-southeast-1%2Fs3%2Faws4_request&X-Amz-Date=20221021T135608Z&X-Amz-Expires=172800&X-Amz-SignedHeaders=content-md5%3Bhost&x-id=PutObject&X-Amz-Signature=e23da84351a1afa7faaabfad533a26c8f2dbeb0b14fc4384560b40622726cbaa
访问URL: https://xxxx.s3.ap-southeast-1.amazonaws.com/demo/test.jpg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIASQSSOHDZ5AN5LXUF%2F20221021%2Fap-southeast-1%2Fs3%2Faws4_request&X-Amz-Date=20221021T135608Z&X-Amz-Expires=172800&X-Amz-SignedHeaders=host&x-id=GetObject&X-Amz-Signature=7af80761fd063ed554827b5ab3e7d00b35cf67d98221157bbb39d247865a152e
我们可以通过postman来调用进行文件上传,使用put方式,binary上传文件二进制
这里千万不能使用from-data 因为上传访问是把所有的body内容保存在文件中,一定要使用binary的方式,如果你使用文件大小校验和md5校验一定会提示校验不通过,因为文件内容和实际文件内容不一样,如下:
----------------------------642585057435781546021821
Content-Disposition: form-data; name=""; filename="test.txt"
Content-Type: text/plain
omori
----------------------------642585057435781546021821--
对于使用了长度限制和MD5校验的前端在请求过程中需要在Headers里面增加对应的配置,才能通过sign校验,因为服务端在生成签名的时候 md5和len也参与了运算,http状态码返回200,恭喜你已经完成了上传动作了
AWS-S3使用的MD5-Base64,我们场景的MD5 32位是16进制的表示方式,但要注意不是讲16进制的MD5进行base64,是需要对原始的byte数组的MD5进行Base64,比如js-md5:
md5 = require('js-md5');
console.log(md5.base64("test"))
console.log(md5('test'))
console.log(md5.array('test'))
CY9rzUYh03PK3k6DJie09g== <-这个是正确的
098f6bcd4621d373cade4e832627b4f6
[
9, 143, 107, 205, 70,
33, 211, 115, 202, 222,
78, 131, 38, 39, 180,
246
]