чтоAmazon S3? Это сервис хранения объектов от AWS, который обеспечивает лучшую в отрасли масштабируемость, доступность данных, безопасность и производительность. Amazon S3 достигает надежности 99,999999999 % (11 9 с).
Используется в Amazon S3AWS Signature Version 4(именуемый в дальнейшемAWS4), чтобы сделать запрос на аутентификацию, в этой статье объясняется, как использовать Go для реализации аутентификации запроса AWS4.
AWS4 — это протокол для аутентификации входящих запросов API ко всем региональным сервисам AWS.
AWS4
Запросы подписи AWS4 имеют следующие преимущества (но это также зависит от того, как вы его используете):
-
Подтвердить личность запрашивающего- Аутентифицированные запросы требуют использования
AccessKeyIDиSecretAccessKeyСоздайте подпись. -
Защитите данные при передаче- Чтобы предотвратить подделку запроса во время передачи, некоторые элементы запроса (такие как
请求路径,请求头и т. д.) для вычисления подписи запроса. После того как Amazon S3 получит запрос, он использует тот же элемент запроса для вычисления подписи. Если какие-либо компоненты запроса, полученные Amazon S3, не совпадают с компонентами, используемыми для вычисления подписи, Amazon S3 отклонит запрос. - Предотвращение повторного использования подписанных частей запросов- Подписанная часть запроса действительна в течение периода времени с отметки времени в запросе.
Метод авторизации
- Заголовки аутентификации HTTP, такие как
AuthorizationЗаголовок запроса:
Authorization: AWS4-HMAC-SHA256
Credential=AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request,
SignedHeaders=host;range;x-amz-date,
Signature=fe5f80f77d5fa3beca038a248ff027d0445342fe2855ddc963176630326f1024
- Параметры строки запроса URL, такие как предварительно подписанные URL:
https://s3.amazonaws.com/examplebucket/test.txt
?X-Amz-Algorithm=AWS4-HMAC-SHA256
&X-Amz-Credential=<your-access-key-id>/20130721/us-east-1/s3/aws4_request
&X-Amz-Date=20130721T201207Z
&X-Amz-Expires=86400
&X-Amz-SignedHeaders=host
&X-Amz-Signature=<signature-value>
Go реализует заголовки аутентификации HTTP
Компоненты заголовка аутентификации HTTP
- AWS4-HMAC-SHA256- Строка указывает AWS4 и алгоритм подписи (HMAC-SHA256).
-
Credential- Укажите AccessKeyID, дату, регион и сервис для вычисления подписи. Его формат
<your-access-key-id>/<date>/<aws-region>/<aws-service>/aws4_request,dateФорматYYYYMMDD. -
SignedHeaders- Укажите список заголовков запроса для использования для расчета подписи, разделенных точкой с запятой. Содержит только название заголовка запроса и должно быть в нижнем регистре, например:
host;range;x-amz-date. -
Signature- 256-битная подпись, представленная 64 строчными шестнадцатеричными символами, например:
fe5f80f77d5fa3beca038a248ff027d0445342fe2855ddc963176630326f1024.
Загрузить изображение в Наггетс
На основе информации, полученной в результате запроса сертификации AWS4, и см.simples3Код реализуетЗагрузить изображение в Наггетс(Служба хранения Bytedance, ее имя службыimagex), следующее является частью реализации кода (в основном опущеноClientЧасть реализации, полный код будет в открытом доступе после последующей доработки):
package juejin
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"hash/crc32"
"io"
"io/ioutil"
"net/http"
"net/url"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
"time"
"github.com/tidwall/gjson"
)
const (
amzDateISO8601TimeFormat = "20060102T150405Z"
shortTimeFormat = "20060102"
algorithm = "AWS4-HMAC-SHA256"
serviceName = "imagex"
serviceID = "k3u1fbpfcp"
version = "2018-08-01"
uploadURLFormat = "https://%s/%s"
RegionCNNorth = "cn-north-1"
actionApplyImageUpload = "ApplyImageUpload"
actionCommitImageUpload = "CommitImageUpload"
polynomialCRC32 = 0xEDB88320
)
var (
newLine = []byte{'\n'}
// if object matches reserved string, no need to encode them
reservedObjectNames = regexp.MustCompile("^[a-zA-Z0-9-_.~/]+$")
)
type ImageX struct {
AccessKey string
SecretKey string
Region string
Client *http.Client
Token string
Version string
BaseURL string
}
type UploadToken struct {
AccessKeyID string `json:"AccessKeyID"`
SecretAccessKey string `json:"SecretAccessKey"`
SessionToken string `json:"SessionToken"`
}
func (c *Client) UploadImage(region, imgPath string) (string, error) {
uploadToken, err := c.GetUploadToken()
if err != nil {
return "", err
}
ix := &ImageX{
AccessKey: uploadToken.AccessKeyID,
SecretKey: uploadToken.SecretAccessKey,
Token: uploadToken.SessionToken,
Region: region,
}
applyRes, err := ix.ApplyImageUpload()
if err != nil {
return "", err
}
storeInfo := gjson.Get(applyRes, "Result.UploadAddress.StoreInfos.0")
storeURI := storeInfo.Get("StoreUri").String()
storeAuth := storeInfo.Get("Auth").String()
uploadHost := gjson.Get(applyRes, "Result.UploadAddress.UploadHosts.0").String()
uploadURL := fmt.Sprintf(uploadURLFormat, uploadHost, storeURI)
if err := ix.Upload(uploadURL, imgPath, storeAuth); err != nil {
return "", err
}
sessionKey := gjson.Get(applyRes, "Result.UploadAddress.SessionKey").String()
if _, err = ix.CommitImageUpload(sessionKey); err != nil {
return "", err
}
return c.GetImageURL(storeURI)
}
func (c *Client) GetImageURL(uri string) (string, error) {
endpoint := "/imagex/get_img_url"
params := &url.Values{
"uri": []string{uri},
}
raw, err := c.Get(APIBaseURL, endpoint, params)
if err != nil {
return "", err
}
rawurl := gjson.Get(raw, "data.main_url").String()
return rawurl, nil
}
func (c *Client) GetUploadToken() (*UploadToken, error) {
endpoint := "/imagex/gen_token"
params := &url.Values{
"client": []string{"web"},
}
raw, err := c.Get(APIBaseURL, endpoint, params)
if err != nil {
return nil, err
}
var token *UploadToken
err = json.Unmarshal([]byte(gjson.Get(raw, "data.token").String()), &token)
return token, err
}
func (ix *ImageX) ApplyImageUpload() (string, error) {
rawurl := fmt.Sprintf("https://imagex.bytedanceapi.com/?Action=%s&Version=%s&ServiceId=%s",
actionApplyImageUpload, version, serviceID)
req, err := http.NewRequest(http.MethodGet, rawurl, nil)
if err != nil {
return "", err
}
if err := ix.signRequest(req); err != nil {
return "", err
}
res, err := ix.getClient().Do(req)
if err != nil {
return "", err
}
defer res.Body.Close()
b, err := ioutil.ReadAll(res.Body)
if err != nil {
return "", err
}
raw := string(b)
if res.StatusCode != 200 || gjson.Get(raw, "ResponseMetadata.Error").Exists() {
return "", fmt.Errorf("raw: %s, response: %+v", raw, res)
}
return raw, nil
}
func (ix *ImageX) CommitImageUpload(sessionKey string) (string, error) {
rawurl := fmt.Sprintf("https://imagex.bytedanceapi.com/?Action=%s&Version=%s&SessionKey=%s&ServiceId=%s",
actionCommitImageUpload, version, sessionKey, serviceID)
req, err := http.NewRequest(http.MethodPost, rawurl, nil)
if err != nil {
return "", err
}
if err := ix.signRequest(req); err != nil {
return "", err
}
res, err := ix.getClient().Do(req)
if err != nil {
return "", err
}
defer res.Body.Close()
b, err := ioutil.ReadAll(res.Body)
if err != nil {
return "", err
}
raw := string(b)
if res.StatusCode != 200 || gjson.Get(raw, "ResponseMetadata.Error").Exists() {
return "", fmt.Errorf("raw: %s, response: %+v", raw, res)
}
return raw, nil
}
func (ix *ImageX) getClient() *http.Client {
if ix.Client == nil {
return http.DefaultClient
}
return ix.Client
}
func (ix *ImageX) signKeys(t time.Time) []byte {
h := makeHMac([]byte("AWS4"+ix.SecretKey), []byte(t.Format(shortTimeFormat)))
h = makeHMac(h, []byte(ix.Region))
h = makeHMac(h, []byte(serviceName))
h = makeHMac(h, []byte("aws4_request"))
return h
}
func (ix *ImageX) writeRequest(w io.Writer, r *http.Request) error {
r.Header.Set("host", r.Host)
w.Write([]byte(r.Method))
w.Write(newLine)
writeURI(w, r)
w.Write(newLine)
writeQuery(w, r)
w.Write(newLine)
writeHeader(w, r)
w.Write(newLine)
w.Write(newLine)
writeHeaderList(w, r)
w.Write(newLine)
return writeBody(w, r)
}
func (ix *ImageX) writeStringToSign(w io.Writer, t time.Time, r *http.Request) error {
w.Write([]byte(algorithm))
w.Write(newLine)
w.Write([]byte(t.Format(amzDateISO8601TimeFormat)))
w.Write(newLine)
w.Write([]byte(ix.creds(t)))
w.Write(newLine)
h := sha256.New()
if err := ix.writeRequest(h, r); err != nil {
return err
}
fmt.Fprintf(w, "%x", h.Sum(nil))
return nil
}
func (ix *ImageX) creds(t time.Time) string {
return t.Format(shortTimeFormat) + "/" + ix.Region + "/" + serviceName + "/aws4_request"
}
func (ix *ImageX) signRequest(req *http.Request) error {
t := time.Now().UTC()
req.Header.Set("x-amz-date", t.Format(amzDateISO8601TimeFormat))
req.Header.Set("x-amz-security-token", ix.Token)
k := ix.signKeys(t)
h := hmac.New(sha256.New, k)
if err := ix.writeStringToSign(h, t, req); err != nil {
return err
}
auth := bytes.NewBufferString(algorithm)
auth.Write([]byte(" Credential=" + ix.AccessKey + "/" + ix.creds(t)))
auth.Write([]byte{',', ' '})
auth.Write([]byte("SignedHeaders="))
writeHeaderList(auth, req)
auth.Write([]byte{',', ' '})
auth.Write([]byte("Signature=" + fmt.Sprintf("%x", h.Sum(nil))))
req.Header.Set("authorization", auth.String())
return nil
}
func writeURI(w io.Writer, r *http.Request) {
path := r.URL.RequestURI()
if r.URL.RawQuery != "" {
path = path[:len(path)-len(r.URL.RawQuery)-1]
}
slash := strings.HasSuffix(path, "/")
path = filepath.Clean(path)
if path != "/" && slash {
path += "/"
}
w.Write([]byte(path))
}
func writeQuery(w io.Writer, r *http.Request) {
var a []string
for k, vs := range r.URL.Query() {
k = url.QueryEscape(k)
for _, v := range vs {
if v == "" {
a = append(a, k)
} else {
v = url.QueryEscape(v)
a = append(a, k+"="+v)
}
}
}
sort.Strings(a)
for i, s := range a {
if i > 0 {
w.Write([]byte{'&'})
}
w.Write([]byte(s))
}
}
func writeHeader(w io.Writer, r *http.Request) {
i, a := 0, make([]string, len(r.Header))
for k, v := range r.Header {
sort.Strings(v)
a[i] = strings.ToLower(k) + ":" + strings.Join(v, ",")
i++
}
sort.Strings(a)
for i, s := range a {
if i > 0 {
w.Write(newLine)
}
io.WriteString(w, s)
}
}
func writeHeaderList(w io.Writer, r *http.Request) {
i, a := 0, make([]string, len(r.Header))
for k := range r.Header {
a[i] = strings.ToLower(k)
i++
}
sort.Strings(a)
for i, s := range a {
if i > 0 {
w.Write([]byte{';'})
}
w.Write([]byte(s))
}
}
func writeBody(w io.Writer, r *http.Request) error {
var (
b []byte
err error
)
// If the payload is empty, use the empty string as the input to the SHA256 function
// http://docs.amazonwebservices.com/general/latest/gr/sigv4-create-canonical-request.html
if r.Body == nil {
b = []byte("")
} else {
b, err = ioutil.ReadAll(r.Body)
if err != nil {
return err
}
r.Body = ioutil.NopCloser(bytes.NewBuffer(b))
}
h := sha256.New()
h.Write(b)
fmt.Fprintf(w, "%x", h.Sum(nil))
return nil
}
func makeHMac(key []byte, data []byte) []byte {
hash := hmac.New(sha256.New, key)
hash.Write(data)
return hash.Sum(nil)
}
func (ix *ImageX) Upload(rawurl, fp, auth string) error {
crc32, err := hashFileCRC32(fp)
if err != nil {
return err
}
file, err := os.Open(fp)
if err != nil {
return err
}
defer file.Close()
req, err := http.NewRequest(http.MethodPost, rawurl, file)
if err != nil {
return err
}
req.Header.Add("authorization", auth)
req.Header.Add("Content-Type", "application/octet-stream")
req.Header.Add("content-crc32", crc32)
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
b, err := ioutil.ReadAll(res.Body)
if err != nil {
return err
}
raw := string(b)
if gjson.Get(raw, "success").Int() != 0 {
return fmt.Errorf("raw: %s, response: %+v", raw, res)
}
return nil
}
// hashFileCRC32 generate CRC32 hash of a file
// Refer https://mrwaggel.be/post/generate-crc32-hash-of-a-file-in-golang-turorial/
func hashFileCRC32(filePath string) (string, error) {
file, err := os.Open(filePath)
if err != nil {
return "", err
}
defer file.Close()
tablePolynomial := crc32.MakeTable(polynomialCRC32)
hash := crc32.New(tablePolynomial)
if _, err := io.Copy(hash, file); err != nil {
return "", err
}
return hex.EncodeToString(hash.Sum(nil)), nil
}
Суммировать
Одним словом, Наггетс — быки!