签名公钥
云应用访问伙伴接口采用 RSA 签名方式,公钥信息同 License JWT 校验公钥一致:
-----BEGIN PUBLIC KEY-----MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA5EQJv0v0f1hrB7NIGwXihFmw+ugrJi7gnmOqaiYp7oFrzf4RSJ3DPr4K01F+CrjTdCPghDLq4fsKVxxHAjNjnbstbVlHZEVOfzQ4umeocJpxWFuyKyGwHv+obnEZ/4689fxVpTbG3IbUTGn1TRJs9s3xM8nFd6LLAoh1Hhrdf2D4mLRToLvtRVat1l8fH3gsM+RoG4L4h+3hghn4bpyAna2MBFDzvmBeVGUVzqRjSvUaexd+Bo1wTsllAdqjP6MTlAAWGmIAMStBSRS+YpRQxjhE9Rdb9zTE54q3Ui7UJg5BMe+R3kVrBINbnT6Va8/Lzjg4+THdpMTLr6fY6ObF7r+i/924XgxqQOFvGaFJSyjXTORnK42T5YRr5TSqxr9CzhybPcdRvws2GdAq9f558whj1DYcgg0X8kR06Iu+/9Mk/CqssdrZ8LYDwSkDI8S/RwpdNQfifUa8wyY0R2xNnY+bnkrjvGPz7Rokr0Ki9/orT9i4yQWA1mMCDi2vcP+oXqrEs7XAyH85gDSzuTp+dXbTYPZpIAK6Kejwssw1IE1lGNP4PNQZk9EXU7+vB1csz4GUao7Mr7F5VbrGKvTsaGxbIc6b0MDWMEFA7L/CWC9UtReWCk1MYwJzy105bWU/VBpYJPmyZTFRQaY2MEH4fnsK2+jtZ1IYIQw/YsHU6CcCAwEAAQ==-----END PUBLIC KEY-----
请求示例
POST http://localhost:8081/interfacesContent-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。 |
CanonicalHeaders | 参与签名的头部信息,至少包含 X-Cloudapp-Timestamp 和 X-Cloudapp-Host 两个头部,具体参与计算的 Header ,以 Header 中 X-Cloudapp-Signature-Headers 为准。 示例代码:
|
SignedHeaders | 参与签名的头部信息,说明此次请求有哪些头部参与了签名,和 CanonicalHeaders 包含的头部内容是一一对应的。示例:X-Cloudapp-Timestamp;X-Cloudapp-Host;content-type |
HashedRequestPayload | 请求正文的哈希值,计算伪代码为 Lowercase(HexEncode(Hash.SHA256(RequestPayload))),即对 HTTP 请求正文做 SHA256 哈希,然后十六进制编码,最后编码串转换成小写字母。 对于 GET 请求,RequestPayload 固定为空字符串。此示例计算结果是:35e9c5b0e3ae67532d3c9f17ead6c90222632e5b1ff7f6e89887f1398934f064。 |
根据以上规则,示例中得到的规范请求串如下:
HMAC-SHA2561762256838POST/interfacesX-Cloudapp-Timestamp=1762256838X-Cloudapp-Host=localhost:8081content-type=application/jsonX-Cloudapp-Timestamp;X-Cloudapp-Host;content-type56e18c53da8f844bb0394aea84de65396bd0b64514ae9b7818b214aee792768b
2. 验证签名
伪代码如下:
Signature = RsaSignVerify(PublicKey, HMAC_SHA256(CanonicalRequest), Base64Decode(Signature))
字段名称 | 描述 |
CanonicalRequest | 签名字符串,第一步得到的值。 |
Signature | 请求签名。从 header 的 X-Cloudapp-Signature 中获取到。 |
PublicKey | 验签公钥。 |
3. 示例代码
package mainimport ("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-----MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvIPknZUDTxI8ep3wDFsNC2vF1sTUfF8f6pnjSduwtIc5yUYV/1hONRe4DwWRiXQPrDRTjlDridNRfglmktoegUewNKfluGlxuTrUV35BBSGXdFTWJNg8/9j5zpsQS69mjwlh0wO8RxL0N9JatyHDHZBg9psp4RGj57wxEdyANv5IUvPQ0MUwuZ64UATl/0VI5eRM1FCJI5rE9kC+eJyH+c/63SNqBoSjG2kmXUb4nN8DPoDs90oA0wS2Yq1kr83kPAaFpcCIvnNKbXCK/hbYJymt92Tcd8/viCxcEd88hacfzavWkyiLPl0W7Golnn2N9ZIyPwUb3a52yC4HiS5h4XQSogiFluMQ+OIm4YwoaGgTILoU/Ip03LX7AILNI/Fcx9oGsLv2v4Lj01bStdJj7EaCeitIw3SVyjlNAaoBTLzee0opBgVHGf8AnCzzf6qe7a0ics+pJbJi8+SGN6CFOBZxeQxqu7ZE9c6y05ZYQEh0e5V/5KlIZMG0FtmyMY1Q1l2CHjVJz4xzG8t8asqZjg7uVpsnQhOxrbz68cAnw9X/9297K62VnECa8z9/3kSfY0SWd+lmc5HpFRzRJPt8DLjFR/r/turTJ+HnvUe1aJzD2oa8D8Y09T6gQWmAlmqLOnt7aSPm/zN3rVt/6CPY6EKSQMgJ7oOgKg4FybkNELcCAwEAAQ==-----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 的 sha256HashedCanonicalRequest := 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 boolpublicKey, ok := key.(*rsa.PublicKey)if !ok {return nil, errors.New("not an RSA private key")}return publicKey, nil}