1. 背景
1.1. 阿里云OSS的SDK接入
由于项目需求,本文作者于2024年11月08日(日期很重要)使用GO的web框架GIN接入阿里云OSS的SDK进行开发。刚开始使用的是aliyun-oss-go-sdk
这个SDK,后面发现有更新的,当然是用新的了。使用go get github.com/aliyun/alibabacloud-oss-go-sdk-v2
下载SDK后引入。代码如下:
package aliyun_util
import (
"context"
"time"
"github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss"
"github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss/credentials"
)
type AliyunOSS struct {
Client *oss.Client
BucketName string
Region string
}
func GetAliyunOSS() *AliyunOSS {
provider := credentials.NewEnvironmentVariableCredentialsProvider()
config := oss.LoadDefaultConfig().WithCredentialsProvider(provider).WithRegion("cn-shenzhen")
return &AliyunOSS{
Client: oss.NewClient(config),
BucketName: "my-bucket",
Region: "my-region",
}
}
func (o *AliyunOSS) Presign() *oss.PresignResult {
result, err := o.Client.Presign(context.TODO(), &oss.GetObjectRequest{
Bucket: oss.Ptr(o.BucketName),
Key: oss.Ptr("example.png"),
},
oss.PresignExpires(10*time.Minute),
)
if err != nil {
panic(err)
}
return result
}
以上是测试用的代码,生产环境用的代码也大差不差。增加一些判断逻辑和返回。
1.2. 功能要求
我的需求是能够在前端完成图片的上传,而不是上传到后端,由后端向阿里云OSS上传。这样就相当于上传两遍了,浪费带宽和时间。所以 在浏览阿里云文档的时候,发现可以由后端返回一个凭证,前端拿着凭证就请求阿里云oss的相关接口进行上传了。
而且有三种方式可以选择:
- 服务器生成临时STS凭证,前端可以用SDK和临时凭证想怎么传就怎么传。
- 服务端生成签名和POST Policy。
- 服务端生成PutObject所需要的签名URL。这种是我选择的觉得最简单的。
奈何,最简单也是遇到坑最多的。
2. 踩坑
2.1. 跨域错误
前端调用后端的接口,会经常碰到cors跨域问题。所以在阿里云OSS控制台的跨域设置那里直接设置就可以了。设置完之后就不会报错。
2.2. 403错误
但是,这个问题真的没有想到。看了报回来的错误,EC码是0002-00000201,也就是所谓的您发起的请求采用V4版本签名,但是请求中提供的签名与OSS计算的签名不匹配。
第一个反应是,是不是我的AppIdKey和AppIdSecret有问题啊。但是发现用后端进行ListObject和GetObject还有PutObject都没有问题。
第二个反应是,是不是EndPoint或者Bucket写错了?也不对啊,上面能够调用成功,证明这些参数应该是没问题的。
第三个反应是,☝️(OVO)诶。是不是SDK用的方式有问题,于是看SDK的文档。看来看去,还是没有问题。最后连续看了几个小时文档和验证猜测,头昏脑胀中下班。问题还是没有解决。
于是在周末的最后一天傍晚,写了一个测试文件。发现真不是后端的锅。而是前端的调用问题,为什么有问题呢?因为我照着阿里云OSS文档做的前端调用。文档给的例子如下,无论什么语言的SDK调用,都用以下例子作为前端调用的代码演示,还写着Web端使用签名URL上传文件到OSS的示例代码如下:
const form = document.querySelector("form");
form.addEventListener("submit", (event) => {
event.preventDefault();
const fileInput = document.querySelector("#file");
const file = fileInput.files[0];
fetch("/get_presigned_url_for_oss_upload", { method: "GET" })
.then((response) => {
if (!response.ok) {
throw new Error("获取预签名URL失败");
}
return response.text();
})
.then((url) => {
const formData = new FormData();
formData.append("file", file);
fetch(url, {
method: "PUT",
headers: new Headers({
"Content-Type": "image/png",
}),
body: file,
}).then((response) => {
if (!response.ok) {
throw new Error("文件上传到OSS失败");
}
console.log(response);
alert("文件已上传");
});
})
.catch((error) => {
console.error("发生错误:", error);
alert(error.message);
});
});
读完这段代码,明白流程是:
- 获得前端的file对象,成功之后调用后端的接口,返回一个预签名URL字符串
- 用这个URL进行PUT请求,请求头要表明这是一个png图片格式,body里面放file。
好!很好!在周末的最后一天,我才发现,这段代码是坑!
首先,为什么要有一个formData,而且你接下来是用不到。用不到你声明干嘛!弄得我觉得要传的是FormData一样!
其次,这是v1的SDK相应的前端调用。而不是v2的。那么v2的前端调用例子在那里呢?没找到!找了两天了,我没找到!那么,我是怎么知道正确的调用的呢?
那不就用穷举法咯QAQ。既然不是后端的锅,那么签名URL是没有问题的。而在查找前端fetch请求的过程中,发现了别人调用七牛云OSS的例子,人家在body里面用的不是File格式的,而是Blob格式的,也就是用二进制流格式的。所以,我把file转成Blob格式再上传,还是熟悉的403错误,还是熟悉的EC码0002-00000201。
没力气生气了,饿了,吃个饭先。
(嚼嚼嚼~)
酒足饭饱之后,回想了一下后端使用预签名PUT请求为什么会成功呢?在github给的例子如下:
client := oss.NewClient(cfg)
result, err := client.Presign(context.TODO(), &oss.PutObjectRequest{
Bucket: oss.Ptr("bucket"),
Key: oss.Ptr("key"),
Metadata: map[string]string{"user": "jack"}},
oss.PresignExpires(10*time.Minute),
)
req, _ := http.NewRequest(result.Method, result.URL, nil)
for k, v := range result.SignedHeaders {
req.Header.Add(k, v)
}
resp, err := http.DefaultClient.Do(req)
我一看第10行代码,PUT方法,预签名URL,还有最后应该是要上传的对象。我改了一下进行请求,发现是可以的。可以直接get下来一个txt文件,里面存的就是hello world字符串。
client := oss.NewClient(cfg)
result, err := client.Presign(context.TODO(), &oss.PutObjectRequest{
Bucket: oss.Ptr("bucket"),
Key: oss.Ptr("hello.txt"),
Metadata: map[string]string{"user": "jack"}},
oss.PresignExpires(10*time.Minute),
)
req, _ := http.NewRequest(result.Method, result.URL, strings.NewReader("hello world"))
for k, v := range result.SignedHeaders {
req.Header.Add(k, v)
}
resp, err := http.DefaultClient.Do(req)
也就是说,最后是要二进制文件,而且不能有header? 我在前端试了下,发现还真!是!这样!于是,最后的前端代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
>
<title>Test</title>
</head>
<body>
<button id="btn">点击</button>
</body>
<script>
document.getElementById("btn").addEventListener("click", () => {
const input = document.createElement("input");
input.type = "file";
input.accept = "image/*";
input.multiple = false;
input.addEventListener("change", (evt) => {
const file = evt.target.files[0];
if (!file) return;
console.log("file", file);
const fileReader = new FileReader();
fileReader.readAsArrayBuffer(file);
fileReader.addEventListener("load", (e) => {
const result = e.target.result;
let blob = null;
if (typeof result === 'object') {
blob = new Blob([result]);
} else {
blob = result;
}
fetch("/oss/presign", { method: "GET" }).then((response) => {
if (!response.ok) return console.log("resposne1", response);
return response.json();
}).then(data => {
console.log("url", data);
fetch(data.url, { method: "PUT", body: blob })
.then(res => {
console.log("成功!", res);
}).catch(err => {
console.log("失败", err);
})
});
});
});
input.click();
})
</script>
</html>
3. 后记
从这件事情上,我得出两个教训:
- 不要完全相信文档!不要完全相信文档!尤其是大型的项目,多个版本的那种!一定要看文档的创建日期和修改日期!
- 要多做项目,和在任何时候都要补足基础知识!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。