SaaS 类产品开发接口接入到华为云云商店
in Tutorial with 0 comment
SaaS 类产品开发接口接入到华为云云商店
in Tutorial with 0 comment

背景

我们有个需求,需要将公司的 SaaS 产品发布到华为云商店上,要对接和开发相应的接口,官方文档如下:here

但是我们的开发语言是 Golang,官方只提供了 Java 示例代码,需要我们仿照 Java 示例去写 Golang 版本,所以才有这篇文章。

需求拆解

Java 示例太长了,我们直接看 main 函数部分

    public static void main(String args[]) throws Exception {

        // ------------服务商验证请求---------------
        // 将请求转换为map,模拟从request中获取参数操作(request.getParameterMap())
        Map<String, String[]> paramsMap = getTestUrlMap();

        // 加密类型 256加密(AES256_CBC_PKCS5Padding),128加密(AES128_CBC_PKCS5Padding)
        System.out.println("服务商验证请求:" + verificateRequestParams(paramsMap, ACCESS_KEY, 256));

        // 需要加密的手机、密码等
        String needEncryptStr = "15905222222";
        String encryptStr = generateSaaSUsernameOrPwd(needEncryptStr, ACCESS_KEY, ENCRYPT_TYPE_256);
        System.out.println("加密的手机、密码等:" + encryptStr);

        // 解密
        String decryptStr = decryptMobilePhoneOrEMail(ACCESS_KEY, encryptStr, ENCRYPT_TYPE_256);
        System.out.println("解密的手机、密码等:" + decryptStr);

        // body签名
        String needEncryptBody =
        "{\"resultCode\":\"00000\",\"resultMsg\":\"购买成功\",\"encryptType\":\"1\",\"instanceId\":\"000bd4e1-5726-4ce9-8fe4-fd081a179304\",\"appInfo\"{\"userName\":\"3LQvu8363e5O4zqwYnXyJGWz8y+GAcu0rpM0wQ==\",\"password\":\"RY31aEnR5GMCFmt3iG1hW7UF1HK09MuAL2sgxA==\"}}";

        String encryptBody = generateResponseBodySignature(ACCESS_KEY, needEncryptBody);
        System.out.println("body签名:" + encryptBody);
    }

看代码我们可知,需要做的工作有:

我们如果再细看验证响应和签名body,基本上都是调用同一个底层函数,所以进一步需要做的工作有:

验证和签名

业务代码:

package utils

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/base64"
    "sort"
    "strings"
)

// 基于 HMAC-SHA256 算法
// 用于 authToken 校验,如 HmacSha256(Key+timeStamp, p1=1&p3=3&p2=2&timeStamp=201706211855321)
// 用于 http body 包体签名,如 HmacSha256(key, httpBody)
func HmacSha256(key string, data string) string {
    h := hmac.New(sha256.New, []byte(key))
    h.Write([]byte(data))
    return base64.StdEncoding.EncodeToString(h.Sum(nil))
}

// map 按照 key 进行自然排序后转换为 query 字符串
func MapToQueryString(m map[string][]string) string {
    // 对key进行自然排序
    sortKeys := make([]string, 0)
    for k := range m {
        sortKeys = append(sortKeys, k)
    }
    sort.Strings(sortKeys)

    // 根据排序结果,拼接为 p=v&a=c 的 url query 字符串结构
    var queryParts []string
    for _, v := range sortKeys {
        queryParts = append(queryParts, v+"="+m[v][0])
    }
    return strings.Join(queryParts, "&")
}

测试代码:

package test

import (
    "testing"

    "saas/pkg/utils"
)

type hmacTestInfo struct {
    str             string // url query string, 需要去掉authToken
    authToken       string // url 传过来的 authToken
    timeStamp       string // url 传过来的 timeStamp
    expectSignature string // 预期的 body sign
}

