AWS 檔案預簽名URL

划水的猫發表於2024-04-29

1.《獲取STS臨時授權憑證》

2.《透過STS Token分片上傳檔案》

一、相關文件

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
}

相關文章