国内对象存储服务 Pre-Signed Put 代码示例(腾讯云、阿里云、缤纷云)

Published: 2024-08-26
Updated: 2024-08-27

Tags: S3

本文总阅读量

本文仅作 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 保持的一致,看文档中也记录了一些独有功能支持,我没测试

作为功能测试,就先整理到这个程度。