使用 Serverless 实现微信企业号应用

前言

微信作为目前最热门的社交媒体之一,已经成为了企业展示、沟通的一种重要方式。而在企业内部沟通中,企业号则是微信的重要组成部分。利用企业号,企业可以通过微信平台来对内、对外进行沟通和管理。

在本文中,我们将探讨如何使用 Serverless 技术来实现微信企业号应用的开发。Serverless 是一种基于云计算的应用部署方式,它不需要我们花费大量的时间和精力来管理服务器,而是使我们可以快速、高效地编写和部署应用程序。

Serverless 架构

在 Serverless 架构中,没有服务器的概念,而是将所有的应用程序组成部分均分散在了多个服务上。这些服务可以在云环境下进行部署和管理。Serverless 架构的核心是 FaaS(Functions as a Service),即以函数为基础的服务。

使用 Serverless 架构的好处有:

  • 提供高度可伸缩性,减少或增加服务器无需任何的人工干预。
  • 降低了管理和维护服务器所需的时间和人力成本。
  • 对于应用程序的部署和更新过程,Serverless 提供了全自动化的处理。

微信企业号开发

开发环境

在开始使用 Serverless 进行微信企业号的开发之前,我们需要先搭建好相应的开发环境。我们需要以下三个工具:

  • 微信企业号
  • Serverless Framework
  • Node.js

注册企业号

如果您还没有微信企业号,您可以通过微信企业号官网进行注册。注册完成之后,您还需要通过审核才能获得可用的 API。

安装 Serverless Framework

Serverless Framework 是一个 Serverless 应用程序开发平台,它支持多个云服务商。在本文中,我们将使用 Serverless Framework 的阿里云函数计算插件来完成微信企业号应用的开发。

# 安装 Serverless Framework
npm install -g serverless
# 安装阿里云函数计算插件
npm install -g serverless-plugin-alibabacloud

配置微信企业号

完成前两个准备工作之后,我们需要在微信企业号后台中创建应用,并配置获取接口的 URL。

实现微信企业号应用

接下来,我们需要创建一个 Serverless 项目,并在其中实现微信企业号应用的功能。

创建 Serverless 项目

# 创建 Serverless 项目
serverless create --template aliyun-nodejs --path serverless-wechat-enterprise
# 进入项目
cd serverless-wechat-enterprise

配置项目

serverless.yml 文件中,我们需要添加以下内容:

# serverless.yml
service: serverless-wechat-enterprise

# serverless 配置
provider:
  name: aliyun
  runtime: nodejs12

# 插件配置
plugins:
  - serverless-plugin-alibabacloud

# 函数配置
functions:
  wechat:
    handler: index.handler # 函数
    events:
      - http:
          path: /
          method: POST # POST 请求

index.handler 中,我们将实现微信企业号应用的功能。

实现应用功能

index.js 文件中,我们需要实现以下几个功能:

  1. 验证请求是否来自微信服务器
  2. 解密微信服务器发送的消息
  3. 处理微信服务器发送的消息
  4. 加密消息并返回给微信服务器
验证请求消息
// index.js
const TOKEN = 'YOUR_TOKEN'; // 填写你的 token,与微信后台保持一致

exports.handler = async function (event, context, callback) {
  const { query, body } = event;
  try {
    const { signature, timestamp, nonce, echostr } = query;

    if (signature && timestamp && nonce && echostr) {
      // 计算签名
      const sortedArr = [TOKEN, timestamp, nonce].sort();
      const str = sortedArr.join('');
      const hash = crypto.createHash('sha1').update(str).digest('hex');

      // 验证签名
      if (signature === hash) {
        console.log('验证成功');
        return { isBase64Encoded: false, statusCode: 200, body: echostr };
      }
      console.warn('签名校验失败');
      return { isBase64Encoded: false, statusCode: 403, body: '签名校验失败' };
    }

    // 其他请求消息
    console.log(BODY_XML.parse(body));
    // TODO: 处理其他类型的请求消息

    return { isBase64Encoded: false, statusCode: 200, body: 'success' };
  } catch (err) {
    console.error(err.stack);
    return { isBase64Encoded: false, statusCode: 502, body: 'Bad Gateway' };
  }
};
解密消息

