跳转至

请求构造与签名指南

本章节说明在对接管理类接口时如何构造 HTTP 请求,确保请求可被成功验证。

1. 请求基本要素

  • HTTP 方法:根据操作选择 GETPOSTPUT 等,签名时必须使用大写方法名。
  • 终端地址:使用符合环境(Sandbox/Production)的完整 URL,包括查询参数。
  • 关键头部
  • Content-Digest:请求体的 SHA-256 摘要,没有请求体时需对空字符串取摘要。
  • Signature-InputSignature:签名元数据与签名结果,标签统一为 magik
  • 请求体:JSON 负载需严格遵循接口定义,签名前后不得篡改。

2. 签名流程

  1. 准备签名组件
    挑选并排序需要签名的组件:"content-digest""@path""@method""@query"。若无查询参数,@query 值必须设为 ?

  2. 构造 Signature-Input Signature-Input: magik=("content-digest" "@path" "@method" "@query");keyid="<access-key-id>";created=<timestamp>;nonce=<random-uint32>;alg="hmac-sha256"

    • keyid: 由 MagikCloud 签发的访问密钥对中的 AccessKeyId。
    • created:Unix 秒级时间戳,服务端接收时若超过 5 分钟会视为过期。
    • nonce:UINT32 类型的随机数。
    • alg:固定为 hmac-sha256
  3. 拼接签名基串
    将各组件按顺序写成 "key": value 形式,每行一条,并在末尾附加 "@signature-params" 指向上一步的参数列。

  4. 生成数字签名
    使用与 AccessKeyId 对应的SecretAccessKey,对基串应用 hmac-sha256 算法得到签名值,并写入: Signature: magik=:<Base64Signature>:

  5. 发送请求并处理错误
    若签名或证书校验失败,MagikCloud 会返回 4xx 错误,响应体 codemessage 字段提示缺失头部、摘要错误或签名无效等原因,应据此排查。

3. 代码示例

package httpsign

import (
    "bytes"
    "crypto/hmac"
    "crypto/rand"
    "crypto/sha256"
    "encoding/base64"
    "fmt"
    "io"
    "net/http"
    "strings"
    "time"
)

// SignRequest 使用给定的 accessKeyId 和 secretAccessKey 对 HTTP 请求进行签名。
func SignRequest(req *http.Request, accessKeyId string, secretAccessKey []byte) error {
    if req == nil {
        return fmt.Errorf("request is nil")
    }
    if req.Body != nil {
        bodyBytes, err := io.ReadAll(req.Body)
        if err != nil {
            return fmt.Errorf("failed to read request body: %w", err)
        }
        _ = req.Body.Close()
        sum := sha256.Sum256(bodyBytes)
        digestBase64 := base64.URLEncoding.EncodeToString(sum[:])
        req.Header.Set("Content-Digest", fmt.Sprintf("%s=%s", "sha-256", digestBase64))
        req.Body = io.NopCloser(bytes.NewReader(bodyBytes))
    }
    nonceBytes := make([]byte, 32)
    _, _ = rand.Read(nonceBytes)
    created := time.Now()
    nonce := base64.RawURLEncoding.EncodeToString(nonceBytes)
    alg := "hmac-sha256"
    if req.Method == "" {
        return fmt.Errorf("method is required")
    }
    query := "?"
    if req.URL != nil {
        if raw := req.URL.RawQuery; raw != "" {
            query += raw
        }
    }
    cd := req.Header.Get("Content-Digest")
    if cd == "" {
        return fmt.Errorf("Content-Digest header is required")
    }
    path := "/"
    if req.URL != nil && req.URL.Path != "" {
        path = req.URL.Path
    }
    escape := func(s string) string {
        s = strings.ReplaceAll(s, `\`, `\\`)
        s = strings.ReplaceAll(s, `"`, `\"`)
        return s
    }
    pairs := []string{
        fmt.Sprintf("created=%d", created.Unix()),
        fmt.Sprintf(`nonce="%s"`, escape(nonce)),
        fmt.Sprintf(`keyid="%s"`, escape(accessKeyId)),
        fmt.Sprintf(`alg="%s"`, escape(alg)),
    }
    sigParams := `("@method" "@query" "@path" "content-digest")`
    if len(pairs) > 0 {
        sigParams += ";" + strings.Join(pairs, ";")
    }
    parts := []string{
        fmt.Sprintf(`"@method": %s`, req.Method),
        fmt.Sprintf(`"@query": %s`, query),
        fmt.Sprintf(`"@path": %s`, path),
        fmt.Sprintf(`"content-digest": %s`, cd),
        fmt.Sprintf(`"@signature-params": %s`, sigParams),
    }
    signatureBase := strings.Join(parts, "\n")
    for _, r := range signatureBase {
        if r > 127 {
            return fmt.Errorf("signature base contains non-ASCII character")
        }
    }
    mac := hmac.New(sha256.New, secretAccessKey)
    if _, err := mac.Write([]byte(signatureBase)); err != nil {
        return fmt.Errorf("failed to compute hmac: %w", err)
    }
    sigBytes := mac.Sum(nil)
    sigInput := sigParams
    label := "magik"
    signatureInputValue := fmt.Sprintf(`%s=%s`, label, sigInput)
    sigBase64 := base64.RawURLEncoding.EncodeToString(sigBytes)
    signatureValueValue := fmt.Sprintf(`%s=:%s:`, label, sigBase64)
    req.Header.Set("Signature-Input", signatureInputValue)
    req.Header.Set("Signature", signatureValueValue)
    return nil
}