背景
公司为了扩展销售渠道,将saas产品上架到华为严选。作为服务提供商来说需要开发对应的回调接口提供给华为,华为根据新购,续费等事件来回调服务提供商
接入前准备
参见官方文档:https://support.huaweicloud.com/accessg-marketplace/zh-cn_topic_0070649060.html
模板代码
- 语言:golang
- web框架: https://github.com/gotomicro/ego
该框架基于斗鱼的微服务框架jupiter演进而来,如何使用请移步官方文档:
https://ego.gocn.vip/
代码展示
路由层
func ServerHttp() *egin.Component {
// 基于Gin框架封装
r := invoker.Gin
apiV1 := r.Group("/api/v1")
huawei(apiV1)
return invoker.Gin
}
// 华为相关api
func huawei(r *gin.RouterGroup) {
// 华为严选相关
huaweiYX(r)
}
func huaweiYX(r *gin.RouterGroup) {
// 华为严选-sass-回调
// ValidAuthToken 校验华为请求的token中间件
hwyxGroup := r.Group("/huawei/yx", core.Handle(middleware.ValidAuthToken))
hwyxGroup.GET("/sass/callback", core.Handle(v1.HuaweiYXCallback))
}
请求token中间件
// ValidAuthToken 验证华为的 authToken
func ValidAuthToken(c *core.Context) {
var (
request = c.Request
err = request.ParseForm()
resp = new(view.HuaweiYXResponse)
)
if err != nil {
elog.Error("ValidAuthToken: request.ParseForm failed", zap.Error(err))
resp.BuildFailedRes(constx.ResultCodeAuthenticationFailed, fmt.Sprintf("request.ParseForm failed:%s", err.Error()))
// 响应华为的信息要进行签名处理,后续会给出模板代码
api.HuaweiJSON(c, resp, api.HuaweiSignBodyStr(resp))
c.Abort()
return
}
params := request.Form
elog.Info("ValidAuthToken: request info", zap.Any("params", params))
keys := make([]string, 0)
newParamsStr := ""
timeStamp := ""
authToken := ""
// 华为提供的 auth key
key := config.GetHuaweiAuthKey()
for k, v := range params {
if k == "authToken" {
authToken = v[0]
continue
}
if k == "timeStamp" {
timeStamp = v[0]
}
keys = append(keys, k)
}
sort.Sort(sort.StringSlice(keys))
for _, v := range keys {
newParamsStr += v + "=" + params[v][0] + "&"
}
newParamsStr = strings.Trim(newParamsStr, "&")
keyTimeStamp := []byte(key + timeStamp)
newParamsByte := []byte(newParamsStr)
mac := hmac.New(sha256.New, keyTimeStamp)
mac.Write(newParamsByte)
expectedMAC := mac.Sum(nil)
sEnc := base64.StdEncoding.EncodeToString(expectedMAC)
if sEnc == authToken {
// 验证通过
c.Next()
return
}
// 未验证通过
elog.Error("ValidAuthToken:failed", zap.String("sEnc", sEnc), zap.String("authToken", authToken))
resp.BuildFailedRes(constx.ResultCodeAuthenticationFailed, "authentication failed")
api.HuaweiJSON(c, resp, api.HuaweiSignBodyStr(resp))
c.Abort()
}
响应信息封装
// HuaweiJSON 提供了系统标准JSON输出方法(huawei需要对响应的相关字段进行加密)。
func HuaweiJSON(c *core.Context, resp interface{}, jsonByte []byte) {
jsonStr := string(jsonByte)
setBodySign(c, jsonStr)
c.Context.JSON(http.StatusOK, resp)
}
func HuaweiSignBodyStr(resp interface{}) (jsonResStr []byte) {
jsonResByte, err := json.Marshal(resp)
if err != nil {
elog.Error("HuaweiSignBodyStr: json.Marshal(resp) failed", zap.Error(err), zap.Any("resp", resp))
}
return jsonResByte
}
func setBodySign(c *core.Context, jsonResStr string) {
key := config.GetHuaweiAuthKey()
mac2 := hmac.New(sha256.New, []byte(key))
mac2.Write([]byte(jsonResStr))
expectedMAC := mac2.Sum(nil)
sEnc := base64.StdEncoding.EncodeToString(expectedMAC)
a := `sign_type="HMAC-SHA256", signature= "%s"`
s := fmt.Sprintf(a, sEnc)
c.Writer.Header().Set("Body-Sign", s)
}
业务逻辑处理
func HuaweiYXCallback(c *core.Context) {
activity := constx.Activity(c.Query("activity"))
resp := new(view.HuaweiYXResponse)
switch activity {
// 新购商品
case constx.AddGoods:
elog.Info("HuaweiYXCallback: receive AddGoods callback")
resp = addGoods(c)
// 商品续费
case constx.GoodsRenewal:
elog.Info("HuaweiYXCallback: receive GoodsRenewal callback")
resp = goodsRenewal(c)
// 商品过期
case constx.GoodsExpire:
elog.Info("HuaweiYXCallback: receive GoodsExpire callback")
resp = goodsExpire(c)
case constx.GoodsResourceRelease:
elog.Info("HuaweiYXCallback: receive GoodsResourceRelease callback")
// 资源释放
resp = goodsResourceRelease(c)
case constx.GoodsUpgrade:
elog.Info("HuaweiYXCallback: receive GoodsUpgrade callback")
// 升级
resp = goodsUpgrade(c)
default:
elog.Warn("HuaweiYXCallback: invalid activity", zap.Any("activity", activity))
resp.BuildFailedRes(constx.ResultCodeInvalidParam, fmt.Sprintf("invalid activity:%s", activity))
}
api.HuaweiJSON(c, resp, api.HuaweiSignBodyStr(resp))
}
解密华为传递的敏感信息(手机和邮箱)
// decrypt 解密电话号码和邮箱
func decryptMobileAndEmail(params *view.AddGoodsParam) {
var (
mobilePhone = strings.TrimSpace(params.MobilePhone)
email = strings.TrimSpace(params.Email)
)
// 解密电话号码
if mobilePhone == "" {
elog.Warn("decryptMobileAndEmail mobile is empty")
} else {
// GetHuaweiAuthKey 华为提供的authKey
decryptMobilePhone, err := service.EncryptInstance().Decrypt(mobilePhone, config.GetHuaweiAuthKey())
if err != nil {
elog.Error("decryptMobileAndEmail: Decrypt mobile failed", zap.String("mobilePhone", mobilePhone),
zap.String("orderId", params.OrderId),
zap.String("bid", params.BusinessId))
} else {
mobilePhone = decryptMobilePhone
}
}
// 解密邮箱
if email == "" {
elog.Warn("decryptMobileAndEmail: email is empty")
} else {
decryptEmail, err := service.EncryptInstance().Decrypt(email, config.GetHuaweiAuthKey())
if err != nil {
elog.Error("decryptMobileAndEmail: Decrypt email failed", zap.String("email", email),
zap.String("orderId", params.OrderId),
zap.String("bid", params.BusinessId))
} else {
email = decryptEmail
}
}
params.Email = email
params.MobilePhone = mobilePhone
}
EncryptInstance代码
// Decrypt 解密
// text: 待解密的字符串
// goodsKey: 华为提供的authKey
func (h *huaweiEncrypt) Decrypt(text, goodsKey string) (content string, err error) {
if len(text) < 16 {
content = text
err = errors.New("解密字符串长度不合法")
elog.Warn("huaweiEncrypt-Decrypt: the len of text is over size", zap.String("text", text))
return
}
huaweiKey := goodsKey
iv := text[0:16]
encryptStr := text[16:]
// 注意此处的参数128 要与华为参数加密算法保持一致,否则解密会失败
key, err := AESSHA1PRNG(huaweiKey, 128)
if err != nil {
elog.Error("huaweiEncrypt-Decrypt: AESSHA1PRNG failed", zap.Error(err))
return
}
decodeData, err := base64.StdEncoding.DecodeString(encryptStr)
if err != nil {
elog.Error("huaweiEncrypt-Decrypt: base64.StdEncoding.DecodeString failed", zap.Error(err), zap.String("encryptStr", encryptStr))
return
}
// 生成密码数据块cipher.Block
block, _ := aes.NewCipher([]byte(key))
// 解密模式
blockMode := cipher.NewCBCDecrypter(block, []byte(iv))
// 输出到[]byte数组
originData := make([]byte, len(decodeData))
blockMode.CryptBlocks(originData, decodeData)
// 去除填充,并返回
return string(UnPadding(originData)), nil
}
// Encrypt 解密 华为要求我们返回的账号,密码得如此加密才认为我们正确 aes-cbc-128(用户购买saas产品后,可能需要给用户返回应用开通的账号和密码,此时可以拿该方法对相关参数进行加密传递)
func (h *huaweiEncrypt) Encrypt(content string, encryptLength int, goodsKey string) (encodeStr string, err error) {
if content == "" {
encodeStr = content
return
}
var key string
text := []byte(content)
huaweiKey := goodsKey
key, err = AESSHA1PRNG(huaweiKey, encryptLength)
iv := GetIv()
block, err := aes.NewCipher([]byte(key))
if err != nil {
elog.Error("huaweiEncrypt-Encrypt: aes.NewCipher failed", zap.Error(err))
return
}
// 填充内容,如果不足16位字符
blockSize := block.BlockSize()
originData := Pad(text, blockSize)
// 加密方式
blockMode := cipher.NewCBCEncrypter(block, iv)
// 加密,输出到[]byte数组
crypted := make([]byte, len(originData))
blockMode.CryptBlocks(crypted, originData)
encodeStr = string(iv) + base64.StdEncoding.EncodeToString(crypted)
return
}
// AESSHA1PRNG
// 模拟 Java 接口 generatorKey()
// 目前 encryptLength 仅支持 128bit 长度
// 因为 SHA1() 返回的长度固定为 20byte 160bit
// 所以 encryptLength 超过这个长度,就无法生成了
// 因为不知道 java 中 AES SHA1PRNG 的生成逻辑
// https://support.huaweicloud.com/accessg-marketplace/zh-cn_topic_0070649063.html
// 对接华为云的key加密有个随机值过程,采用此方法随机
func AESSHA1PRNG(content string, encryptLength int) (encryptStr string, err error) {
keyBytes := []byte(content)
hashs := SHA1(SHA1(keyBytes))
maxLen := len(hashs)
realLen := encryptLength / 8
if realLen > maxLen {
err = errors.New("realLen 不能比 maxLen 还要大")
return
}
encryptStr = string(hashs[0:realLen])
return
}
func SHA1(data []byte) []byte {
h := sha1.New()
h.Write(data)
return h.Sum(nil)
}
// UnPadding ...
func UnPadding(ciphertext []byte) []byte {
length := len(ciphertext)
// 去掉最后一次的padding
unPadding := int(ciphertext[length-1])
return ciphertext[:(length - unPadding)]
}
// Pad 加密内容长度不够自动补全
func Pad(ciphertext []byte, blockSize int) []byte {
padding := blockSize - len(ciphertext)%blockSize
padText := bytes.Repeat([]byte{byte(padding)}, padding)
return append(ciphertext, padText...)
}
// GetIv 获取iv随机变量
func GetIv() []byte {
str := "0123456789abcdefghijklmnopqrstuvwxyz"
bytes := []byte(str)
var result []byte
r := rand.New(rand.NewSource(time.Now().UnixNano()))
for i := 0; i < 16; i++ {
result = append(result, bytes[r.Intn(len(bytes))])
}
return result
}
如果有帮到你,烦请点个👍🏻喽!