AWS 文件预签名URL
一、相关文档
1.AWS S3预签名URL文档:https://docs.aws.amazon.com/zh_cn/AmazonS3/latest/userguide/example_s3_Scenario_PresignedUrl_section.html
AWS S3只针对文件的存储,若想实现阿里云oss通过URL参数对图片进行处理,则需要使用AWS CloudFront。
CloudFront支持图片处理的解决方案:https://www.amazonaws.cn/solutions/technology/app-development/serverless-image-handler/?nc1=h_ls
CloudFront支持图片处理的实施文档:https://aws-gcr-solutions.s3.cn-north-1.amazonaws.com.cn/cn-serverless-image-handler/latest/docs.zh.pdf
CloudFront预签名URL:https://aws-gcr-solutions.s3.cn-north-1.amazonaws.com.cn/cn-serverless-image-handler/latest/docs.zh.pdf
二、GO示例
需要自己创建公私钥
1. Create private key:
openssl genrsa -out private_key.pem 2048
2. Create public key from private key.
openssl rsa -pubout -in private_key.pem -out public_key.pem
私钥用于配置在代码中,公钥用于AWS控制台配置,会生成一个KeyPairID;
配置了CDN,则走CDN预签名;否则,走bucket预签名
package main import ( "crypto" "crypto/rsa" "crypto/sha1" "crypto/x509" "encoding/base64" "encoding/pem" "errors" "fmt" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/credentials" "github.com/aws/aws-sdk-go-v2/service/s3" "strings" "time" "context" ) var PrivatePemKey = `-----BEGIN RSA PRIVATE KEY----- {私钥文件内容} -----END RSA PRIVATE KEY-----` var KeyPairID = "{keypid}" //AWS控制台PairID func main() { cfg := &StoreClientConf{ RoleArn: "{bucket roleArn}", Region: "{bucket region}", AccessKeyID: "{bucket ak}", AccessKeySecret: "{bucket sk}", CDNDomain: "{cdn 域名}", //通过该参数可控制使用S3的预签名,还是使用CDN预签名 } client := NewAwsClient(cfg) objectKey := "aws/20240422104414.png" var bucketName = "{bucket name}" url, err := client.SignUrl(context.Background(), bucketName, objectKey, 86400, "image/resize,w_200,h_100") if err != nil { fmt.Println("client.SignUrl err: " + err.Error()) } fmt.Println("sign url: " + url) } type AwsClient struct { roleArn string region string accessKeyID string accessKeySecret string cdnDomain string } type StoreClientConf struct { RoleArn string Region string AccessKeyID string AccessKeySecret string CDNDomain string } func NewAwsClient(cfg *StoreClientConf) *AwsClient { return &AwsClient{ roleArn: cfg.RoleArn, region: cfg.Region, accessKeyID: cfg.AccessKeyID, accessKeySecret: cfg.AccessKeySecret, cdnDomain: cfg.CDNDomain, //开启了AWS CloudFront CDN,可支持URL参数处理图片 } } func (s *AwsClient) SignUrl(ctx context.Context, bucketName, objectKey string, expire int32, ossProcess string) (string, error) { // 判断是走S3的预签名还是走CDN的预签名 if s.cdnDomain == "" { //S3的文件预签名URL,不支持图片处理 return s.S3SignUrl(ctx, bucketName, objectKey, expire) } return s.CDNSignUrl(ctx, objectKey, expire, ossProcess) //AWS CDN预签名,支持URL参数处理图片 } func (s *AwsClient) CDNSignUrl(ctx context.Context, objectKey string, expire int32, ossProcess string) (string, error) { objectKey = strings.TrimLeft(objectKey, "/") //去除最左边的/ path := "https://" + s.cdnDomain + "/" + objectKey if ossProcess != "" { path += "?x-oss-process=" + ossProcess } expires := time.Now().Add(time.Duration(expire) * time.Second).Unix() separator := "?" if strings.Contains(path, "?") { separator = "&" } policy := fmt.Sprintf(`{"Statement":[{"Resource":"%s","Condition":{"DateLessThan":{"AWS:EpochTime":%d}}}]}`, path, expires) signature, err := rsaSHA1Sign(policy) if err != nil { return "", errors.New("CDNSignUrl rsaSHA1Sign err: " + err.Error()) } signatureEncoded := urlSafeBase64Encode(signature) signUrl := fmt.Sprintf("%s%sExpires=%d&Signature=%s&Key-Pair-Id=%s", path, separator, expires, signatureEncoded, KeyPairID) return signUrl, nil } // https://docs.aws.amazon.com/zh_cn/AmazonS3/latest/userguide/example_s3_Scenario_PresignedUrl_section.html func (s *AwsClient) S3SignUrl(ctx context.Context, bucketName, objectKey string, expire int32) (string, error) { // 1.初始化客户端 cfg, err := s.loadConfig(ctx) if err != nil { return "", err } client := s3.NewFromConfig(cfg) pClient := s3.NewPresignClient(client) // 2.调用s3接口,获取文件预签名URL input := &s3.GetObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKey), } response, err := pClient.PresignGetObject(ctx, input, func(opts *s3.PresignOptions) { opts.Expires = time.Duration(expire) * time.Second }) if err != nil { return "", errors.New("SignUrl pClient.PresignGetObject err: "+err.Error()) } if response == nil { return "", errors.New("SignUrl response is nil") } return response.URL, nil } func (s *AwsClient) loadConfig(ctx context.Context) (aws.Config, error) { cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(s.region), config.WithCredentialsProvider(credentials.StaticCredentialsProvider{ Value: aws.Credentials{ AccessKeyID: s.accessKeyID, SecretAccessKey: s.accessKeySecret, SessionToken: "", Source: "", }, }), ) if err != nil { fmt.Println("awsClient LoadDefaultConfig err:" + err.Error()) return aws.Config{}, errors.New("awsClient LoadDefaultConfig err") } return cfg, nil } func rsaSHA1Sign(policy string) ([]byte, error) { // Load the private key privateKeyBlock, _ := pem.Decode([]byte(PrivatePemKey)) privateKey, err := x509.ParsePKCS1PrivateKey(privateKeyBlock.Bytes) if err != nil { return nil, err } // Compute the signature hashed := sha1.Sum([]byte(policy)) signature, err := rsa.SignPKCS1v15(nil, privateKey, crypto.SHA1, hashed[:]) // if err != nil { return nil, err } return signature, nil } func urlSafeBase64Encode(value []byte) string { encoded := base64.StdEncoding.EncodeToString(value) // Replace unsafe characters +, = and / with the safe characters -, _, and ~ encoded = strings.ReplaceAll(encoded, "+", "-") encoded = strings.ReplaceAll(encoded, "=", "_") encoded = strings.ReplaceAll(encoded, "/", "~") return encoded }