Go使用预签名URL上传

默认情况下,OSS Bucket中的文件是私有的,仅文件拥有者可访问。本文介绍如何使用OSS Go SDK生成带有过期时间的PUT方法签名URL,以允许他人临时上传文件。在有效期内可多次访问,超期后需重新生成。

注意事项

  • 本文示例代码以华东1(杭州)的地域IDcn-hangzhou为例,默认使用外网Endpoint,如果您希望通过与OSS同地域的其他阿里云产品访问OSS,请使用内网Endpoint。关于OSS支持的Region与Endpoint的对应关系,请参见OSS地域和访问域名

  • 本文以从环境变量读取访问凭证为例。如何配置访问凭证,请参见配置访问凭证

  • 生成PUT方法的签名URL时,您必须具有oss:PutObject权限。具体操作,请参见为RAM用户授权自定义的权限策略

    说明

    在生成签名 URL 的过程中,SDK 利用本地存储的密钥信息,根据特定算法计算出签名(signature),然后将其附加到 URL 上,以确保 URL 的有效性和安全性。这一系列计算和构造 URL 的操作都是在客户端完成的,不涉及网络请求到服务端。因此,生成签名 URL 时不需要授予调用者特定权限。然而,为避免第三方用户无法对签名URL授权的资源执行相关操作,需要确保调用生成签名 URL 接口的身份主体已被授予相应的权限。

  • 本文示例代码使用V4签名URL,有效期最大为7天。更多信息,请参见签名版本4(推荐)

方法定义

您可以使用预签名接口生成预签名URL,授予对存储空间中对象的限时访问权限。在过期时间之前,您可以多次使用预签名URL。

预签名接口定义如下:

func (c *Client) Presign(ctx context.Context, request any, optFns ...func(*PresignOptions)) (result *PresignResult, err error)

请求参数列表

参数名

类型

说明

ctx

context.Context

请求的上下文

request

*PutObjectRequest

设置需要生成签名URL的接口名

optFns

...func(*PresignOptions)

(可选)设置过期时间,如果不指定,默认有效期为15分钟

其中,PresignOptions选项列举如下:

选项值

类型

说明

Expires

time.Duration

从当前时间开始,多长时间过期。例如设置一个有效期为30分钟,30 * time.Minute

Expiration

time.Time

绝对过期时间

重要

在签名版本V4下,有效期最长为7天。同时设置Expiration和Expires时,优先取Expiration。

返回值列表

返回值名

类型

说明

result

*PresignResult

返回结果,包含预签名URL,HTTP方法,过期时间和参与签名的请求头

err

error

请求的状态,当请求失败时,err不为nil

其中,PresignResult返回值列举如下:

参数名

类型

说明

Method

string

HTTP方法,和接口对应,例如PutObject接口,返回PUT

URL

string

预签名URL

Expiration

time.Time

签名URL的过期时间

SignedHeaders

map[string]string

被签名的请求头,例如PutObject接口,设置了Content-Type时,会返回 Content-Type的信息

