本文仅作 Pre-Signed URL Put 功能的初步测试,并未在使用上提供最佳实践,一切以官方文档推荐方式为准。
代码示例与遇到的问题记录如下
阿里云 OSS
不指定 Content-Disposition 时,访问下载 URL,因为阿里云返回的下载 URL 的 Key 路径会转义为 "test%2Fhello.txt",导致 "/test/hello.txt" 的默认下载名称会变为 test_hello.txt 而非 hello.txt
阿里云支持生成 Pre-Signed URL 时设置 Header 和 Meta 自定义属性,并在客户端上传时校验,不一致时会抛出 403 访问被禁止报错,跟 AWS S3 的行为一致
示例代码
package main
import (
"fmt"
"github.com/aliyun/aliyun-oss-go-sdk/oss"
"io"
"log"
"net/http"
"strings"
)
const (
Endpoint = "<your-endpoint>"
AccessKeyID = "<your-access-key-id>"
AccessKeySecret = "<your-access-key-secret>"
BucketName = "<your-bucket-name>"
ObjectKey = "example-object.txt"
)
func main() {
// 初始化 OSS 客户端
client, err := oss.New(Endpoint, AccessKeyID, AccessKeySecret)
if err != nil {
log.Fatalf("创建 OSS 客户端失败: %v", err)
}
// 获取 Put Signed URL
putSignedURL := getPutSignedURL(client)
fmt.Println("Put Signed URL:", putSignedURL)
// 上传文本内容到 OSS
err = uploadTextToSignedURL(putSignedURL, "hello world!")
if err != nil {
log.Fatalf("上传失败: %v", err)
}
fmt.Println("上传成功")
// 获取 Get Signed URL 并访问内容
getSignedURL := getGetSignedURL(client)
fmt.Println("Get Signed URL:", getSignedURL)
content, err := accessContentBySignedURL(getSignedURL)
if err != nil {
log.Fatalf("访问失败: %v", err)
}
fmt.Println("访问到的内容:", content)
}
// 获取 Put Signed URL
func getPutSignedURL(client *oss.Client) string {
bucket, err := client.Bucket(BucketName)
if err != nil {
log.Fatalf("获取 Bucket 实例失败: %v", err)
}
options := []oss.Option{
oss.Meta("myprop", "mypropval"),
//oss.ContentType("text/plain; charset=utf-8"),
oss.ContentDisposition(`attachment; filename="hello.txt"`),
}
// 生成 PUT 方法的签名 URL,设置过期时间为 1 小时
signedURL, err := bucket.SignURL(ObjectKey, oss.HTTPPut, 3600, options...)
if err != nil {
log.Fatalf("生成签名 URL 失败: %v", err)
}
return signedURL
}
// 通过 Signed URL 上传文本内容
func uploadTextToSignedURL(url, content string) error {
f := strings.NewReader(content)
request, err := http.NewRequest("PUT", url, f)
if err != nil {
return fmt.Errorf("创建 PUT 请求失败: %v", err)
}
// 添加自定义的 HTTP 头
//request.Header.Add("Content-Type", "text/plain; charset=utf-8")
request.Header.Add("Content-Disposition", `attachment; filename="hello.txt"`)
request.Header.Add("x-oss-meta-myprop", "mypropval")
client := &http.Client{}
resp, err := client.Do(request)
if err != nil {
return fmt.Errorf("执行 PUT 请求失败: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("PUT 请求返回状态码: %d", resp.StatusCode)
}
return nil
}
// 获取 Get Signed URL
func getGetSignedURL(client *oss.Client) string {
bucket, err := client.Bucket(BucketName)
if err != nil {
log.Fatalf("获取 Bucket 实例失败: %v", err)
}
// 生成 GET 方法的签名 URL,设置过期时间为 1 小时
signedURL, err := bucket.SignURL(ObjectKey, oss.HTTPGet, 3600)
if err != nil {
log.Fatalf("生成签名 URL 失败: %v", err)
}
return signedURL
}
// 通过 Signed URL 访问 OSS 上的内容
func accessContentBySignedURL(url string) (string, error) {
resp, err := http.Get(url)
if err != nil {
return "", fmt.Errorf("获取内容失败: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("GET 请求返回状态码: %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("读取响应内容失败: %v", err)
}
return string(body), nil
}
腾讯云 COS
腾讯云 COS Golang SDK 返回的 URL 有一些离谱(cos-go-sdk-v5 v0.7.54 版本测试),根据文档提供的代码进行测试,上传文件会报错抛出 SignatureDoesNotMatch,以至于 Signed URL 需要对 Query 参数单独 Encode 处理
另外预签名时虽然限定了 Content-Type,但上传时用户还是可以重新设置 Content-Type,不清楚是 AK/SK 权限导致还是说跟我使用 Encode 编码后导致的连锁问题
20240827 最新补充
因为桶的权限放开,设置 ak/sk 为空字符串时通过以下代码也可以上传、访问,所以我在测试的时候变量设置的是空 + EncodeQueryParams 编码,可以正常使用。当 ak/sk 为空值时,去掉 EncodeQueryParams 会抛出 SignatureDoesNotMatch,感觉是 COS Golang SDK 没对这种情况做兼容,建议参考官方文档,总是传递有效的 ak/sk 参数。
另外设置 Content-Type 没有生效的问题,是因为 _, err = http.DefaultClient.Do(req)
处没有对 resp 回值的 Status Code 做判断,其实它返回了 403,而我误以为上传成功,直到修改文本内容上传发现打印的还是旧内容
代码示例
package main
import (
"context"
"fmt"
"github.com/tencentyun/cos-go-sdk-v5"
"io"
"net/http"
"net/url"
"strings"
"time"
)
const (
bucketURL = "https://my-bucket-130****981.cos.ap-beijing.myqcloud.com"
tak = "<your-key>"
tsk = "<your-secret>"
)
func main() {
u, _ := url.Parse(bucketURL)
b := &cos.BaseURL{BucketURL: u}
c := cos.NewClient(b, &http.Client{})
name := "test/hello.txt"
ctx := context.Background()
// 1. 获取可供上传的 Signed-URL
// PresignedURLOptions 提供用户添加请求参数和请求头部
opt := &cos.PresignedURLOptions{
// http 请求参数,传入的请求参数需与实际请求相同,能够防止用户篡改此 HTTP 请求的参数
Query: &url.Values{},
// http 请求头部,传入的请求头部需包含在实际请求中,能够防止用户篡改签入此处的 HTTP 请求头部
Header: &http.Header{},
}
// 添加请求头部,返回的预签名 url 只是将请求头部设置到签名里,请求时还需要自行设置对应的 header。
opt.Header.Add("Content-Type", "text/plain")
// 获取预签名, 签名中携带host
presignedURL, err := c.Object.GetPresignedURL(ctx, http.MethodPut, name, tak, tsk, time.Hour, opt, true)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
fixedSignedURL, err := EncodeQueryParams(presignedURL.String())
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
// 2. 通过预签名方式上传对象
data := "hello, world!"
f := strings.NewReader(data)
req, err := http.NewRequest(http.MethodPut, fixedSignedURL, f)
if err != nil {
panic(err)
}
req.Header.Set("Content-Type", "text/plain")
_, err = http.DefaultClient.Do(req) // 此处没有接收判断 resp
if err != nil {
panic(err)
}
// 应添加以下判断
// if resp.StatusCode != http.StatusOK {
// fmt.Println("status_code: ", resp.StatusCode)
// panic("http put error")
// }
fmt.Println("upload content success")
// 3. 获取预签名 URL 下载对象
presignedURL, err = c.Object.GetPresignedURL(ctx, http.MethodGet, name, tak, tsk, time.Hour, nil)
if err != nil {
panic(err)
}
fixedSignedURL, err = EncodeQueryParams(presignedURL.String())
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
resp, err := http.Get(presignedURL.String())
if err != nil {
panic(err)
}
bs, _ := io.ReadAll(resp.Body)
resp.Body.Close()
fmt.Println(string(bs))
}
// EncodeQueryParams takes a full URL, extracts the query parameters,
// URL-encodes them, and returns the full URL with encoded query string.
func EncodeQueryParams(fullURL string) (string, error) {
// Parse the provided URL
parsedURL, err := url.Parse(fullURL)
if err != nil {
return "", fmt.Errorf("invalid URL: %v", err)
}
// Extract the raw query part
rawQuery := parsedURL.RawQuery
// URL-encode the entire query string
encodedQuery := url.QueryEscape(rawQuery)
// Construct the new URL with the encoded query
finalURL := strings.Split(fullURL, "?")[0] + "?" + encodedQuery
return finalURL, nil
}
文档:https://cloud.tencent.com/document/product/436/35059
Bitiful 缤纷云
这个 Bitiful 服务在 V2EX 推广的时候注册的,我一般临时测试时使用,测试完阿里云 OSS 和 腾讯云 COS,想到缤纷云,就也一并验证
缤纷云 S4 兼容 AWS S3 协议,所以代码可以直接使用 Web 直接上传 S3 技术方案验证 | Golang 实现 中的 S3 示例代码,仅做如下改动,指定服务端点:
// 需要设置 Endpoint
endpoint := "https://s3.bitiful.net"
sess, err := session.NewSession(&aws.Config{
Endpoint: aws.String(endpoint),
Region: aws.String(region),
Credentials: credentials.NewStaticCredentials(accessKey, secretKey, ""),
})
官方文档中示例使用的是 aws-sdk-go-v2 SDK,我这里测试使用的是 aws-sdk-go 版,推荐参考官方文档使用 V2 版
虽是小厂,但是体验上我觉的 OK,路径无需手动再进行编码处理,同时设置的 Header 也被 Signed,在上传文件时进行了校验,这两个点都跟 AWS S3 保持的一致,看文档中也记录了一些独有功能支持,我没测试
作为功能测试,就先整理到这个程度。