微信公众号消息回复服务端踩坑小计

1,021 阅读8分钟

在微信公众号开发中,Webhook是一项非常重要的技术,它可以帮助我们在自己的服务器上接收、解密和处理微信服务器发送的消息通知,从而实现与用户的互动和业务逻辑的处理。本文将介绍如何在NestJS框架中配置微信公众号服务端Webhook,并在服务器上接收、解密和返回回复的过程。

准备工作

在开始配置Webhook之前,我们需要先准备好以下工作:

  • 微信公众号的开发者账号和相关配置;
  • 一台可以访问互联网的服务器;
  • 一个可用的内网穿透服务,如ngrok。

配置Webhook

配置URL

在微信公众号开发者中心中,我们需要在“接口配置信息”中配置Webhook URL和Token等参数。首先,我们需要在自己的服务器上安装ngrok,使用ngrok的HTTP代理功能来穿透到自己的服务器上。

在Windows操作系统中,我们可以使用Powershell执行以下命令来启动ngrok:

bashCopy code
./ngrok.exe http [port]

其中,port是自己的服务器上运行Webhook的端口号。启动成功后,可以看到ngrok生成的隧道URL地址,这个地址就是我们需要配置到微信公众号开发者中心中的Webhook URL地址。

需要注意的是,ngrok生成的URL地址必须支持HTTPS协议,否则微信服务器无法访问。

微信公众号服务端配置Webhook

微信公众号提供了一个服务端API,允许开发者接收来自微信服务器的消息和事件通知,其中就包括Webhook通知。以下是配置微信公众号服务端Webhook的步骤:

  1. 登录微信公众平台,进入“开发-基本配置”页面,找到“服务器配置”一栏,点击“修改配置”按钮。

  2. 在“服务器配置”页面中,勾选“启用”选项,填写URL和Token。

    URL是用于接收微信服务器发送的消息和事件通知的地址,必须是公网可访问的HTTP或HTTPS协议,一般形式为https://yourdomain.com/wechat/callback,其中/wechat/callback是开发者自己定义的路径。

    Token是用于验证消息真实性的参数,是开发者自己定义的一串字符串,可以是任意字符,但必须与接收消息时使用的Token一致。

  3. 点击“提交”按钮,系统会向填写的URL地址发送一个GET请求,用于验证Token的有效性,开发者需要在自己的服务器上接收并处理该请求,返回与Token一致的字符串。

    验证请求的URL格式为https://yourdomain.com/wechat/callback?signature=xxx&timestamp=yyy&nonce=zzz&echostr=aaa,其中signature是微信服务器生成的签名字符串,timestamp是当前时间戳,nonce是随机数,echostr是随机字符串。开发者需要对signaturetimestampnonce三个参数进行字典序排序,将它们拼接成一个字符串,再使用SHA1算法进行加密,最后与接收到的signature进行比较,如果相等则表示验证通过,返回echostr参数即可。

  4. 验证通过后,微信服务器会开始向开发者填写的URL地址发送消息和事件通知,开发者需要在自己的服务器上编写接收处理程序,对不同类型的消息和事件进行不同的处理。

在微信公众号验证Webhook时,微信服务器会向开发者配置的URL地址发送一个GET请求,开发者需要在自己的服务器上接收并处理该请求,进行signature校验和echostr返回。

signature是微信服务器传过来的一个参数,用于校验消息的真实性,开发者需要根据Token和微信服务器传过来的timestamp和nonce参数计算出一个signature值,并与微信服务器传过来的signature值进行比较,如果相等则说明消息没有被篡改,验证通过。

如果验证通过,则需要将接收到的echostr参数直接返回给微信服务器,确保原封不动。这一点需要注意,如果使用NestJS框架进行开发,返回的格式必须是text而不能是json,否则微信服务器会认为验证失败。开发者可以使用res.send(echostr)的方式返回echostr参数。

需要注意的是,这个验证过程只需要在初次配置Webhook时进行,之后的Webhook通知都不需要进行验证。开发者只需要在自己的服务器上编写接收处理程序,对不同类型的消息和事件进行不同的处理即可。

微信后台验证通过后,如果有用户回复消息,则会将消息转发给开发者在微信公众号服务端配置的Webhook URL,此时转发的请求为POST请求。因此,在NestJS中,需要编写两个不同的接口来分别处理GET请求和POST请求。

首先,我们需要编写用于验证的GET接口,用于接收微信服务器发送的验证请求,进行signature校验和echostr返回。接口代码可以类似如下:


@Controller('wechat')
export class WechatController {
  constructor(private readonly wechatService: WechatService) {}

  @Get('callback')
  verifyCallback(@Query() query: any, @Res() res: Response) {
    const { signature, timestamp, nonce, echostr } = query;
    const isValid = this.wechatService.verifySignature(signature, timestamp, nonce);

    if (isValid) {
      res.send(echostr);
    } else {
      res.status(HttpStatus.FORBIDDEN).send('Invalid signature');
    }
  }
}