func TestHmac(t *testing.T) {
    accessKey := "13c4bc57-3192-4b72-98b5-0866e797ef1f"
    tests := []*hmacTestInfo{
        {
            str:       "activity=newInstance&businessId=c9d9b417-6fbc-4c3d-99a9-14f4935d41a3&customerId=68cbc86abc2018ab880d92f36422fa0e&email=e53uj88T9t3hHr4hqIk6soJkaZSA2AvxzGuRRg==&expireTime=20210427142237&orderAmount=100&orderId=CS1906666666ABCDE&periodNumber=1&periodType=month&productId=00301-666666-0--0&provisionType=1&testFlag=1&timeStamp=20210427065424963&userName=admin",
            authToken: "45QHXRL3O5UTAEH71zhAOSpgVSI+yI4kFLz6P2I0kzw=",
            timeStamp: "20210427065424963",
        },
        {
            str:       "activity=newInstance&businessId=c9d9b417-6fbc-4c3d-99a9-14f4935d41a3&customerId=68cbc86abc2018ab880d92f36422fa0e&email=6f7NbC5Q3c635NonrAoyMDY7P8DmS1PihsmcFg==&expireTime=20210427142237&orderAmount=100&orderId=CS1906666666ABCDE&periodNumber=1&periodType=month&productId=00301-666666-0--0&provisionType=1&testFlag=1&timeStamp=20210427062302033&userName=admin",
            authToken: "wjM4YiEM3OSs3mYrcR4/ezoay6Qs2wAEchK8WwvI5YE=",
            timeStamp: "20210427062302033",
        },
    }

    for index, test := range tests {
        geneToken := utils.HmacSha256(accessKey+test.timeStamp, test.str)
        t.Logf("index %v authToken: %v", index, test.authToken)
        t.Logf("index %v geneToken: %v", index, geneToken)
        if test.authToken != geneToken {
            t.Error(index, " authToken fail")
        }
    }

    signTests := []*hmacTestInfo{
        {
            str:             "{\"resultCode\":\"000000\",\"resultMsg\":\"success\",\"instanceId\":\"b6357e85-e230-4710-8b23-3394a2211d10\",\"encryptType\":\"2\",\"appInfo\":{\"frontEndUrl\":\"https://www.linpx.com\",\"adminUrl\":\"https://www.linpx.com\",\"userName\":\"ONRhfKsUOHoF8iVdtHC4HfFuLN5AN5gnwGUX8fuXj4CecIYIQZPY8zr8ZIU=\",\"password\":\"iJs5huhtgVW8Q5MT4GWCClpsPYHGUNqhdGsXXQ==\"}}",
            expectSignature: "MijLbcDkO1iAwsN07/b2zvwIQ80JNdNTMkcyozBkedY=",
        },
    }

    for index, test := range signTests {
        httpRspSignature := utils.HmacSha256(accessKey, test.str)
        t.Logf("index %v httpRspSignature: %v", index, httpRspSignature)
        t.Logf("index %v expectSignature: %v", index, test.expectSignature)
        if test.expectSignature != httpRspSignature {
            t.Error(index, " body sign fail")
        }
    }
}

字符串加解密

业务代码:

package utils

import (
    "bytes"
    "crypto/aes"
    "crypto/cipher"
    "crypto/sha1"
    "encoding/base64"
    "math/rand"
)

const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"

// 获取随机字符串
func getRandomChars(length int) string {
    bytes := make([]byte, length)
    for i := range bytes {
        bytes[i] = letterBytes[rand.Int63()%int64(len(letterBytes))]
    }
    return string(bytes)
}

// key 签名算法: AES_SHA1PRNG
// body 对称算法,现在用 AES_CBC_128_pkcs5_base64

