You need to enable JavaScript to run this app.
导航
签名机制
最近更新时间:2024.07.08 11:50:35首次发布时间:2024.07.08 11:32:09

火山引擎对于每一次的 HTTPS 协议访问请求,会通过访问签名信息中的访问密钥(包括 Access Key ID 和 Secret Access Key),验证访问请求者身份。

获取访问密钥

账户和有权限的用户可以新建访问密钥,操作如下:

  1. 使用主账号或拥有密钥管理权限的 IAM 用户登录 访问控制控制台
  2. 在左侧导航栏选择 资源管理 > 密钥管理
  3. 查看您账号的访问密钥列表。
    • 每个账号最多同时拥有 2 个访问密钥。
    • 如果当前账号的访问密钥数量未达到上限,则可单击 新建密钥,创建新的访问密钥。创建完成后单击 查看AccessKey详情,查看新的访问密钥信息。

创建一个正规化请求

说明

  • Hash 代指 SHA256 算法。
  • HexEncode 代指转 16 进制编码。
CanonicalRequest = HTTPRequestMethod + '\n' + CanonicalURI + '\n' + CanonicalQueryString + '\n' + CanonicalHeaders + '\n' + SignedHeaders + '\n' + HexEncode(Hash(RequestPayload))

在签名之前,要将请求正规化,使得签名计算过程无异议,其主要过程及伪代码如下:

字段说明
HTTPRequestMethodHTTP 请求方法,例如 POST。
CanonicalURI正规化后的 URI。如果 URI 为空,那么使用/作为绝对路径。

CanonicalQueryString

正规化后的 QueryString。QueryString 正规化流程如下:

  1. URL 编码(UrlEncode)每个 QueryString 参数名称和参数值。
  2. 按照 ASCII 字节顺序对参数名称严格排序。
  3. 将排序好的参数名称和参数值用=连接;按照排序结果将参数对用&连接。
CanonicalQueryString = "Action=ListUsers&Version=2018-01-01"

CanonicalHeaders

正规化后的Header。其中伪代码如下:

CanonicalHeaders = CanonicalHeadersEntry0 + CanonicalHeadersEntry1 + ... + CanonicalHeadersEntryN
CanonicalHeadersEntry = Lowercase(HeaderName) + ':' + Trimall(HeaderValue) + '\n'

其中,Lowercase表示将 Header 的名称全部转化成小写,Trimall表示去掉 Header 的值的前后多余空格。

注意

  • 结尾需要添加换行符:`\n`。
  • Header 的顺序由headerName的小写后 ASCII 排序。

SignedHeaders

参与签名的 Header 名称。
签名 Header 需包含在正规化 Headers 名称列表中,用于指明哪些 Header 参与签名计算,从而忽略请求被 Proxy 添加的额外 Header。如果存在hostx-date两个 Header,则必须添加到正规化 Headers 名称列表中。
伪代码如下:

SignedHeaders = Lowercase(HeaderName0) + ';' + Lowercase(HeaderName1) + ";" + ... + Lowercase(HeaderNameN)

RequestPayload

完整的请求的 Body。

  • URL 编码(同 RFC3986 方法) 每个 QueryString 参数名称和参数值。
    其中 GET 方法需要包含哈希算法、信任状、签名日期和签名 Header 等全部参数。
  • 按照 ASCII 字节顺序对参数名称严格排序。
  • 将排序好的参数名称和参数值用=连接;按照排序结果将参数对用&连接。

创建签名字符串

签名字符串主要包含请求以及正规化请求的元数据信息,由签名算法、请求日期、信任状和正规化请求哈希值连接组成,伪代码如下:

StringToSign = Algorithm + '\n' + RequestDate + '\n' + CredentialScope + '\n' + HexEncode(Hash(CanonicalRequest))

字段说明如下。

字段说明
Algorithm签名的算法。目前火山引擎仅支持 HMAC-SHA256 签名算法。
RequestDate请求 UTC 时间。请使用YYYYMMDD'T'HHMMSS'Z'格式,例如:20201103T104027Z
CredentialScope凭证范围。格式为YYYYMMDD/region/service/request
CanonicalRequest前序正规化请求的结果。

计算签名秘钥

  1. 计算签名前,首先从 Secret Access Key 派生出签名密钥(Signing Key),而不是直接使用 Secret Access Key。具体计算过程如下:
    kSecret = *Your Secret Access Key*
    kDate = HMAC(kSecret, Date)
    kRegion = HMAC(kDate, Region)
    kService = HMAC(kRegion, Service)
    kSigning = HMAC(kService, "request")
    
  2. 计算签名。
    Signature = HexEncode(HMAC(kSigning, StringToSign))
    
  3. 构建 Header。

    说明

    表达式中用{}内的内容,代表上文计算出的中间过程。

    Authorization: HMAC-SHA256 Credential={AccessKeyId}/{CredentialScope}, SignedHeaders={SignedHeaders}, Signature={Signature}
    

