背景
我们有个需求,需要将公司的 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
我们如果再细看验证响应和签名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,但也足够用了。
👊 收工~
本文由 Chakhsu Lau 创作,采用 知识共享署名4.0 国际许可协议进行许可。
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名。