文档中心>云应用>服务商指南>伙伴 API 签名方式

伙伴 API 签名方式

最近更新时间:2026-01-15 16:45:43

我的收藏

签名公钥

云应用访问伙伴接口采用 RSA 签名方式,公钥信息同 License JWT 校验公钥一致:
-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA5EQJv0v0f1hrB7NIGwXi
hFmw+ugrJi7gnmOqaiYp7oFrzf4RSJ3DPr4K01F+CrjTdCPghDLq4fsKVxxHAjNj
nbstbVlHZEVOfzQ4umeocJpxWFuyKyGwHv+obnEZ/4689fxVpTbG3IbUTGn1TRJs
9s3xM8nFd6LLAoh1Hhrdf2D4mLRToLvtRVat1l8fH3gsM+RoG4L4h+3hghn4bpyA
na2MBFDzvmBeVGUVzqRjSvUaexd+Bo1wTsllAdqjP6MTlAAWGmIAMStBSRS+YpRQ
xjhE9Rdb9zTE54q3Ui7UJg5BMe+R3kVrBINbnT6Va8/Lzjg4+THdpMTLr6fY6ObF
7r+i/924XgxqQOFvGaFJSyjXTORnK42T5YRr5TSqxr9CzhybPcdRvws2GdAq9f55
8whj1DYcgg0X8kR06Iu+/9Mk/CqssdrZ8LYDwSkDI8S/RwpdNQfifUa8wyY0R2xN
nY+bnkrjvGPz7Rokr0Ki9/orT9i4yQWA1mMCDi2vcP+oXqrEs7XAyH85gDSzuTp+
dXbTYPZpIAK6Kejwssw1IE1lGNP4PNQZk9EXU7+vB1csz4GUao7Mr7F5VbrGKvTs
aGxbIc6b0MDWMEFA7L/CWC9UtReWCk1MYwJzy105bWU/VBpYJPmyZTFRQaY2MEH4
fnsK2+jtZ1IYIQw/YsHU6CcCAwEAAQ==
-----END PUBLIC KEY-----

请求示例

POST http://localhost:8081/interfaces
Content-Type: application/json

{"Fields":{"aaa":1233,"BBBBB":"1212212"},"a111":"11111"}

签名过程

1. 拼接规范请求串

按如下伪代码格式拼接规范请求串(CanonicalRequest):
CanonicalRequest =
Algorithm + '\\n' +
RequestTimestamp + '\\n' +
HTTPRequestMethod + '\\n' +
CanonicalURI + '\\n' +
CanonicalQueryString + '\\n' +
CanonicalHeaders + '\\n' +
SignedHeaders + '\\n' +
HashedRequestPayload
字段名称
描述
Algorithm
签名算法名称。目前仅支持 RSA-SHA256。
RequestTimestamp
请求时间戳,精确到秒级。从 Header 中获取:X-Cloudapp-Timestamp。
HTTPRequestMethod
请求方法,支持 POST、GET。以接口实际访问方式为准。
CanonicalQueryString
发起 HTTP 请求 URL 中的查询字符串,对于 POST 请求,固定为空字符串"";对于 GET 请求,则为 URL 中问号(?)后面的字符串内容,例如:Limit=10&Offset=0。
注意:CanonicalQueryString 需要参考 RFC3986 进行 URLDecode 编码(特殊字符编码后需大写字母),字符集 UTF-8。推荐使用编程语言标准库进行编码。
CanonicalHeaders
参与签名的头部信息,至少包含 X-Cloudapp-Timestamp 和 X-Cloudapp-Host 两个头部,具体参与计算的 Header ,以 Header 中 X-Cloudapp-Signature-Headers 为准。
示例代码:
SignHeaderValue := []string{}
for _, v := range SignedHeaders {
SignHeaderValue = append(SignHeaderValue, fmt.Sprintf("%v=%v", strings.Trim(v, " "), strings.Trim(header.Get(v), " ")))
}
CanonicalHeaders := strings.Join(SignHeaderValue, "\\n")
SignedHeaders
参与签名的头部信息,说明此次请求有哪些头部参与了签名,和 CanonicalHeaders 包含的头部内容是一一对应的。示例:X-Cloudapp-Timestamp;X-Cloudapp-Host;content-type
HashedRequestPayload
请求正文的哈希值,计算伪代码为 Lowercase(HexEncode(Hash.SHA256(RequestPayload))),即对 HTTP 请求正文做 SHA256 哈希,然后十六进制编码,最后编码串转换成小写字母。
对于 GET 请求,RequestPayload 固定为空字符串。此示例计算结果是:35e9c5b0e3ae67532d3c9f17ead6c90222632e5b1ff7f6e89887f1398934f064。
根据以上规则,示例中得到的规范请求串如下:
HMAC-SHA256
1762256838
POST
/interfaces