微信服务器向我们的应用发送的消息有加密和未加密两种,我们可以通过判断消息体中是否有 Encrypt 字段来判断当前消息是否加密。如果是加密的消息,我们需要先对消息进行解密。

加密消息的解密算法如下:

// messageDecrypt.js
const crypto = require('crypto');
const zlib = require('zlib');

/**
 * 解密微信商户平台消息
 * @param {String} appId 商户号 `app_id`
 * @param {String} aesKey 商户号 `api_secret`
 * @param {String} encrypted 加密的消息
 * @return {String} 解密后的消息
 */
module.exports = function messageDecrypt(appId, aesKey, encrypted) {
  const buf = Buffer.from(encrypted, 'base64');
  const decipher = crypto.createDecipheriv('aes-256-cbc', aesKey.slice(0, 32), buf.slice(0, 16));
  decipher.setAutoPadding(false);
  try {
    let decrypted = decipher.update(buf.slice(16), 'binary', 'utf8');
    decrypted += decipher.final('utf8');
    decrypted = decrypted.replace(/[\x00-\x20]+/g, ''); // 去除填充数据

    const result = decrypted.match(new RegExp(`^.*<AppId><!\\[CDATA\\[(.*)\\]\\]><\\/AppId><Encrypt><!\\[CDATA\\[(.*)\\]\\]><\\/Encrypt>$`));
    if (!result || result.length !== 3) {
      throw new Error('Decryption failed: Invalid response format');
    }
    if (result[1] !== appId) {
      throw new Error('Decryption failed: Invalid appid');
    }

    const decryptedXML = zlib.inflateRawSync(Buffer.from(result[2], 'base64')).toString('utf8');
    return decryptedXML;
  } catch (err) {
    console.error(err.stack);
    throw new Error('Decryption failed');
  }
};
处理消息

解密消息之后,我们就可以对消息进行处理了。在处理时,我们需要根据 MsgType 字段来区分不同类型的消息,并根据需求来作出回应。

// index.js
const parseXML = require('xml2js').parseStringPromise;
const messageDecrypt = require('./messageDecrypt');

exports.handler = async function (event, context, callback) {
  const { query, body } = event;
  try {
    const { signature, timestamp, nonce, echostr } = query;

    if (signature && timestamp && nonce && echostr) {
      // 计算签名
      const sortedArr = [TOKEN, timestamp, nonce].sort();
      const str = sortedArr.join('');
      const hash = crypto.createHash('sha1').update(str).digest('hex');

      // 验证签名
      if (signature === hash) {
        console.log('验证成功');
        return { isBase64Encoded: false, statusCode: 200, body: echostr };
      }
      console.warn('签名校验失败');
      return { isBase64Encoded: false, statusCode: 403, body: '签名校验失败' };
    }

    // 解密消息
    const cryptMsg = BODY_XML.parse(body);
    let message = cryptMsg.xml;
    if (cryptMsg.xml && cryptMsg.xml.Encrypt) {
      const decryptedMsg = messageDecrypt(APP_ID, API_SECRET, cryptMsg.xml.Encrypt);
      message = await parseXML(decryptedMsg, { explicitArray: false }).then(res => res.xml);
    }

    console.log(message);
    switch (message.MsgType) {
      case 'text':
        // 处理文本消息
        const textResp = new TextResponse(message.FromUserName, message.ToUserName, '你好!欢迎关注本公众号');
        console.log(textResp);
        break;
      case 'event':
        // 处理事件消息
        if (message.Event === 'subscribe') {
          const eventResp = new TextResponse(message.FromUserName, message.ToUserName, '欢迎关注本公众号');
          console.log(eventResp);
        }
        break;
      default:
        break;
    }

    // 返回成功消息
    const encryptedResp = messageEncrypt(APP_ID, API_SECRET, textResp.toXML());
    const timestamp = parseInt(new Date().getTime() / 1000, 10).toString();

    const sortedArr = [TOKEN, timestamp, nonce, encryptedResp].sort();
    const str = sortedArr.join('');
    const hash = crypto.createHash('sha1').update(str).digest('hex');
    const nonceConsistent = '1234567890'; // 随机字符串
    const resp = SOCKET_XML.buildObject({
      xml: {
        Encrypt: encryptedResp,
        MsgSignature: hash,
        TimeStamp: timestamp,
        Nonce: nonceConsistent,
      },
    });
    console.log(resp);

    return { isBase64Encoded: false, statusCode: 200, body: resp };
  } catch (err) {
    console.error(err.stack);
    return { isBase64Encoded: false, statusCode: 502, body: 'Bad Gateway' };
  }
};
加密消息并返回

