# 消息推送
消息推送是开放平台推出的一种主动推送服务，基于该推送服务，开发者及时获取开放平台相关信息，无需调用 API。
目前提供三种方式接入：
* 开发者服务器接收消息推送
* 云函数接收消息推送
* 微信云托管服务接收消息推送



## 开发者服务器接收消息推送
总数据链路如图所示：![](https://res.wx.qq.com/op_res/CyXI7cGX7jxeP-vdY2Hkm9s2cMZlsxeY23fkgw44pPOtTwMbZQq-iM-3Lv4054rfJtUFVOwyac0E9I3vFclKqw
)
### 消息推送服务器配置
消息推送服务于小程序、公众号、小游戏、视频号小店、第三方平台，这里介绍小程序平台的配置。

#### 填写相关信息
登录[小程序管理后台](https://mp.weixin.qq.com/)，在「开发」-「开发管理」-「消息推送配置」中，需填写以下信息：
1. URL 服务器地址：开发者用来接收微信消息和事件的接口 URL，必须以 http:// 或 https:// 开头，分别支持 80 端口和 443 端口。
2. Token 令牌：用于签名处理，下文会介绍相关流程。
3. EncodingAESKey：将用作消息体加解密密钥。
4. 消息加解密方式：
    * 明文模式：不使用消息加解密，明文发送，安全系数较低，不建议使用。
    * 兼容模式：明文、密文共存，不建议使用。
    * 安全模式：使用消息加解密，纯密文，安全系数高，强烈推荐使用。
5. 数据格式：消息体的格式，可选 XML 或 JSON。
![](https://res.wx.qq.com/op_res/CyXI7cGX7jxeP-vdY2HkmzkwiwWyyWSlOTfHFQQaDu_sgJtBKLYMmUo2O0vdu7IzWmZlJ6a-sMAzoEwZPCcs7A)
#### 发起验证
点击「提交」后，微信服务器会对开发者服务器发起验证，请在提交前按以下方式开发：
微信服务器将发送 GET 请求到填写的服务器地址 URL 上， GET 请求携带参数如下表所示：

|参数|描述|
|---|---|
|signature|签名|
|timestamp|时间戳|
|nonce|随机数|
|echostr|随机字符串|

其中，signature 签名的生成方式是：
1. 将 Token、timestamp、nonce 三个参数进行字典序排序。
2. 将三个参数字符串拼接成一个字符串进行 sha1 计算签名，即可获得 signature。
开发者需要校验 signature 是否正确，以判断请求是否来自微信服务器，验签通过后，请原样返回 echostr 字符串。

举例：假设填写的 URL="https://www.qq.com/revice"， Token="AAAAA"。
1. 推送的 URL 链接：https://www.qq.com/revice?signature=f464b24fc39322e44b38aa78f5edd27bd1441696&echostr=4375120948345356249&timestamp=1714036504&nonce=1514711492
2. 将 token、timestamp、nonce 三个参数进行字典序排序，排序后结果为:["1514711492","1714036504","AAAAA"]。
3. 将三个参数字符串拼接成一个字符串："15147114921714036504AAAAA"
4. 进行 sha1 计算签名：f464b24fc39322e44b38aa78f5edd27bd1441696
5. 与 URL 链接中的 signature 参数进行对比，相等说明请求来自微信服务器，合法。
6. 构造回包返回微信，回包消息体内容为 URL 链接中的 echostr 参数 4375120948345356249。

为了便于开发者调试，我们提供了[URL 验证工具](https://developers.weixin.qq.com/apiExplorer?type=messagePush)供开发者使用。![](https://res.wx.qq.com/op_res/CyXI7cGX7jxeP-vdY2Hkm9kdbt1XFpcK-5W3xMW-rUdMFepPUAjXfDxv7v-V28ytPPMMydovWViBMEO4PKXFug
)
开发者需填写[AccessToken](/miniprogram/dev/server/API/mp-access-token/api_getaccesstoken.md)、URL 地址、Token，点击「检查参数并发起验证」后，调试工具会发送 GET 请求到 URL 所指的服务器，并返回相关调试信息。

### 接收消息推送
当特定消息或事件触发时，微信服务器会将消息（或事件）的数据包以 POST 请求发送到开发者配置的 URL，下面以 `debug_demo` 事件为例，详细介绍整个过程：
#### 消息解密方式为明文模式
1. 假设 URL 配置为 https://www.qq.com/revice， 数据格式为 JSON，Token="AAAAA"。
2. 推送的 URL 链接：https://www.qq.com/recive?signature=899cf89e464efb63f54ddac96b0a0a235f53aa78&timestamp=1714037059&nonce=486452656
3. 推送的包体：
```json
{
    "ToUserName": "gh_97417a04a28d",
    "FromUserName": "o9AgO5Kd5ggOC-bXrbNODIiE3bGY",
    "CreateTime": 1714037059,
    "MsgType": "event",
    "Event": "debug_demo",
    "debug_str": "hello world"
}
```
4. 校验 signature 签名是否正确，以判断请求是否来自微信服务器。
    1. 将 token、timestamp（URL 参数中的）、nonce（URL 参数中的）三个参数进行字典序排序，排序后结果为:["1714037059","486452656","AAAAA"]
    2. 将三个参数字符串拼接成一个字符串："1714037059486452656AAAAA"
    3. 进行 sha1 计算签名：899cf89e464efb63f54ddac96b0a0a235f53aa78
    4. 与 URL 链接中的 signature 参数进行对比，相等说明请求来自微信服务器，合法。
5. 回包给微信，具体回包内容取决于特定接口文档要求，如无特定要求，回复空串或者 success 即可。

#### 消息解密方式为安全模式
1. 假设 URL 配置为 https://www.qq.com/revice， 数据格式为 JSON，Token="AAAAA"，EncodingAESKey="AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"，小程序 AppID="wxba5fad812f8e6fb9"。
2. 推送的 URL 链接：：https://www.qq.com/recive?signature=6c5c811b55cc85e0e1b54100749188c20beb3f5d&timestamp=1714112445&nonce=415670741&openid=o9AgO5Kd5ggOC-bXrbNODIiE3bGY&encrypt_type=aes&msg_signature=046e02f8204d34f8ba5fa3b1db94908f3df2e9b3
3. 推送的包体：
```json
{
    "ToUserName": "gh_97417a04a28d",
    "Encrypt": "+qdx1OKCy+5JPCBFWw70tm0fJGb2Jmeia4FCB7kao+/Q5c/ohsOzQHi8khUOb05JCpj0JB4RvQMkUyus8TPxLKJGQqcvZqzDpVzazhZv6JsXUnnR8XGT740XgXZUXQ7vJVnAG+tE8NUd4yFyjPy7GgiaviNrlCTj+l5kdfMuFUPpRSrfMZuMcp3Fn2Pede2IuQrKEYwKSqFIZoNqJ4M8EajAsjLY2km32IIjdf8YL/P50F7mStwntrA2cPDrM1kb6mOcfBgRtWygb3VIYnSeOBrebufAlr7F9mFUPAJGj04="
}
```
4. 校验 msg_signature 签名是否正确，以判断请求是否来自微信服务器。注意：不要使用 signature 验证！
    * 将 token、timestamp（URL 参数中的）、nonce（URL 参数中的）、Encrypt（包体内的字段）四个参数进行字典序排序，排序后结果为:
    ["+qdx1OKCy+5JPCBFWw70tm0fJGb2Jmeia4FCB7kao+/Q5c/ohsOzQHi8khUOb05JCpj0JB4RvQMkUyus8TPxLKJGQqcvZqzDpVzazhZv6JsXUnnR8XGT740XgXZUXQ7vJVnAG+tE8NUd4yFyjPy7GgiaviNrlCTj+l5kdfMuFUPpRSrfMZuMcp3Fn2Pede2IuQrKEYwKSqFIZoNqJ4M8EajAsjLY2km32IIjdf8YL/P50F7mStwntrA2cPDrM1kb6mOcfBgRtWygb3VIYnSeOBrebufAlr7F9mFUPAJGj04=",
    "1714112445",
    "415670741",
    "AAAAA"]。
    * 将四个参数字符串拼接成一个字符串，然后进行 sha1 计算签名：046e02f8204d34f8ba5fa3b1db94908f3df2e9b3
    * 与 URL 参数中的 msg_signature 参数进行对比，相等说明请求来自微信服务器，合法。
5. 解密消息体 `Encrypt` 密文。
    1. AESKey = Base64_Decode( EncodingAESKey + "=" )，EncodingAESKey 尾部填充一个字符的 "="，用 Base64_Decode 生成 32 个字节的 AESKey；
    2. 将 Encrypt 密文进行 Base64 解码，得到 TmpMsg， 字节长度为 224
    3. 将 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#），请开发者尽量使用示例代码，仔细阅读技术文档、示例代码及其注释后，再进行编码调试。[示例下载](https://wximg.gtimg.com/shake_tv/mpwiki/cryptoDemo.zip)
    4. FullStr=random(16B) + msg_len(4B) + msg + appid，其中：
        * random(16B)为 16 字节的随机字符串；
        * msg_len 为 msg 长度，占 4 个字节(网络字节序)；
        * msg 为解密后的明文；
        * appid 为小程序 AppID，开发者需验证此 AppID 是否与自身小程序相符。
    5. 在此示例中：
        * 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"
6. 回包给微信服务器，首先需确定回包包体的明文内容，具体取决于特定接口文档要求，如无特定要求，回复空串或者 success（无需加密）即可，其他回包内容需加密处理。这里假设回包包体的明文内容为"{"demo_resp":"good luck"}"，数据格式为 JSON，下面介绍如何对回包进行加密：
7. 回包格式如下，具体取决于你配置的数据格式（JSON 或 XML），其中：
    * Encrypt：加密后的内容；
    * MsgSignature：签名，微信服务器会验证签名；
    * TimeStamp：时间戳；
    * Nonce：随机数
    ```json
    {
        "Encrypt": "${msg_encrypt}$",
        "MsgSignature": "${msg_signature}$",
        "TimeStamp": ${timestamp}$,
        "Nonce": ${nonce}$
    }
    ```
    ```xml



        ${timestamp}$


    ```
8. Encrypt 的生成方法：
    1. AESKey = Base64_Decode( EncodingAESKey + "=" )，EncodingAESKey 尾部填充一个字符的 "="，用 Base64_Decode 生成 32 个字节的 AESKey；
    2. 构造 FullStr=random(16B) + msg_len(4B) + msg + appid，其中
        * random(16B)为 16 字节的随机字符串；
        * msg_len 为 msg 长度，占 4 个字节(网络字节序)；
        * msg 为明文；
        * appid 为小程序 AppID。
    3. 在此示例中：
        * random(16B)="707722b803182950"
        * msg_len=25（注意：需按网络字节序，占 4 个字节）
        * msg="{"demo_resp":"good luck"}"
        * appid="wxba5fad812f8e6fb9"
        * FullStr 的字节大小为 63
    4. 将 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#），请开发者尽量使用示例代码，仔细阅读技术文档、示例代码及其注释后，再进行编码调试。[示例下载](https://wximg.gtimg.com/shake_tv/mpwiki/cryptoDemo.zip)
    5. 对 TmpMsg 进行 Base64 编码，得到 Encrypt="ELGduP2YcVatjqIS+eZbp80MNLoAUWvzzyJxgGzxZO/5sAvd070Bs6qrLARC9nVHm48Y4hyRbtzve1L32tmxSQ=="。
9. TimeStamp 由开发者生成，使用当前时间戳即可，示例使用 1713424427。
10. Nonce 回填 URL 参数中的 nonce 参数即可，示例使用 415670741。
11. MsgSignature 的生成方法：
    1. 将 token、TimeStamp（回包中的）、Nonce（回包中的）、Encrypt（回包中的）四个参数进行字典序排序，排序后结果为:
    ["1713424427",
    "415670741",
    "AAAAA",
    "ELGduP2YcVatjqIS+eZbp80MNLoAUWvzzyJxgGzxZO/5sAvd070Bs6qrLARC9nVHm48Y4hyRbtzve1L32tmxSQ=="]
    2. 将四个参数字符串拼接成一个字符串，并进行 sha1 计算签名：1b9339964ed2e271e7c7b6ff2b0ef902fc94dea1
12. 最终回包为：
```json
{
    "Encrypt": "ELGduP2YcVatjqIS+eZbp80MNLoAUWvzzyJxgGzxZO/5sAvd070Bs6qrLARC9nVHm48Y4hyRbtzve1L32tmxSQ==",
    "MsgSignature": "1b9339964ed2e271e7c7b6ff2b0ef902fc94dea1",
    "TimeStamp": 1713424427,
    "Nonce": "415670741"
}
```

为了便于开发者调试，我们提供了相关的调试工具（[请求构造](https://developers.weixin.qq.com/apiExplorer?type=messagePush)、[调试工具](https://developers.weixin.qq.com/apiExplorer?type=messagePush)）供开发者使用。
* 「请求构造」允许开发者填写相关参数后，生成 debug_demo 事件发包或回包的相关调试信息，供开发者使用。![](https://res.wx.qq.com/op_res/CyXI7cGX7jxeP-vdY2HkmzPAxYtOmSK_jSvQsQKLjBlK9LTCJjQY14AmHwQmPIcyQWFYPWpn8lqzVjfQKB9y1w
)
* 「调试工具」允许开发者填写[AccessToken](/miniprogram/dev/server/API/mp-access-token/api_getaccesstoken.md)、Body 后，微信服务器会拉取你在小程序后台配置的消息推送配置，实际推送一条 debug_demo 事件供开发者调试。![](https://res.wx.qq.com/op_res/CyXI7cGX7jxeP-vdY2Hkm-hEi9O6s8jFZI7ewGAoi8bUp6X86EpNxCM2wn_rnHrhQ7ByOfhYVmrN-TLJVR69HQ)

## 云函数接收消息推送

> 需开发者工具版本至少 `1.02.1906252`

开通了[云开发](/miniprogram/dev/wxcloudservice/wxcloud/basis/getting-started.md)的小程序可以使用云函数接收消息推送，目前仅支持客服消息推送。

接入步骤如下：

1. 云开发控制台中填写配置并上传
2. 云函数中处理消息

### 第一步：开发者工具云开发控制台中增加配置

前往路径「云开发」-「设置」-「其他设置」-「消息推送」，选择推送模式为云函数；
![](https://res.wx.qq.com/op_res/_mTlTvkYwgSJDQq3yEXGWj-IownxxYbDq3-UzqnfW0-wMSNK67JcHiAhcNsdP0W7Cu8sx_o_MFCH-0R6OWZ8tw)
添加消息推送配置。消息类型对应收包的 `MsgType`，事件类型对应收包的 `Event`，同一个 `<消息类型, 事件类型>` 二元组只能推到一个环境的一个云函数。例如客服消息文本消息对应的就是消息类型为 `text`，事件类型为空。具体值请查看各个消息的消息格式。
![](https://res.wx.qq.com/op_res/qqj08GfodZw3VUmDSD56OKhZ1Xv5mg1P92C4rtchP0hWzvqX6Po7DTYznm-WP_knXQRHiY8hbLabeBXt0bclOg)
多个消息类型、事件类型多次添加消息推送配置即可。

**注意**：如在云函数中配置了某个类型的消息，该类型消息将不再推送至「微信公众平台-开发设置-消息推送」中配置的域名中。

### 第二步：云函数中处理消息

云函数被触发时，其 `event` 参数即是接口所定义的 JSON 结构的对象（统一 `JSON` 格式，不支持 `XML` 格式）。

以客服消息为例，接收到客服消息推送时，`event` 结构如下：

```json
{
  "FromUserName": "ohl4L0Rnhq7vmmbT_DaNQa4ePaz0",
  "ToUserName": "wx3d289323f5900f8e",
  "Content": "测试",
  "CreateTime": 1555684067,
  "MsgId": "49d72d67b16d115e7935ac386f2f0fa41535298877_1555684067",
  "MsgType": "text"
}
```

此时可调用客服消息[发送](/miniprogram/dev/server/API/kf-mgnt/kf-message/api_sendcustommessage.md)接口回复消息，一个简单的接收到消息后统一回复「收到」的示例如下：

```js
// 云函数入口文件
const cloud = require('wx-server-sdk')

cloud.init()

// 云函数入口函数
exports.main = async (event, context) => {
  const wxContext = cloud.getWXContext()

  await cloud.openapi.customerServiceMessage.send({
    touser: wxContext.OPENID,
    msgtype: 'text',
    text: {
      content: '收到',
    },
  })

  return 'success'
}
```

## 微信云托管接收消息推送
使用[微信云托管](https://cloud.weixin.qq.com/cloudrun?utm_source=wxdoc&utm_content=msgpush)的小程序/公众号可以使用云托管服务接收消息推送，只需配置一个云托管服务即可支持所有类型的消息推送。

接入步骤如下：

1. 微信云托管控制台中填写配置
2. 云托管服务中处理消息

### 第一步：云托管控制台填写配置
前往路径「微信云托管」-「设置」-「其他设置」-「消息推送」中配置；
![](https://res.wx.qq.com/op_res/8hBkzu9UFs7RiXKaj8GiT61rKJwHGEZiQVGDcnq8lw3yIgvxbXpguJl69KE6yWoS)

点击配置，选择目标云开发环境、填写对应的云托管服务路径（路径可前往「云托管」-「服务列表」-「路径字段」中复制）、选择推送类型；
* 环境 ID：选择接收消息推送
* 服务名称：接收消息推送的服务，只需配置 1 个服务即可接收所有类型消息；
* path：服务下哪个接口接收即写该接口在服务内的路径即可；
* 推送模式：支持 JSON、XML 两种模式；

配置完成后，该云托管服务即可接收当前小程序/公众号下所有类型消息推送。

#### 配置测试

配置消息推送时，微信后台会向配置的服务发起一个检测请求。

当配置格式为 JSON 时，请求体为：

```
{ "action": "CheckContainerPath"}
```

当配置格式为 XML 时，请求体为：

```
<xml><action>CheckContainerPath</action></xml>
```

开发者回复 success 或回复空即可。

#### 确认消息来源
若云托管未开启公网访问，则可以信任所有消息推送。若云托管开启了公网访问，需要验证消息推送的请求头，带 **x-wx-sources** 的请求才是微信侧发起的推送。

### 第二步：云托管服务中处理消息
下面的例子展示如何使用云托管结合消息推送，实现客服消息回复。
注意：需要先部署好以下的镜像，再在设置-其他设置-消息推送中，填入对应服务的路径和环境 ID。

```js
const express = require('express')
const bodyParser = require('body-parser')
const axios = require('axios')

const PORT = process.env.PORT || 80
const HOST = '0.0.0.0'

// App
const app = express()

app.use(bodyParser.raw())
app.use(bodyParser.json({}))
app.use(bodyParser.urlencoded({ extended: true }))

const client = axios.default

app.all('/', async (req, res) => {
    const headers = req.headers
    const weixinAPI = `http://api.weixin.qq.com/cgi-bin/message/custom/send`
    const payload = {
        touser: headers['x-wx-openid'],
        msgtype: 'text',
        text: {
            content: `云托管接收消息推送成功，内容如下：\n${JSON.stringify(req.body, null, 2)}`
        }
    }
    // dispatch to wx server
    const result = await client.post(weixinAPI, payload)
    console.log('received request', req.body, result.data)
    res.send('success')
});

app.listen(PORT, HOST)
console.log(`Running on http://${HOST}:${PORT}`)
```

配置成功后，使用 `<button open-type="contact">` 类型的按钮唤起客服会话，发送任意消息即可看到云托管处理的回复。
