本文我将分享给大家关于微信小游戏推送的一些接入开发经验,首先微信小游戏的消息推送是他们开放平台推出的 一种主动推送服务,基于该推送服务,开发者及时获取开放平台相关信息,无需调用API。 这里我给大家分享下开发者服务器接收消息推送的经验。
1. 如何接入接入消息推送功能(go语言代码实现)
2. 使用安全模式接受微信消息推送并返回消息(go语言代码实现)
这里我就以小游戏礼包的这种推送方式来举例,其他的消息推送大同小异,你只需要修改包体的格式即可。 有任何疑问都可以在文章下方留言交流。
小游戏礼包
功能介绍
小游戏开发者的游戏内道具,可通过接入平台「礼包系统」,在微信平台各场景发放游戏礼包。 发放场景包括:游戏圈、活动运营工具、发现-游戏-福利中心、广告、搜索等
道具礼包发货基于微信小游戏统一的消息推送能力,因此在使用礼包中心的发货回调时,需要开发者服务器处理好消息推送的协议。 注意:MP配置消息回调地址只有1个,所有消息推送场景都会使用该通道,所以需要先根据MsgType与Event来判断消息类型,进行相应的处理。
下面是发送和返回的格式:
更多详情可以查看文档: 小游戏礼包 | 微信开放文档
开发者服务器接收消息推送
接下来我们着重讲下小游戏安全模式下的数据链路:
消息推送服务器配置
消息推送服务于小程序、公众号、小游戏、视频号小店、第三方平台,这里介绍小程序平台的配置
填写相关信息
登陆小程序管理后台,在「开发」-「开发管理」-「消息推送配置」中,需填写以下信息:
URL服务器地址:开发者用来接收微信消息和事件的接口 URL,必须以 http:// 或 https:// 开头,分别支持 80 端口和 443 端口。Token令牌:用于签名处理,下文会介绍相关流程。EncodingAESKey:将用作消息体加解密密钥。消息加解密方式:
明文模式:不使用消息加解密,明文发送,安全系数较低,不建议使用。兼容模式:明文、密文共存,不建议使用。安全模式:使用消息加解密,纯密文,安全系数高,强烈推荐使用。数据格式:消息体的格式,可选XML或JSON。
发起验证的工具大家可以使用 微信调试工具
验证逻辑go代码实现
其中,signature签名的生成方式是:
将Token、timestamp、nonce三个参数进行字典序排序。将三个参数字符串拼接成一个字符串进行sha1计算签名,即可获得signature。 开发者需要校验signature是否正确,以判断请求是否来自微信服务器,验签通过后,请原样返回echostr字符串。
验证接口的核心逻辑如下:
func (c *ThirdPartyServiceController) WechatVerify() {
keys := make([]string, 0)
keys = append(keys, c.GetString("timestamp"))
keys = append(keys, c.GetString("nonce"))
keys = append(keys, beego.AppConfig.String("miniwechat_token")) //配置文件提前配置好小游戏的token,从小游戏后台可以获取
sort.Strings(keys)
signstr := keys[0] + keys[1] + keys[2]
mysign := Sha1Signature(signstr)
sign := c.GetString("signature")
if sign == mysign {
c.Ctx.WriteString(c.GetString("echostr"))
return
}
c.Ctx.WriteString(c.GetString("echostr"))
}
签名函数实现如下:
func Sha1Signature(text string) string {
hash := sha1.New()
hash.Write([]byte(text))
return fmt.Sprintf("%x", hash.Sum(nil))
}
接收消息推送
当特定消息或事件触发时,比如我们的小游戏礼包,当玩家领取礼包的时候,微信服务器会将消息(或事件)的数据包以 POST 请求发送到开发者配置的 URL,下面以“debug_demo”事件为例,详细介绍整个过程(这里主要讲安全模式的相关加解密过程,很多都可以直接在官网上看,只不过官网只提供了了C++,java,php,python,C#的代码示例,我这里提供go的代码示例):
消息解密方式为安全模式
假设URL配置为https://www.qq.com/revice, 数据格式为JSON,Token="AAAAA",EncodingAESKey="AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",小程序Appid="wxba5fad812f8e6fb9"。推送的URL链接::https://www.qq.com/recive?signature=6c5c811b55cc85e0e1b54100749188c20beb3f5d×tamp=1714112445&nonce=415670741&openid=o9AgO5Kd5ggOC-bXrbNODIiE3bGY&encrypt_type=aes&msg_signature=046e02f8204d34f8ba5fa3b1db94908f3df2e9b3推送的包体:
{
"ToUserName": "gh_97417a04a28d",
"Encrypt": "+qdx1OKCy+5JPCBFWw70tm0fJGb2Jmeia4FCB7kao+/Q5c/ohsOzQHi8khUOb05JCpj0JB4RvQMkUyus8TPxLKJGQqcvZqzDpVzazhZv6JsXUnnR8XGT740XgXZUXQ7vJVnAG+tE8NUd4yFyjPy7GgiaviNrlCTj+l5kdfMuFUPpRSrfMZuMcp3Fn2Pede2IuQrKEYwKSqFIZoNqJ4M8EajAsjLY2km32IIjdf8YL/P50F7mStwntrA2cPDrM1kb6mOcfBgRtWygb3VIYnSeOBrebufAlr7F9mFUPAJGj04="
}
校验msg_signature签名是否正确,以判断请求是否来自微信服务器。注意:不要使用signature验证!
将token、timestamp(URL参数中的)、nonce(URL参数中的)、Encrypt(包体内的字段)四个参数进行字典序排序,排序后结果为: ["+qdx1OKCy+5JPCBFWw70tm0fJGb2Jmeia4FCB7kao+/Q5c/ohsOzQHi8khUOb05JCpj0JB4RvQMkUyus8TPxLKJGQqcvZqzDpVzazhZv6JsXUnnR8XGT740XgXZUXQ7vJVnAG+tE8NUd4yFyjPy7GgiaviNrlCTj+l5kdfMuFUPpRSrfMZuMcp3Fn2Pede2IuQrKEYwKSqFIZoNqJ4M8EajAsjLY2km32IIjdf8YL/P50F7mStwntrA2cPDrM1kb6mOcfBgRtWygb3VIYnSeOBrebufAlr7F9mFUPAJGj04=", "1714112445", "415670741", "AAAAA"]。将四个参数字符串拼接成一个字符串,然后进行sha1计算签名:046e02f8204d34f8ba5fa3b1db94908f3df2e9b3与URL参数中的msg_signature参数进行对比,相等说明请求来自微信服务器,合法。解密消息体"Encrypt"密文。
AESKey = Base64_Decode( EncodingAESKey + "=" ),EncodingAESKey 尾部填充一个字符的 "=", 用 Base64_Decode 生成 32 个字节的 AESKey;将Encrypt密文进行Base64解码,得到TmpMsg, 字节长度为224将TmpMsg使用AESKey进行AES解密,得到FullStr,字节长度为205。AES 采用 CBC 模式,秘钥长度为 32 个字节(256 位),数据采用 PKCS#7 填充; PKCS#7:K 为秘钥字节数(采用 32),Buf 为待加密的内容,N 为其字节数。Buf 需要被填充为 K 的整数倍。在 Buf 的尾部填充(K - N%K)个字节,每个字节的内容 是(K - N%K)。微信团队提供了多种语言的示例代码(包括 PHP、Java、C++、Python、C#),请开发者尽量使用示例代码,仔细阅读技术文档、示例代码及其注释后,再进行编码调试。示例下载FullStr=random(16B) + msg_len(4B) + msg + appid,其中:
random(16B)为 16 字节的随机字符串;msg_len 为 msg 长度,占 4 个字节(网络字节序);msg为解密后的明文;appid为小程序Appid,开发者需验证此Appid是否与自身小程序相符。在此示例中:
random(16B)="a8eedb185eb2fecf"msg_len=167(注意:需按网络字节序,占4个字节)msg="{"ToUserName":"gh_97417a04a28d","FromUserName":"o9AgO5Kd5ggOC-bXrbNODIiE3bGY","CreateTime":1714112445,"MsgType":"event","Event":"debug_demo","debug_str":"hello world"}"appid="wxba5fad812f8e6fb9"回包给微信服务器,首先需确定回包包体的明文内容,具体取决于特定接口文档要求,如无特定要求,回复空串或者success(无需加密)即可,其他回包内容需加密处理。这里假设回包包体的明文内容为"{"demo_resp":"good luck"}",数据格式为JSON,下面介绍如何对回包进行加密:回包格式如下,具体取决于你配置的数据格式(JSON或XML),其中:
Encrypt:加密后的内容;MsgSignature:签名,微信服务器会验证签名;TimeStamp:时间戳;Nonce:随机数{
"Encrypt": "${msg_encrypt}$",
"MsgSignature": "${msg_signature}$",
"TimeStamp": ${timestamp}$,
"Nonce": ${nonce}$
}
Encrypt的生成方法:
AESKey = Base64_Decode( EncodingAESKey + "=" ),EncodingAESKey 尾部填充一个字符的 "=", 用 Base64_Decode 生成 32 个字节的 AESKey;构造FullStr=random(16B) + msg_len(4B) + msg + appid,其中
random(16B)为 16 字节的随机字符串;msg_len 为 msg 长度,占 4 个字节(网络字节序);msg为明文;appid为小程序Appid。在此示例中:
random(16B)="707722b803182950"msg_len=25(注意:需按网络字节序,占4个字节)msg="{"demo_resp":"good luck"}"appid="wxba5fad812f8e6fb9"FullStr的字节大小为63将FullStr用AESKey进行加密,得到TmpMsg,字节大小为64。AES 采用 CBC 模式,秘钥长度为 32 个字节(256 位),数据采用 PKCS#7 填充; PKCS#7:K 为秘钥字节数(采用 32),Buf 为待加密的内容,N 为其字节数。Buf 需要被填充为 K 的整数倍。在 Buf 的尾部填充(K - N%K)个字节,每个字节的内容 是(K - N%K)。微信团队提供了多种语言的示例代码(包括 PHP、Java、C++、Python、C#),请开发者尽量使用示例代码,仔细阅读技术文档、示例代码及其注释后,再进行编码调试。示例下载对TmpMsg进行Base64编码,得到Encrypt="ELGduP2YcVatjqIS+eZbp80MNLoAUWvzzyJxgGzxZO/5sAvd070Bs6qrLARC9nVHm48Y4hyRbtzve1L32tmxSQ=="。TimeStamp由开发者生成,使用当前时间戳即可,示例使用1713424427。Nonce回填URL参数中的nonce参数即可,示例使用415670741。MsgSignature的生成方法:
将token、TimeStamp(回包中的)、Nonce(回包中的)、Encrypt(回包中的)四个参数进行字典序排序,排序后结果为: ["1713424427", "415670741", "AAAAA", "ELGduP2YcVatjqIS+eZbp80MNLoAUWvzzyJxgGzxZO/5sAvd070Bs6qrLARC9nVHm48Y4hyRbtzve1L32tmxSQ=="]将四个参数字符串拼接成一个字符串,并进行sha1计算签名:1b9339964ed2e271e7c7b6ff2b0ef902fc94dea1最终回包为:
{
"Encrypt": "ELGduP2YcVatjqIS+eZbp80MNLoAUWvzzyJxgGzxZO/5sAvd070Bs6qrLARC9nVHm48Y4hyRbtzve1L32tmxSQ==",
"MsgSignature": "1b9339964ed2e271e7c7b6ff2b0ef902fc94dea1",
"TimeStamp": 1713424427,
"Nonce": "415670741"
}
消息推送验证工具,大家可以使用微信调试工具
接收微信消息推送的go代码实现
定义接收和回报给微信推送消息结构:
type WechatMiniGamePushReq struct {
ToUserName string `json:"ToUserName"`
Encrypt string `json:"Encrypt"`
}
type WechatMiniGamePushResp struct {
Encrypt string `json:"Encrypt"`
MsgSignature string `json:"MsgSignature"`
TimeStamp int32 `json:"TimeStamp"`
Nonce string `json:"Nonce"`
}
定义礼包消息的结构
//接收微信小游戏礼包的结构
type WechatMiniGameGiftPushMsg struct {
CreateTime int32 `json:"CreateTime"`
Event string `json:"Event"`
MiniGame struct {
GiftID string `json:"GiftId"`
GiftTypeID int32 `json:"GiftTypeId"`
GoodsList []struct {
ID string `json:"Id"`
Num int `json:"Num"`
} `json:"GoodsList"`
IsPreview int32 `json:"IsPreview"`
OrderID string `json:"OrderId"`
SendTime int32 `json:"SendTime"`
ToUserOpenid string `json:"ToUserOpenid"`
Zone int32 `json:"Zone"`
} `json:"MiniGame"`
MsgType string `json:"MsgType"`
}
//返回给微信小游戏礼包的结构
type WechatMiniGameGift struct {
ErrCode int32 `json:"ErrCode"` //是 发送状态。0:成功,其他:失败
ErrMsg string `json:"ErrMsg"` //错误原因,用于调试。在errcode非0 的情况下可以返回
SubErrCode int32 `json:"SubErrCode"` // 指定的失败场景返回指定值,对应平台不同的重试、熔断策略 172935494 未注册用户,无法发货
}
func (c *ThirdPartyServiceController) parseBody(req interface{}) bool {
reqBody := c.Ctx.Input.RequestBody
err := json.Unmarshal(reqBody, &req)
if err != nil {
beego.Warning("resp to client: %+v", err, string(reqBody))
return false
}
return true
}
func (c *ThirdPartyServiceController) WechatPush() {
var req WechatMiniGamePushReq
if !c.parseBody(&req) {
c.Redirect("/", 404)
return
}
//将token、timestamp(URL参数中的)、nonce(URL参数中的)、Encrypt(发包中的)四个参数进行字典序排序
keys := make([]string, 0)
keys = append(keys, beego.AppConfig.String("miniwechat_token")) //从配置里拿到token
keys = append(keys, c.GetString("timestamp"))
keys = append(keys, c.GetString("nonce"))
keys = append(keys, req.Encrypt)
sort.Strings(keys)
signstr := keys[0] + keys[1] + keys[2] + keys[3]
mysign := Sha1Signature(signstr)
msg_signature := c.GetString("msg_signature")
if msg_signature != mysign {
c.Redirect("/", 404)
return
}
encodingaeskey := beego.AppConfig.String("miniwechat_encodingaeskey") //配置的加密aeskey
aeskey, err := base64.StdEncoding.DecodeString(encodingaeskey + "=")
if err != nil {
beego.Warning("decode aeskey failed", encodingaeskey, err)
c.Redirect("/", 404)
return
}
TmpMsg, err := base64.StdEncoding.DecodeString(req.Encrypt)
if err != nil {
beego.Warning("decode aeskey failed", encodingaeskey, err)
c.Redirect("/", 404)
return
}
decryptedText := AesDecryptCBC(TmpMsg, aeskey)
if decryptedText == nil {
beego.Warning("解密失败:", err)
c.Redirect("/", 404)
return
}
randomBits := decryptedText[:16]
// 解包前4个字节作为包长
packageLength := binary.BigEndian.Uint32(decryptedText[16:20])
// 如果需要剩下的字节数据
msgBytes := decryptedText[20 : 20+packageLength]
appid := string(decryptedText[20+packageLength:])
if appid != beego.AppConfig.String("miniwechat_appid") { //验证下必须是当前游戏的appid,防止被漏洞利用
beego.Critical("appid参数不对:", err)
c.Redirect("/", 404)
return
}
pushObj := WechatMiniGameGiftPushMsg{}
err = json.Unmarshal(msgBytes, &pushObj)
if err != nil {
c.Redirect("/", 404)
return
}
beego.Warning("wechat recieve:", string(msgBytes))
//发货给玩家
giftresp := WechatMiniGameGift{
ErrCode: 0, //是 发送状态。0:成功,其他:失败
ErrMsg: "", //错误原因,用于调试。在errcode非0 的情况下可以返回
SubErrCode: 0, // 指定的失败场景返回指定值,对应平台不同的重试、熔断策略 172935494 未注册用户,无法发货
}
//用户自定义逻辑,你要根据玩家的openid找到对应的账号信息
ac := models.AccountRecord{}
err = models.GetUserByOpenid(pushObj.MiniGame.ToUserOpenid, &ac)
if err != nil {
giftresp.ErrCode = 1
giftresp.SubErrCode = 172935494
giftresp.ErrMsg = "未注册的用户"
respBytes := BuildMinigameWechatEncryptBytes(giftresp,
randomBits,
appid,
aeskey,
c.GetString("nonce"),
beego.AppConfig.String("xipu::miniwechat_token"))
c.Ctx.WriteString(string(respBytes))
beego.Informational(string(respBytes))
return
}
//给玩家发道具通知 TODO
respBytes := BuildMinigameWechatEncryptBytes(giftresp,
randomBits,
appid,
aeskey,
c.GetString("nonce"),
beego.AppConfig.String("xipu::miniwechat_token"))
c.Ctx.WriteString(string(respBytes))
beego.Informational(string(respBytes))
}
相关的加密解密逻辑:
func AesEncryptCBC(origData []byte, key []byte) (encrypted []byte) {
// 分组秘钥
// NewCipher该函数限制了输入k的长度必须为16, 24或者32
block, _ := aes.NewCipher(key)
blockSize := block.BlockSize() // 获取秘钥块的长度
origData = pkcs5Padding(origData, blockSize) // 补全码
blockMode := cipher.NewCBCEncrypter(block, key[:blockSize]) // 加密模式
encrypted = make([]byte, len(origData)) // 创建数组
blockMode.CryptBlocks(encrypted, origData) // 加密
return encrypted
}
func AesDecryptCBC(encrypted []byte, key []byte) (decrypted []byte) {
block, _ := aes.NewCipher(key) // 分组秘钥
blockSize := block.BlockSize() // 获取秘钥块的长度
blockMode := cipher.NewCBCDecrypter(block, key[:blockSize]) // 加密模式
decrypted = make([]byte, len(encrypted)) // 创建数组
blockMode.CryptBlocks(decrypted, encrypted) // 解密
decrypted = pkcs5UnPadding(decrypted) // 去除补全码
return decrypted
}
func pkcs5Padding(ciphertext []byte, blockSize int) []byte {
padding := blockSize - len(ciphertext)%blockSize
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
return append(ciphertext, padtext...)
}
func pkcs5UnPadding(origData []byte) []byte {
length := len(origData)
unpadding := int(origData[length-1])
return origData[:(length - unpadding)]
}
给微信返回包的加密逻辑:
func BuildMinigameWechatEncryptBytes(sendBody interface{},
randomBits []byte,
appid string,
aesKey []byte,
nonce string,
token string) []byte {
resp := WechatMiniGamePushResp{
TimeStamp: int32(time.Now().Unix()),
Nonce: nonce,
Encrypt: "",
MsgSignature: "",
}
bodyBytes, _ := json.Marshal(sendBody)
// 将消息长度转换为4字节整数
var msgLenBytes [4]byte
binary.BigEndian.PutUint32(msgLenBytes[:], uint32(len(bodyBytes)))
// 连接随机数、消息长度和消息
FullStr := append(randomBits, msgLenBytes[:]...)
FullStr = append(FullStr, bodyBytes...)
FullStr = append(FullStr, appid...)
tmpMsg := AesEncryptCBC([]byte(FullStr), aesKey)
resp.Encrypt = base64.StdEncoding.EncodeToString([]byte(tmpMsg))
signkeys := make([]string, 0)
signkeys = append(signkeys, token)
signkeys = append(signkeys, fmt.Sprint(resp.TimeStamp))
signkeys = append(signkeys, resp.Nonce)
signkeys = append(signkeys, resp.Encrypt)
sort.Strings(signkeys)
signstr := signkeys[0] + signkeys[1] + signkeys[2] + signkeys[3]
resp.MsgSignature = Sha1Signature(signstr)
respBytes, _ := json.Marshal(resp)
return respBytes
}