X-Cloudapp-Timestamp=1762256838
X-Cloudapp-Host=localhost:8081
content-type=application/json
X-Cloudapp-Timestamp;X-Cloudapp-Host;content-type
56e18c53da8f844bb0394aea84de65396bd0b64514ae9b7818b214aee792768b

2. 验证签名

伪代码如下:
Signature = RsaSignVerify(PublicKey, HMAC_SHA256(CanonicalRequest), Base64Decode(Signature))
字段名称
描述
CanonicalRequest
签名字符串,第一步得到的值。
Signature
请求签名。从 header 的 X-Cloudapp-Signature 中获取到。
PublicKey
验签公钥。

3. 示例代码

package main

import (
"context"
"crypto"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"errors"
"fmt"
"net/http"
"net/url"
"reflect"
"sort"
"strings"

"github.com/gin-gonic/gin"
jsoniter "github.com/json-iterator/go"
)

const rsaPub = `-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvIPknZUDTxI8ep3wDFsN
C2vF1sTUfF8f6pnjSduwtIc5yUYV/1hONRe4DwWRiXQPrDRTjlDridNRfglmktoe
gUewNKfluGlxuTrUV35BBSGXdFTWJNg8/9j5zpsQS69mjwlh0wO8RxL0N9JatyHD
HZBg9psp4RGj57wxEdyANv5IUvPQ0MUwuZ64UATl/0VI5eRM1FCJI5rE9kC+eJyH
+c/63SNqBoSjG2kmXUb4nN8DPoDs90oA0wS2Yq1kr83kPAaFpcCIvnNKbXCK/hbY
Jymt92Tcd8/viCxcEd88hacfzavWkyiLPl0W7Golnn2N9ZIyPwUb3a52yC4HiS5h
4XQSogiFluMQ+OIm4YwoaGgTILoU/Ip03LX7AILNI/Fcx9oGsLv2v4Lj01bStdJj
7EaCeitIw3SVyjlNAaoBTLzee0opBgVHGf8AnCzzf6qe7a0ics+pJbJi8+SGN6CF
OBZxeQxqu7ZE9c6y05ZYQEh0e5V/5KlIZMG0FtmyMY1Q1l2CHjVJz4xzG8t8asqZ
jg7uVpsnQhOxrbz68cAnw9X/9297K62VnECa8z9/3kSfY0SWd+lmc5HpFRzRJPt8
DLjFR/r/turTJ+HnvUe1aJzD2oa8D8Y09T6gQWmAlmqLOnt7aSPm/zN3rVt/6CPY
6EKSQMgJ7oOgKg4FybkNELcCAwEAAQ==
-----END PUBLIC KEY-----`

