如何在Go中预先签名POST上传到AWS S3?

时间:2015-09-03 14:00:13

标签: go amazon-s3 pre-signed-url

我想做pre-signed POST to upload files to an AWS S3 bucket - 如何在Go中完成?

请注意,这与使用PUT的预签名上传不同。

4 个答案:

答案 0 :(得分:3)

所以为了帮助别人,我会自己回答这个问题并提供一些代码来帮助那些可能遇到同样问题的人。

Google App Engine的示例网络应用程序可以找到预先签名的POST表单here

a small library I created doing the pre-signed POST in Go

简而言之,对公开读取的Amazon S3存储桶执行预先分配的POST,您需要:

<强> 1。将S3存储桶配置为仅允许公共下载。

仅允许公开阅读的存储桶策略示例。

{
    "Version": "2012-10-17",
    "Id": "akjsdhakshfjlashdf",
    "Statement": [
        {
            "Sid": "kjahsdkajhsdkjasda",
            "Effect": "Allow",
            "Principal": {
                "AWS": "*"
            },
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::BUCKETNAMEHERE/*"
        }
    ]
}

<强> 2。为允许上传的HTTP POST创建策略。

AWS S3 docs

示例POST策略模板,其过期时将特定密钥上载到特定存储桶并允许公共读取访问。

{ "expiration": "%s",
    "conditions": [
        {"bucket": "%s"},
        ["starts-with", "$key", "%s"],
        {"acl": "public-read"},

        {"x-amz-credential": "%s"},
        {"x-amz-algorithm": "AWS4-HMAC-SHA256"},
        {"x-amz-date": "%s" }
    ]
}

第3。使用S3存储桶拥有者的凭据生成并签署策略。

AWS docs

  • 填写过期,存储桶,密钥,凭据和日期的正确值。
  • base64对策略进行编码。
  • HMAC-SHA256获取签名的政策。
  • 十六进制编码签名。

<强> 4。构造和发布多部分表单数据

AWS S3 docs

现在要么生成HTML表单并自动获取正确的多部分表单数据请求,如上面链接中所述。

我想在Go手动这样做,所以这里是如何做到的。

无论哪种方式,您都需要提供在步骤2和3中创建的POST策略中指定的所有部分。除了强制性(不在策略中)之外,您还可以在请求中添加其他字段。 / p>

还指定了字段的顺序,并且它们都是HTTP POST请求中的多部分字段。

func Upload(url string, fields Fields) error {
    var b bytes.Buffer
    w := multipart.NewWriter(&b)
    for _, f := range fields {
            fw, err := w.CreateFormField(f.Key)
            if err != nil {
                    return err
            }
            if _, err := fw.Write([]byte(f.Value)); err != nil {
                    return err
            }
    }
    w.Close()

    req, err := http.NewRequest("POST", url, &b)
    if err != nil {
            return err
    }
    req.Header.Set("Content-Type", w.FormDataContentType())

    client := &http.Client{}
    res, err := client.Do(req)
    if err != nil {
            return err
    }
    if res.StatusCode != http.StatusOK {
            err = fmt.Errorf("bad status: %s", res.Status)
    }
    return nil
}

答案 1 :(得分:2)

一眼就看起来POST使用附加的策略和签名 - 专为基于浏览器的上传而设计。请参阅AWS Docs for details

具体来说,您需要generate a policy and sign that - 然后将它们包含在HTML表单中,从而包含POST请求 - 以及其他所需信息。或者让浏览器为您完成。

对于HTML表单POST上传,您只需签署策略字符串。要发布到的最终网址可能因表单内容而异:https://bucket.s3.amazonaws.com/<depends-on-form-content>。因此,您无法预设该URL,因为您不知道它是什么。

这与您输入文件的签名URL不同。您可以签名,因为您知道完整的网址:https://bucket.s3.amazonaws.com/known-key

您可以使用适当的策略和参数构建POST请求,并通过POST方式上传。但是,您需要知道表单的内容才能事先知道URL。在这种情况下,您也可以使用预先签名的PUT URL。

至少就是它一目了然......

答案 2 :(得分:2)

以下是来自https://github.com/minio/minio-go的替代方法  您可能希望以完整的程序化方式生成预先签署的邮政策略。

package main

import (
    "fmt"
    "log"
    "time"

    "github.com/minio/minio-go"
)

func main() {
    policy := minio.NewPostPolicy()
    policy.SetKey("myobject")
    policy.SetBucket("mybucket")
    policy.SetExpires(time.Now().UTC().AddDate(0, 0, 10)) // expires in 10 days
    config := minio.Config{
        AccessKeyID:     "YOUR-ACCESS-KEY-HERE",
        SecretAccessKey: "YOUR-PASSWORD-HERE",
        Endpoint:        "https://s3.amazonaws.com",
    }
    s3Client, err := minio.New(config)
    if err != nil {
        log.Fatalln(err)
    }
    m, err := s3Client.PresignedPostPolicy(policy)
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Printf("curl ")
    for k, v := range m {
        fmt.Printf("-F %s=%s ", k, v)
    }
    fmt.Printf("-F file=@/etc/bashrc ")
    fmt.Printf(config.Endpoint + "/mybucket\n")
}

第1步:

    policy := minio.NewPostPolicy()
    policy.SetKey("myobject")
    policy.SetBucket("mybucket")
    policy.SetExpires(time.Now().UTC().AddDate(0, 0, 10)) // expires in 10 days

实例化新的策略结构,此策略结构实现以下方法。

func NewPostPolicy() *PostPolicy
func (p *PostPolicy) SetBucket(bucket string) error
func (p *PostPolicy) SetContentLength(min, max int) error
func (p *PostPolicy) SetContentType(contentType string) error
func (p *PostPolicy) SetExpires(t time.Time) error
func (p *PostPolicy) SetKey(key string) error
func (p *PostPolicy) SetKeyStartsWith(keyStartsWith string) error
func (p PostPolicy) String() string

第2步:

    m, err := s3Client.PresignedPostPolicy(policy)
    if err != nil {
        fmt.Println(err)
        return
    }

现在,PresignedPostPolicy()采用PostPolicy结构并返回“键/值”的映射,可以在HTML表单或curl命令中使用该映射将数据上传到s3。

答案 3 :(得分:0)

在尝试使用@murrekatt 提供的解决方案时遇到此问题并遇到“InvalidAccessKeyId”错误。

后来我发现这个问题是因为我在 lambda 中生成了预先签名的 POST,并且没有在表单数据和策略中包含 x-amz-security-token

所以这是我在@murrekatt 和 boto3 库的帮助下写的:

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/base64"
    "encoding/hex"
    "encoding/json"
    "fmt"
    "time"

    "github.com/aws/aws-sdk-go-v2/aws"
)

type PresignedPOST struct {
    URL           string `json:"url"`
    Key           string `json:"key"`
    Policy        string `json:"policy"`
    Credential    string `json:"credential"`
    SecurityToken string `json:"securityToken,omitempty"`
    Signature     string `json:"signature"`
    Date          string `json:"date"`
}

func NewPresignedPost(input *NewPresignedPostInput) (*PresignedPOST, error) {

    // expiration time
    expirationTime := time.Now().Add(time.Second * time.Duration(input.ExpiresIn)).UTC()
    dateString := expirationTime.Format("20060102")

    // credentials string
    creds := fmt.Sprintf("%s/%s/%s/s3/aws4_request", input.Credentials.AccessKeyID, dateString, input.Region)

    // policy
    policyDoc, err := createPolicyDocument(expirationTime, input.Bucket, input.Key, creds, &input.Credentials.SessionToken, input.Conditions)
    if err != nil {
        return nil, err
    }

    // create signature
    signature := createSignature(input.Credentials.SecretAccessKey, input.Region, dateString, policyDoc)

    // url
    url := fmt.Sprintf("https://%s.s3.amazonaws.com/", input.Bucket)

    // expiration time
    dateTimeString := expirationTime.Format("20060102T150405Z")

    // post
    post := &PresignedPOST{
        Key:           input.Key,
        Policy:        policyDoc,
        Signature:     signature,
        URL:           url,
        Credential:    creds,
        SecurityToken: input.Credentials.SessionToken,
        Date:          dateTimeString,
    }

    return post, nil
}

type NewPresignedPostInput struct {
    // Key name
    Key string

    // Creds
    Credentials aws.Credentials

    // Region
    Region string

    // The name of the bucket to presign the post to
    Bucket string

    // Expiration -  The number of seconds the presigned post is valid for.
    ExpiresIn int64

    // A list of conditions to include in the policy. Each element can be either a list or a structure.
    // For example:
    // [
    //      {"acl": "public-read"}, ["content-length-range", 2, 5], ["starts-with", "$success_action_redirect", ""]
    // ]
    Conditions []interface{}
}

// helpers
func createPolicyDocument(expirationTime time.Time, bucket string, key string, credentialString string, securityToken *string, extraConditions []interface{}) (string, error) {

    doc := map[string]interface{}{}
    doc["expiration"] = expirationTime.Format("2006-01-02T15:04:05.000Z")

    // conditions
    conditions := []interface{}{}
    conditions = append(conditions, map[string]string{
        "bucket": bucket,
    })

    conditions = append(conditions, []string{
        "starts-with", "$key", key,
    })

    conditions = append(conditions, map[string]string{
        "x-amz-credential": credentialString,
    })

    if securityToken != nil {
        conditions = append(conditions, map[string]string{
            "x-amz-security-token": *securityToken,
        })
    }

    conditions = append(conditions, map[string]string{
        "x-amz-algorithm": "AWS4-HMAC-SHA256",
    })

    conditions = append(conditions, map[string]string{
        "x-amz-date": expirationTime.Format("20060102T150405Z"),
    })

    // other conditions
    conditions = append(conditions, extraConditions...)

    doc["conditions"] = conditions

    // base64 encoded json string
    jsonBytes, err := json.Marshal(doc)
    if err != nil {
        return "", err
    }

    return base64.StdEncoding.EncodeToString(jsonBytes), nil
}

func createSignature(secretKey string, region string, dateString string, stringToSign string) string {

    // Helper to make the HMAC-SHA256.
    makeHmac := func(key []byte, data []byte) []byte {
        hash := hmac.New(sha256.New, key)
        hash.Write(data)
        return hash.Sum(nil)
    }

    h1 := makeHmac([]byte("AWS4"+secretKey), []byte(dateString))
    h2 := makeHmac(h1, []byte(region))
    h3 := makeHmac(h2, []byte("s3"))
    h4 := makeHmac(h3, []byte("aws4_request"))
    signature := makeHmac(h4, []byte(stringToSign))
    return hex.EncodeToString(signature)
}

用法

// credentials
conf, _ := config.LoadDefaultConfig(c.Context)
awsCreds, _ := conf.Credentials.Retrieve(c.Context)

// generate presigned post
post, err := s3util.NewPresignedPost(&s3util.NewPresignedPostInput{
    Key:         <file-name>,
    Credentials: awsCreds,
    Region:      <region>,
    Bucket:      <bucket-name>,
    ExpiresIn:   <expiration>,
    Conditions: []interface{}{
        []interface{}{"content-length-range", 1, <size-limit>},
    },
})

然后在前端,在POST表单数据中使用返回的json

key:                    <key>
X-Amz-Credential:       <credential>
X-Amz-Security-Token:   <securityToken>     // if provided
X-Amz-Algorithm:        AWS4-HMAC-SHA256
X-Amz-Date:             <date>
Policy:                 <policy>
X-Amz-Signature:        <signature>
file:                   <file>