Go реализует аутентификацию запросов AWS4 | Go Topic Month

Go AWS
Go реализует аутентификацию запросов AWS4 | Go Topic Month

что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
}

Суммировать

Одним словом, Наггетс — быки!