在对消息进行处理之后,我们需要将回应消息进行加密,并返回给微信服务器。

加密消息的加密算法如下:

// messageEncrypt.js
const crypto = require('crypto');
const zlib = require('zlib');

/**
 * 加密微信商户平台消息
 * @param {String} appId 商户号 `app_id`
 * @param {String} aesKey 商户号 `api_secret`
 * @param {String} message 加密的消息
 * @return {String} 加密后的消息
 */
module.exports = function messageEncrypt(appId, aesKey, message) {
  const randStr = crypto.randomBytes(16).toString('hex');
  const prefix = Buffer.from(randStr).toString('base64') + Buffer.from(message.length.toString()).toString('base64');
  const msg = prefix + message; // 消息体

  const str = Buffer.from(message).toString('binary');
  const x16encoded = aesEncrypt(appId, aesKey, str);
  const x64encoded = Buffer.from(x16encoded, 'binary').toString('base64');

  const msgSignature = sha1([aesKey, randStr, prefix, message.length.toString(), message].sort().join(''));

  const reply = "<xml>" +
    "<Encrypt><![CDATA[" + x64encoded + "]]></Encrypt>" +
    "<MsgSignature><![CDATA[" + msgSignature + "]]></MsgSignature>" +
    "<TimeStamp>" + parseInt(new Date().getTime() / 1000, 10).toString() + "</TimeStamp>" +
    "<Nonce><![CDATA[" + '1234567890' + "]]></Nonce>" +
    "</xml>";
  return reply;
};

function aesEncrypt(appId, aesKey, str) {
  const key = Buffer.from(aesKey + '=', 'base64');
  const iv = key.slice(0, 16);
  const cipher = crypto.createCipheriv('aes-256-cbc', key.slice(0, 32), iv);
  const addLen = 32 - (str.length % 32);
  const pad = String.fromCharCode(addLen).repeat(addLen);
  const encryptText = cipher.update(str + pad) + cipher.final();
  return encryptText.toString('base64');
}

function sha1(str) {
  const shasum = crypto.createHash('sha1');
  shasum.update(str);
  return shasum.digest('hex');
}

处理完成之后,我们就可以将加密后的消息返回给微信服务器了。

总结

通过本文的介绍和示例代码,您应该已经了解了如何使用 Serverless Framework 来开发微信企业号应用。使用 Serverless 架构,不但可以更加高效地开发应用程序,还能够减轻服务器管理和维护的负担。

在进行开发时,您还需要注意以下几点:

  1. Serverless 架构中没有服务器,因此您需要注意对应用程序进行监控和调试。
  2. 微信企业号的接口可能随时发生变化,请根据最新的接口文档进行开发。
  3. 在开发过程中需要注意应用程序的安全性,如签名算法、消息加解密等。

感谢您的阅读,希望能对您有所帮助!

来源:JavaScript中文网 ,转载请注明来源 本文地址:https://www.javascriptcn.com/post/65a8980eadd4f0e0ff1bf4da


纠错反馈