// 只支持 128
func AesEncryptCBC(origData string, key string) string {
    iv := []byte(getRandomChars(16))                // 获取初始化向量
    newKey := AesSha1prng(key)                      // key 加密
    origDataBypes := []byte(origData)               // 转byte
    origDataBypes = pkcs5Padding(origDataBypes, 16) // 补全码

    block, _ := aes.NewCipher(newKey)               // 分组秘钥
    blockMode := cipher.NewCBCEncrypter(block, iv)  // 加密模式
    encrypted := make([]byte, len(origDataBypes))   // 创建数组
    blockMode.CryptBlocks(encrypted, origDataBypes) // 加密

    b64Str := base64.StdEncoding.EncodeToString(encrypted) // byte转base64
    return string(iv) + b64Str                             // 拼接后返回
}

// 只支持 128
func AesDecryptCBC(encrData string, key string) string {
    iv := []byte(encrData[:16])                             // 获取初始化向量
    newKey := AesSha1prng(key)                              // key 加密
    b64Str := encrData[16:]                                 // 获取加密部分
    encrypted, _ := base64.StdEncoding.DecodeString(b64Str) // base64转byte

    block, _ := aes.NewCipher(newKey)              // 分组秘钥
    blockMode := cipher.NewCBCDecrypter(block, iv) // 加密模式
    decrypted := make([]byte, len(encrypted))      // 创建数组
    blockMode.CryptBlocks(decrypted, encrypted)    // 解密
    decrypted = pkcs5UnPadding(decrypted)          // 去除补全码

    return string(decrypted)
}

// 添 pkcs5 补全码算法
func pkcs5Padding(ciphertext []byte, blockSize int) []byte {
    padding := blockSize - len(ciphertext)%blockSize
    padtext := bytes.Repeat([]byte{byte(padding)}, padding)
    return append(ciphertext, padtext...)
}

// 去 pkcs5 补全码算法
func pkcs5UnPadding(origData []byte) []byte {
    length := len(origData)
    unpadding := int(origData[length-1])
    return origData[:(length - unpadding)]
}

// 模拟 java AES SHA1PRNG 处理
func AesSha1prng(key string) []byte {
    data := []byte(key)

    hashs := Sha1(Sha1(data))
    keybytes := hashs[0:16]

    return keybytes
}
func Sha1(data []byte) []byte {
    h := sha1.New()
    h.Write(data)
    return h.Sum(nil)
}

测试代码:

package test

import (
    "testing"

    "saas/pkg/utils"
)

type aecTestInfo struct {
    origData string //原文
    encrData string //密文
}

func TestAesCBC(t *testing.T) {
    accessKey := "13c4bc57-3192-4b72-98b5-0866e797ef1f"

    // 测试来自 url email 的解密
    test128 := []*aecTestInfo{
        {
            origData: "admin@t.com",
            encrData: "6f7NbC5Q3c635NonrAoyMDY7P8DmS1PihsmcFg==",
        },
    }

    for index, test := range test128 {
        decryStr := utils.AesDecryptCBC(test.encrData, accessKey)
        t.Logf("index %v decryStr %v", index, decryStr)
        t.Logf("index %v origData %v", index, test.origData)
        if decryStr != test.origData {
            t.Error(index, "AesDecryptCBC fail")
        }
    }

    // 测试加密和解密
    testAll := []*aecTestInfo{
        {
            origData: "123456",
        },
        {
            origData: "sadfasdf87wer4242fsdf/342*34c5s4f5sf4+0/*fsaf12309",
        },
    }

    for index, test := range testAll {
        encrSrt := utils.AesEncryptCBC(test.origData, accessKey)
        decryStr := utils.AesDecryptCBC(encrSrt, accessKey)
        t.Logf("index %v decryStr %v", index, decryStr)
        t.Logf("index %v origData %v", index, test.origData)
        if decryStr != test.origData {
            t.Error(index, "AesDecryptCBC fail")
        }
    }
}

总结

历时3天,网上也搜了很多相关的代码,基本上都不能直接用起来,需要做大量深入了解,做测试和验证,最终呈现出这个是能跑通,但也有缺点,例如只支持aec128,不支持 aec256,但也足够用了。


👊 收工~

Responses