如何在 Go 中将预签名 POST 上传到 AWS S3?
How to do a Pre-signed POST upload to AWS S3 in Go?
我想做一个 pre-signed POST to upload files to an AWS S3 bucket - 在 Go 中如何完成?
请注意,这与使用 PUT 进行预签名上传不同。
乍一看,POST 可以使用附加的策略和签名——专为基于浏览器的上传而设计。参见AWS Docs for details。
具体来说,您需要 generate a policy and sign that - 然后将它们包含在 HTML 表单中,从而在 POST 请求中 - 以及其他所需信息。或者让浏览器为您完成。
对于 HTML 表单 POST 上传,您仅签署政策字符串。最终要发布到的 URL 可能因表单内容而异:https://bucket.s3.amazonaws.com/<depends-on-form-content>
。所以你不能预先签署 URL 因为你不知道它是什么。
这与您 PUT 文件的签名 URL 不同。您可以签名,因为您知道完整的 URL:https://bucket.s3.amazonaws.com/known-key
您可以使用适当的策略和参数构建一个 POST 请求,然后通过 POST 上传。但是,您需要知道表单的内容才能事先知道 URL。在这种情况下,您也可以使用预先签名的 PUT URL.
至少乍一看是这样...
所以为了帮助别人,我会自己回答这个问题,并提供一些代码来帮助其他可能遇到同样问题的人。
可以找到 Google App Engine 呈现预签名 POST 表单的示例 Web 应用 here。
和a small library I created doing the pre-signed POST in Go.
简而言之,对 public-read Amazon S3 存储桶进行预签名 POST 您需要:
1.将 S3 存储桶配置为仅允许 public 下载。
示例存储桶策略仅允许 public 读取。
{
"Version": "2012-10-17",
"Id": "akjsdhakshfjlashdf",
"Statement": [
{
"Sid": "kjahsdkajhsdkjasda",
"Effect": "Allow",
"Principal": {
"AWS": "*"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::BUCKETNAMEHERE/*"
}
]
}
2。为允许上传的 HTTP POST 创建策略。
示例 POST 策略模板过期将特定密钥上传到特定存储桶并允许 public- 读取访问权限。
{ "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 存储桶所有者的凭据生成并签署策略。
- 为过期时间、存储桶、密钥、凭据和日期填写正确的值。
- base64 编码策略。
- HMAC-SHA256 获取签名的策略。
- 十六进制编码签名。
4.构建并POST 多部分表单数据
现在要么生成一个 HTML 表单并自动获取正确的多部分表单数据请求,如上文所述 link.
我想在 Go 中手动执行此操作,下面是如何操作。
无论哪种方式,您都需要提供在第 2 步和第 3 步中创建的 POST 策略中指定的所有部分。除了必填字段(不是在政策中)。
字段的顺序也是指定的,它们都是 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
}
这是 https://github.com/minio/minio-go 的另一种方法
您可能会喜欢生成预签名 post 策略的完整编程方式。
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
结构和 returns 返回 "key/values" 的映射,可用于您的 HTML 表单或 curl 命令上传数据到 s3。
遇到这个问题并在尝试使用@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>
我想做一个 pre-signed POST to upload files to an AWS S3 bucket - 在 Go 中如何完成?
请注意,这与使用 PUT 进行预签名上传不同。
乍一看,POST 可以使用附加的策略和签名——专为基于浏览器的上传而设计。参见AWS Docs for details。
具体来说,您需要 generate a policy and sign that - 然后将它们包含在 HTML 表单中,从而在 POST 请求中 - 以及其他所需信息。或者让浏览器为您完成。
对于 HTML 表单 POST 上传,您仅签署政策字符串。最终要发布到的 URL 可能因表单内容而异:https://bucket.s3.amazonaws.com/<depends-on-form-content>
。所以你不能预先签署 URL 因为你不知道它是什么。
这与您 PUT 文件的签名 URL 不同。您可以签名,因为您知道完整的 URL:https://bucket.s3.amazonaws.com/known-key
您可以使用适当的策略和参数构建一个 POST 请求,然后通过 POST 上传。但是,您需要知道表单的内容才能事先知道 URL。在这种情况下,您也可以使用预先签名的 PUT URL.
至少乍一看是这样...
所以为了帮助别人,我会自己回答这个问题,并提供一些代码来帮助其他可能遇到同样问题的人。
可以找到 Google App Engine 呈现预签名 POST 表单的示例 Web 应用 here。
和a small library I created doing the pre-signed POST in Go.
简而言之,对 public-read Amazon S3 存储桶进行预签名 POST 您需要:
1.将 S3 存储桶配置为仅允许 public 下载。
示例存储桶策略仅允许 public 读取。
{
"Version": "2012-10-17",
"Id": "akjsdhakshfjlashdf",
"Statement": [
{
"Sid": "kjahsdkajhsdkjasda",
"Effect": "Allow",
"Principal": {
"AWS": "*"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::BUCKETNAMEHERE/*"
}
]
}
2。为允许上传的 HTTP POST 创建策略。
示例 POST 策略模板过期将特定密钥上传到特定存储桶并允许 public- 读取访问权限。
{ "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 存储桶所有者的凭据生成并签署策略。
- 为过期时间、存储桶、密钥、凭据和日期填写正确的值。
- base64 编码策略。
- HMAC-SHA256 获取签名的策略。
- 十六进制编码签名。
4.构建并POST 多部分表单数据
现在要么生成一个 HTML 表单并自动获取正确的多部分表单数据请求,如上文所述 link.
我想在 Go 中手动执行此操作,下面是如何操作。
无论哪种方式,您都需要提供在第 2 步和第 3 步中创建的 POST 策略中指定的所有部分。除了必填字段(不是在政策中)。
字段的顺序也是指定的,它们都是 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
}
这是 https://github.com/minio/minio-go 的另一种方法 您可能会喜欢生成预签名 post 策略的完整编程方式。
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
结构和 returns 返回 "key/values" 的映射,可用于您的 HTML 表单或 curl 命令上传数据到 s3。
遇到这个问题并在尝试使用@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 表单数据
中使用返回的 jsonkey: <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>