本文我将分享给大家关于微信小游戏推送的一些接入开发经验,首先微信小游戏的消息推送是他们开放平台推出的 一种主动推送服务,基于该推送服务,开发者及时获取开放平台相关信息,无需调用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}$

}

${timestamp}$

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

}