go项目接入华为严选模板代码

背景

公司为了扩展销售渠道,将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
}

如果有帮到你,烦请点个👍🏻喽!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值