请求构造与签名指南
本章节说明在对接管理类接口时如何构造 HTTP 请求,确保请求可被成功验证。
1. 请求基本要素
- HTTP 方法:根据操作选择
GET、POST、PUT等,签名时必须使用大写方法名。 - 终端地址:使用符合环境(Sandbox/Production)的完整 URL,包括查询参数。
- 关键头部:
Content-Digest:请求体的 SHA-256 摘要,没有请求体时需对空字符串取摘要。Signature-Input与Signature:签名元数据与签名结果,标签统一为magik。- 请求体:JSON 负载需严格遵循接口定义,签名前后不得篡改。
2. 签名流程
-
准备签名组件
挑选并排序需要签名的组件:"content-digest"、"@path"、"@method"、"@query"。若无查询参数,@query值必须设为?。 -
构造 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。
-
拼接签名基串
将各组件按顺序写成"key": value形式,每行一条,并在末尾附加"@signature-params"指向上一步的参数列。 -
生成数字签名
使用与AccessKeyId对应的SecretAccessKey,对基串应用hmac-sha256算法得到签名值,并写入:Signature: magik=:<Base64Signature>: - 发送请求并处理错误
若签名或证书校验失败,MagikCloud 会返回4xx错误,响应体code及message字段提示缺失头部、摘要错误或签名无效等原因,应据此排查。
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
}