func main() {
r := gin.Default()

r.Any("/*action", func(ctx *gin.Context) {
body, _ := ctx.GetRawData()
signedHeader := strings.Split(ctx.Request.Header.Get("X-cloudapp-Signature-Headers"), ";")

fmt.Printf("header: %v\\n", ctx.Request.Header)
switch ctx.Request.Header.Get("X-Cloudapp-Algorithm") {
case "RSA-SHA256":
if err := RsaSha256SignVerify(ctx, ctx.Request.Method, ctx.Request.URL.Path, ctx.Request.URL.RawQuery, string(body), ctx.Request.Header, signedHeader, ctx.Request.Header.Get("X-Cloudapp-Timestamp"), ctx.Request.Header.Get("X-Cloudapp-Signature")); err != nil {
ctx.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
return
}
ctx.JSON(http.StatusOK, map[string]any{
"OK": false,
"Algorithm": ctx.Request.Header.Get("X-Cloudapp-Algorithm"),
})
default:
ctx.JSON(http.StatusUnauthorized, gin.H{"error": "unsupported algorithm"})
}
})

r.Run(":8081")
}

func RsaSha256SignVerify(ctx context.Context, Method string, path string, query string, body string, header http.Header, SignedHeaders []string, RequestTimestamp string, signed string) error {
Algorithm := "RSA-SHA256"

SignHeaderValue := []string{}
for _, v := range SignedHeaders {
SignHeaderValue = append(SignHeaderValue, fmt.Sprintf("%v=%v", strings.Trim(v, " "), strings.Trim(header.Get(v), " ")))
}

fmt.Printf("Body: %v, sha256: %x\\n", body, sha256.Sum256([]byte(body)))
PayloadSha256 := fmt.Sprintf("%x", sha256.Sum256([]byte(body)))
CanonicalRequest :=
Algorithm + "\\n" + // 签名方法
RequestTimestamp + "\\n" + // 请求时间戳,精确到秒
Method + "\\n" + // 请求方法
path + "\\n" + // 请求路径
query + "\\n" + // GET 请求参数
strings.Join(SignHeaderValue, "\\n") + "\\n" + // 需要签名的 Header 信息
strings.Join(SignedHeaders, ";") + "\\n" + // 签名的 Header 名称
PayloadSha256 // POST 签名 body 的 sha256

HashedCanonicalRequest := sha256.Sum256([]byte(CanonicalRequest))
fmt.Printf("sign plain text : %v. sha256: %x signture: %v", CanonicalRequest, HashedCanonicalRequest, signed)

sign, _ := base64.StdEncoding.DecodeString(signed)

publicKey, _ := LoadPubKey(ctx, rsaPub)
// 使用 PKCS#1 v1.5 进行签名
if err := rsa.VerifyPKCS1v15(publicKey, crypto.SHA256, HashedCanonicalRequest[:], sign); err != nil {
return err
}

return nil
}

func SortParams(ctx context.Context, params map[string]any) map[string]any {
newParams := make(map[string]any)
keys := make([]string, 0, len(params))
for k := range params {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
if reflect.TypeOf(params[k]).Kind() == reflect.Map {
newParams[k] = SortParams(ctx, params[k].(map[string]any))
} else {
newParams[k] = params[k]
}
newParams[k] = params[k]
}
return newParams
}

func ParamsToQuery(params map[string]any) string {
query := url.Values{}
for k, v := range params {
if reflect.TypeOf(v).Kind() == reflect.Map {
js, _ := jsoniter.Marshal(v)
query.Add(k, string(js))
} else {
query.Add(k, fmt.Sprintf("%v", v))
}
}
return query.Encode()
}

func LoadPubKey(ctx context.Context, pubKey string) (*rsa.PublicKey, error) {
block, _ := pem.Decode([]byte(rsaPub))
if block == nil {
return nil, errors.New("failed to decode PEM block containing private key")
}

key, parseErr := x509.ParsePKIXPublicKey(block.Bytes)
if parseErr != nil {
return nil, parseErr
}
var ok bool
publicKey, ok := key.(*rsa.PublicKey)
if !ok {
return nil, errors.New("not an RSA private key")
}

return publicKey, nil
}