其中,verifyCallback方法用于处理GET请求,接收微信服务器发送的查询参数,包括signature、timestamp、nonce和echostr,使用verifySignature方法进行校验,如果校验成功则返回echostr,否则返回错误信息。

接下来,我们需要编写用于处理消息的POST接口,用于接收微信服务器发送的消息通知,进行解密和处理。接口代码可以类似如下:


@Controller('wechat')
export class WechatController {
  constructor(private readonly wechatService: WechatService) {}

  @Post('callback')
  handleCallback(@Body() body: any, @Res() res: Response) {
    const { Encrypt } = body.xml;
    const decrypted = this.wechatService.decryptMessage(Encrypt);

    const result = this.wechatService.handleMessage(decrypted);
    const encrypted = this.wechatService.encryptMessage(result);

    const response = `<xml>
      <Encrypt><![CDATA[${encrypted}]]></Encrypt>
      <MsgSignature><![CDATA[${this.wechatService.generateSignature(result)}]]></MsgSignature>
      <TimeStamp>${Date.now()}</TimeStamp>
      <Nonce><![CDATA[${this.wechatService.generateNonce()}]]></Nonce>
    </xml>`;

    res.send(response);
  }
}

其中,handleCallback方法用于处理POST请求,接收微信服务器发送的加密消息,使用decryptMessage方法进行解密,并使用handleMessage方法对解密后的消息进行处理。处理完成后,再使用encryptMessage方法对返回结果进行加密,生成MsgSignature、TimeStamp和Nonce等参数,组成XML格式的消息并返回给微信服务器。

需要注意的是,在使用NestJS进行开发时,需要将两个接口分别使用Get()和Post()进行修饰,分别对应GET请求和POST请求。同时,开发者还需要根据具体业务需求编写自己的WechatService服务,实现各种消息和事件的处理逻辑。

在处理POST请求时,我们需要对微信服务器发送的加密消息进行解密,并根据消息内容进行处理和回复。处理完成后,我们需要对回复消息进行加密,并使用HTTP 200状态码返回给微信服务器。

在NestJS中,我们可以使用HttpCode装饰器来指定HTTP响应的状态码。具体实现如下:

typescriptCopy code
@Controller('wechat')
export class WechatController {
  constructor(private readonly wechatService: WechatService) {}

  @Post('callback')
  @HttpCode(200)
  handleCallback(@Body() body: any, @Res() res: Response) {
    const { Encrypt } = body.xml;
    const decrypted = this.wechatService.decryptMessage(Encrypt);

    const result = this.wechatService.handleMessage(decrypted);
    const encrypted = this.wechatService.encryptMessage(result);

    const response = `<xml>
      <Encrypt><![CDATA[${encrypted}]]></Encrypt>
      <MsgSignature><![CDATA[${this.wechatService.generateSignature(result)}]]></MsgSignature>
      <TimeStamp>${Date.now()}</TimeStamp>
      <Nonce><![CDATA[${this.wechatService.generateNonce()}]]></Nonce>
    </xml>`;

    res.send(response);
  }
}

需要注意的是,在返回回复消息时,我们需要对回复消息进行加密,并且使用CDATA标记来避免XML转义,确保返回结果的正确性。同时,由于微信服务器需要定时发送心跳包来检测开发者服务器的可用性,因此我们需要确保返回结果的正确性和及时性,保证Webhook能够稳定运行。

在NestJS中,我们可以使用NestInterceptor来实现在请求进入和离开Controller之间的拦截和操作。拦截器可以用于实现各种中间件、日志记录、身份验证、缓存等功能,具有非常广泛的应用场景。

然而,在处理微信公众号Webhook时,我们需要直接使用res.send来返回回复消息,而不是使用NestInterceptor进行拦截和处理。这是因为NestJS默认内置的是Express框架,而在Express框架中,如果直接使用res.send返回结果,就可以直接跳过NestInterceptor的拦截和处理,不会进入到next的call里面。

在微信公众号开发中,我们通常会在微信测试接口中对自己的Webhook进行验证和测试。在进行测试时,需要注意一些细节问题,以确保测试能够成功进行。

首先,需要注意的是,由于ngrok需要VPN才能访问,因此在微信测试接口中无法直接输入ngrok的链接进行测试。因此,在进行简单验证时,可以尝试使用一些在线服务,如webhook.site/等来测试Webhook…

另外,我们需要注意到,在测试过程中,可以通过微信测试号来模拟用户发送消息,从而观察Webhook的处理结果。同时,需要确保Webhook接收到消息后能够及时处理和返回回复消息,保证Webhook能够稳定运行。

最后,需要注意的是,微信公众号开发涉及到很多技术细节和安全风险,开发者需要根据具体业务需求和安全要求进行合理的架构设计和实现,确保代码质量和系统可靠性。同时,我们还需要注意官方文档和API文档的更新和变化,及时了解并适应新的技术和业务发展,提高自己的技术水平和竞争力。