示例代码

  1. 文件拥有者生成PUT方法的签名URL。

    重要

    在生成PUT方法的签名URL时,如果指定了请求头,确保在通过该签名URL发起PUT请求时也包含相应的请求头,以免出现不一致,导致请求失败和签名错误。

    package main
    
    import (
    	"context"
    	"flag"
    	"log"
    	"time"
    
    	"github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss"
    	"github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss/credentials"
    )
    
    // 定义全局变量
    var (
    	region     string // 存储区域
    	bucketName string // 存储空间名称
    	objectName string // 对象名称
    )
    
    // init函数用于初始化命令行参数
    func init() {
    	flag.StringVar(&region, "region", "", "The region in which the bucket is located.")
    	flag.StringVar(&bucketName, "bucket", "", "The name of the bucket.")
    	flag.StringVar(&objectName, "object", "", "The name of the object.")
    }
    
    func main() {
    	// 解析命令行参数
    	flag.Parse()
    
    	// 检查bucket名称是否为空
    	if len(bucketName) == 0 {
    		flag.PrintDefaults()
    		log.Fatalf("invalid parameters, bucket name required")
    	}
    
    	// 检查region是否为空
    	if len(region) == 0 {
    		flag.PrintDefaults()
    		log.Fatalf("invalid parameters, region required")
    	}
    
    	// 检查object名称是否为空
    	if len(objectName) == 0 {
    		flag.PrintDefaults()
    		log.Fatalf("invalid parameters, object name required")
    	}
    
    	// 加载默认配置并设置凭证提供者和区域
    	cfg := oss.LoadDefaultConfig().
    		WithCredentialsProvider(credentials.NewEnvironmentVariableCredentialsProvider()).
    		WithRegion(region)
    
    	// 创建OSS客户端
    	client := oss.NewClient(cfg)
    
    	// 生成PutObject的预签名URL
    	result, err := client.Presign(context.TODO(), &oss.PutObjectRequest{
    		Bucket: oss.Ptr(bucketName),
    		Key:    oss.Ptr(objectName),
    		//ContentType: oss.Ptr("text/txt"),                                 // 请确保在服务端生成该签名URL时设置的ContentType与在使用URL时设置的ContentType一致
    		//Metadata:    map[string]string{"key1": "value1", "key2": "value2"}, // 请确保在服务端生成该签名URL时设置的Metadata与在使用URL时设置的Metadata一致
    	},
    		oss.PresignExpires(10*time.Minute),
    	)
    	if err != nil {
    		log.Fatalf("failed to put object presign %v", err)
    	}
    
    	log.Printf("request method:%v\n", result.Method)
    	log.Printf("request expiration:%v\n", result.Expiration)
    	log.Printf("request url:%v\n", result.URL)
    	if len(result.SignedHeaders) > 0 {
    		//当返回结果包含签名头时,使用签名URL发送Put请求时,需要设置相应的请求头
    		log.Printf("signed headers:\n")
    		for k, v := range result.SignedHeaders {
    			log.Printf("%v: %v\n", k, v)
    		}
    	}
    }
    
  2. 其他人使用PUT方法的签名URL上传文件。

    package main
    
    import (
    	"bytes"
    	"fmt"
    	"io"
    
    	"net/http"
    	"os"
    )
    
    func uploadFile(signedUrl string, filePath string, headers map[string]string, metadata map[string]string) error {
    	// 打开文件
    	file, err := os.Open(filePath)
    	if err != nil {
    		return err
    	}
    	defer file.Close()
    
    	// 读取文件内容
    	fileBytes, err := io.ReadAll(file)
    	if err != nil {
    		return err
    	}
    
    	// 创建请求
    	req, err := http.NewRequest("PUT", signedUrl, bytes.NewBuffer(fileBytes))
    	if err != nil {
    		return err
    	}
    
    	// 设置请求头
    	for key, value := range headers {
    		req.Header.Set(key, value)
    	}
    
    	// 设置用户自定义元数据
    	for key, value := range metadata {
    		req.Header.Set(fmt.Sprintf("x-oss-meta-%s", key), value)
    	}
    
    	// 发送请求
    	client := &http.Client{}
    	resp, err := client.Do(req)
    	if err != nil {
    		return err
    	}
    	defer resp.Body.Close()
    
    	// 处理响应
    	fmt.Printf("返回上传状态码:%d\n", resp.StatusCode)
    	if resp.StatusCode == 200 {
    		fmt.Println("使用网络库上传成功")
    	} else {
    		fmt.Println("上传失败")
    	}
    	body, _ := io.ReadAll(resp.Body)
    	fmt.Println(string(body))
    
    	return nil
    }
    
    func main() {
    	// 将<signedUrl>替换为授权URL。
    	signedUrl := "<signedUrl>"
    
    	// 填写本地文件的完整路径。如果未指定本地路径,则默认从示例程序所属项目对应本地路径中上传文件。
    	filePath := "C:\\Users\\demo.txt"
    
    	// 设置请求头,这里的请求头信息需要与生成URL时的信息一致。
    	headers := map[string]string{
    		// "Content-Type": "text/txt",
    	}
    
    	// 设置用户自定义元数据,这里的用户自定义元数据需要与生成URL时的信息一致。
    	metadata := map[string]string{
    		// "key1": "value1",
    		// "key2": "value2",
    	}
    
    	err := uploadFile(signedUrl, filePath, headers, metadata)
    	if err != nil {
    		fmt.Printf("发生错误:%v\n", err)
    	}
    }
    

常见使用场景

如何使用签名URL分片上传文件

使用签名URL分片上传文件需要配置分片大小并逐片生成签名URL,示例代码如下:

package main

import (
	"bytes"
	"context"
	"flag"
	"fmt"
	"log"
	"net/http"
	"time"

	"github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss"
	"github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss/credentials"
)