代码示例

以下是签名机制的 Golang 实现,签名后,调用 veFaaS OpenAPI ListFunctions 接口。Python、Java、NodeJS、PHP 等更多语言实现的签名算法和示例,请参见 volcengine/volc-openapi-demos

package main

import (
    "bytes"
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "fmt"
    "log"
    "net/http"
    "net/http/httputil"
    "net/url"
    "strings"
    "time"
)

const (
    AccessKeyID     = "AKxxx"
    SecretAccessKey = "yyyyyy"

    Addr = "https://iam.volcengineapi.com"
    Path = "/" // 路径,不包含 Query

    Service = "vefaas"
    Region  = "cn-beijing"
    Action  = "ListFunctions"
    Version = "2024-06-06"
)

func hmacSHA256(key []byte, content string) []byte {
    mac := hmac.New(sha256.New, key)
    mac.Write([]byte(content))
    return mac.Sum(nil)
}

func getSignedKey(secretKey, date, region, service string) []byte {
    kDate := hmacSHA256([]byte(secretKey), date)
    kRegion := hmacSHA256(kDate, region)
    kService := hmacSHA256(kRegion, service)
    kSigning := hmacSHA256(kService, "request")

    return kSigning
}

func hashSHA256(data []byte) []byte {
    hash := sha256.New()
    if _, err := hash.Write(data); err != nil {
       log.Printf("input hash err:%s", err.Error())
    }

    return hash.Sum(nil)
}

func doRequest(method string, queries url.Values, body []byte) error {
    // 1. 构建请求
    queries.Set("Action", Action)
    queries.Set("Version", Version)
    requestAddr := fmt.Sprintf("%s%s?%s", Addr, Path, queries.Encode())
    log.Printf("request addr: %s\n", requestAddr)

    request, err := http.NewRequest(method, requestAddr, bytes.NewBuffer(body))
    if err != nil {
       return fmt.Errorf("bad request: %w", err)
    }

    // 2. 构建签名材料
    now := time.Now()
    date := now.UTC().Format("20060102T150405Z")
    authDate := date[:8]
    request.Header.Set("X-Date", date)

    payload := hex.EncodeToString(hashSHA256(body))
    request.Header.Set("X-Content-Sha256", payload)
    request.Header.Set("Content-Type", "application/x-www-form-urlencoded")

    queryString := strings.Replace(queries.Encode(), "+", "%20", -1)
    signedHeaders := []string{"host", "x-date", "x-content-sha256", "content-type"}
    var headerList []string
    for _, header := range signedHeaders {
       if header == "host" {
          headerList = append(headerList, header+":"+request.Host)
       } else {
          v := request.Header.Get(header)
          headerList = append(headerList, header+":"+strings.TrimSpace(v))
       }
    }
    headerString := strings.Join(headerList, "\n")

    canonicalString := strings.Join([]string{
       method,
       Path,
       queryString,
       headerString + "\n",
       strings.Join(signedHeaders, ";"),
       payload,
    }, "\n")
    log.Printf("canonical string:\n%s\n", canonicalString)

    hashedCanonicalString := hex.EncodeToString(hashSHA256([]byte(canonicalString)))
    log.Printf("hashed canonical string: %s\n", hashedCanonicalString)

    credentialScope := authDate + "/" + Region + "/" + Service + "/request"
    signString := strings.Join([]string{
       "HMAC-SHA256",
       date,
       credentialScope,
       hashedCanonicalString,
    }, "\n")
    log.Printf("sign string:\n%s\n", signString)

    // 3. 构建认证请求头
    signedKey := getSignedKey(SecretAccessKey, authDate, Region, Service)
    signature := hex.EncodeToString(hmacSHA256(signedKey, signString))
    log.Printf("signature: %s\n", signature)

    authorization := "HMAC-SHA256" +
       " Credential=" + AccessKeyID + "/" + credentialScope +
       ", SignedHeaders=" + strings.Join(signedHeaders, ";") +
       ", Signature=" + signature
    request.Header.Set("Authorization", authorization)
    log.Printf("authorization: %s\n", authorization)

    // 4. 打印请求,发起请求
    requestRaw, err := httputil.DumpRequest(request, true)
    if err != nil {
       return fmt.Errorf("dump request err: %w", err)
    }

    log.Printf("request:\n%s\n", string(requestRaw))

    response, err := http.DefaultClient.Do(request)
    if err != nil {
       return fmt.Errorf("do request err: %w", err)
    }

    // 5. 打印响应
    responseRaw, err := httputil.DumpResponse(response, true)
    if err != nil {
       return fmt.Errorf("dump response err: %w", err)
    }

    log.Printf("response:\n%s\n", string(responseRaw))

    if response.StatusCode == 200 {
       log.Printf("请求成功")
    } else {
       log.Printf("请求失败")
    }

    return nil
}

func main() {
    // GET 请求例子
    query1 := make(url.Values)
    doRequest(http.MethodGet, query1, nil)

}