var (
	region     string                     // 区域
	bucketName string                     // 存储桶名称
	objectName string                     // 对象名称
	length     = int64(5000 * 1024)       // 文件总长度,单位为字节
	partSize   = int64(200 * 1024)        // 每个分片的大小,单位为字节
	partsNum   = int(length/partSize + 1) // 分片的数量
	data       = make([]byte, length)     // 模拟的数据,用于上传
)

// 初始化命令行参数
func init() {
	flag.StringVar(&region, "region", "", "The region in which the bucket is located.")
	flag.StringVar(&bucketName, "bucket", "", "The name of the bucket.")
	flag.StringVar(&objectName, "object", "", "The name of the object.")
}

func main() {
	// 解析命令行参数
	flag.Parse()

	// 检查必要参数是否已设置
	if len(bucketName) == 0 {
		flag.PrintDefaults()
		log.Fatalf("invalid parameters, bucket name required")
	}

	if len(region) == 0 {
		flag.PrintDefaults()
		log.Fatalf("invalid parameters, region required")
	}

	if len(objectName) == 0 {
		flag.PrintDefaults()
		log.Fatalf("invalid parameters, object name required")
	}

	// 配置 OSS 客户端
	cfg := oss.LoadDefaultConfig().
		WithCredentialsProvider(credentials.NewEnvironmentVariableCredentialsProvider()).
		WithRegion(region)

	// 创建 OSS 客户端
	client := oss.NewClient(cfg)

	// 初始化分片上传
	initResult, err := client.InitiateMultipartUpload(context.TODO(), &oss.InitiateMultipartUploadRequest{
		Bucket: oss.Ptr(bucketName),
		Key:    oss.Ptr(objectName),
	})
	if err != nil {
		log.Fatalf("failed InitiateMultipartUpload %v", err)
	}

	// 遍历每个分片,生成签名 URL 并上传分片
	for i := 0; i < partsNum; i++ {
		start := int64(i) * partSize
		end := start + partSize
		if end > length {
			end = length
		}
		signedResult, err := client.Presign(context.TODO(), &oss.UploadPartRequest{
			Bucket:     oss.Ptr(bucketName),
			Key:        oss.Ptr(objectName),
			PartNumber: int32(i + 1),
			Body:       bytes.NewReader(data[start:end]),
			UploadId:   initResult.UploadId,
		}, oss.PresignExpiration(time.Now().Add(1*time.Hour))) // 生成签名 URL,有效期为1小时
		if err != nil {
			log.Fatalf("failed to generate presigned URL %v", err)
		}
		fmt.Printf("signed url:%#v\n", signedResult.URL) // 打印生成的签名URL

		// 创建HTTP请求并上传分片
		req, err := http.NewRequest(signedResult.Method, signedResult.URL, bytes.NewReader(data[start:end]))
		if err != nil {
			log.Fatalf("failed to create HTTP request %v", err)
		}

		c := &http.Client{} // 创建HTTP客户端
		_, err = c.Do(req)
		if err != nil {
			log.Fatalf("failed to upload part by signed URL %v", err)
		}
	}

	// 列举已上传的分片
	partsResult, err := client.ListParts(context.TODO(), &oss.ListPartsRequest{
		Bucket:   oss.Ptr(bucketName),
		Key:      oss.Ptr(objectName),
		UploadId: initResult.UploadId,
	})
	if err != nil {
		log.Fatalf("failed to list parts %v", err)
	}

	// 收集已上传的分片信息
	var parts []oss.UploadPart
	for _, p := range partsResult.Parts {
		parts = append(parts, oss.UploadPart{PartNumber: p.PartNumber, ETag: p.ETag})
	}

	// 完成分片上传
	result, err := client.CompleteMultipartUpload(context.TODO(), &oss.CompleteMultipartUploadRequest{
		Bucket:   oss.Ptr(bucketName),
		Key:      oss.Ptr(objectName),
		UploadId: initResult.UploadId,
		CompleteMultipartUpload: &oss.CompleteMultipartUpload{
			Parts: parts,
		},
	})
	if err != nil {
		log.Fatalf("failed to complete multipart upload %v", err)
	}

	// 打印完成分片上传的结果
	log.Printf("complete multipart upload result:%#v\n", result)
}

相关文档

  • 关于预签名URL的完整示例代码,请参见GitHub示例

  • 关于预签名URL的API接口,请参见Presign