接入微信公众号开发初探

571 阅读6分钟

一、业务需求

手机扫描二维码后关注公众号,发送验证码给公众号,公众号返回验证码,然后输入到网页判断验证码是否正确后通过登录。

image.png

二、初步接入微信公众号

接入微信公众平台开发,开发者需要按照如下步骤完成:

  • 1、填写服务器配置

  • 2、验证服务器地址的有效性

  • 3、依据接口文档实现业务逻辑

2.1 填写服务器配置

登录微信公众平台官网后,在公众平台官网的开发-基本设置页面,勾选协议成为开发者,点击“修改配置”按钮,填写服务器地址(URL)、Token和EncodingAESKey,其中URL是开发者用来接收微信消息和事件的接口URL。Token可由开发者可以任意填写,用作生成签名(该Token会和接口URL中包含的Token进行比对,从而验证安全性)。EncodingAESKey由开发者手动填写或随机生成,将用作消息体加解密密钥。

同时,开发者可选择消息加解密方式:明文模式、兼容模式和安全模式。。加解密方式的默认状态为明文模式,选择兼容模式和安全模式需要提前配置好相关加解密代码。

image.png

2.2 验证服务器地址有效性

开发者提交信息后,微信服务器将发送GET请求到填写的服务器地址URL上,GET请求携带参数如下表所示:

参数描述
signature微信加密签名,signature结合了开发者填写的token参数和请求中的timestamp参数、nonce参数。
timestamp时间戳
nonce随机数
echostr随机字符串

开发者通过检验signature对请求进行校验(下面有校验方式)。若确认此次GET请求来自微信服务器,请原样返回echostr参数内容,则接入生效,成为开发者成功,否则接入失败。加密/校验流程如下:

1)将token、timestamp、nonce三个参数进行字典序排序

2)将三个参数字符串拼接成一个字符串进行sha1加密

3)开发者获得加密后的字符串可与signature对比,标识该请求来源于微信

2.2.1 验证消息的确来自微信服务器 - 代码

token值要与配置服务器时的token一致

我们采取明文模式,不需要encrypt密文参数

signature与sha1加密后的token、timestamp、nonce 进行对比,成功则返回echostr,成为开发者。

private static final String token = "ShiShuoMing";

@GetMapping("callback")
public String callback(@RequestParam("signature") String signature,
                       @RequestParam("timestamp") String timestamp,
                       @RequestParam("nonce") String nonce,
                       @RequestParam("echostr") String echostr) {
    log.info("get验签请求参数:signature:{},timestamp:{},nonce:{},echostr:{}",
            signature, timestamp, nonce, echostr);
    String shaStr = SHA1.getSHA1(token, timestamp, nonce, "");
    if (signature.equals(shaStr)) {
        return echostr;
    }
    return "unknown";
}

2.2.2 SHA1 加密

@Log4j2
public class SHA1 {

    /**
     * 用SHA1算法生成安全签名
     *
     * @param token     票据
     * @param timestamp 时间戳
     * @param nonce     随机字符串
     * @param encrypt   密文
     * @return 安全签名
     */
    public static String getSHA1(String token, String timestamp, String nonce, String encrypt) {
        try {
            String[] array = new String[]{token, timestamp, nonce, encrypt};
            StringBuffer sb = new StringBuffer();
            // 字符串排序
            Arrays.sort(array);
            for (int i = 0; i < 4; i++) {
                sb.append(array[i]);
            }
            String str = sb.toString();
            // SHA1签名生成
            MessageDigest md = MessageDigest.getInstance("SHA-1");
            md.update(str.getBytes());
            byte[] digest = md.digest();

            StringBuffer hexStr = new StringBuffer();
            String shaHex = "";
            for (int i = 0; i < digest.length; i++) {
                shaHex = Integer.toHexString(digest[i] & 0xFF);
                if (shaHex.length() < 2) {
                    hexStr.append(0);
                }
                hexStr.append(shaHex);
            }
            return hexStr.toString();
        } catch (Exception e) {
            log.error("sha加密生成签名失败:", e);
            return null;
        }
    }
}

2.2.3 内网穿透

在第一步配置服务器时,除了签名token还需要填写URL,当微信服务器会向URL发送get请求,当验签通过后,我们才可以成为开发者,拥有权限(向用户发送消息等等)。

由于开发环境不方便调试,所以就需要用到natapp内网穿透技术了,把本地ip映射到外网域名地址。

使用natapp进行内网穿透,配置如下:

image.png

配置完成后,会得到authtoken, 之后在natapp.exe的文件目录下,cmd输入start natapp -authtoken=xxxx启动,就可以把本地url地址映射到外网。

image.png

以测试号为例,此时URL地址就为:

image.png

能够成功填写接口配置信息就代表我们已经成为了开发者(提交信息时,微信服务器会发送请求验签),用户向公众号发送消息时。

2.3 接收普通消息(接收消息、关注、取关等) -- 初探

公众号方收到的消息发送者是一个OpenID,是使用用户微信号加密后的结果,每个用户对每个公众号有一个唯一的OpenID。

当普通微信用户向公众账号发消息时,微信服务器将POST消息的XML数据包到开发者填写的URL上(还是callback接口)。

请注意:

  1. 关于重试的消息排重,推荐使用msgid排重。
  2. 微信服务器在五秒内收不到响应会断掉连接,并且重新发起请求,总共重试三次。假如服务器无法保证在五秒内处理并回复,可以直接回复空串,微信服务器不会对此作任何处理,并且不会发起重试。详情请见“发送消息-被动回复消息”。
  3. 如果开发者需要对用户消息在5秒内立即做出回应,即使用“发送消息-被动回复消息”接口向用户被动回复消息时,可以在

公众平台官网的开发者中心处设置消息加密。开启加密后,用户发来的消息和开发者回复的消息都会被加密(但开发者通过客服接口等API调用形式向用户发送消息,则不受影响)。关于消息加解密的详细说明,请见“发送消息-被动回复消息加解密说明”。 各消息类型的推送XML数据包结构如下:

文本消息格式

<xml>
  <ToUserName><![CDATA[toUser]]></ToUserName>
  <FromUserName><![CDATA[fromUser]]></FromUserName>
  <CreateTime>1348831860</CreateTime>
  <MsgType><![CDATA[text]]></MsgType>
  <Content><![CDATA[this is a test]]></Content>
  <MsgId>1234567890123456</MsgId>
  <MsgDataId>xxxx</MsgDataId>
  <Idx>xxxx</Idx>
</xml>
参数描述
ToUserName开发者微信号
FromUserName发送方账号(一个OpenID)
CreateTime消息创建时间 (整型)
MsgType消息类型,文本为text
Content文本消息内容
MsgId消息id,64位整型
MsgDataId消息的数据ID(消息如果来自文章时才有)
Idx多图文时第几篇文章,从1开始(消息如果来自文章时才有)

参数解读:

  • @requestBody: 请求的消息体会放到requestBody里
  • @msg_signature: 开启加密模式时,post请求还会发送meg_signature消息用于验签,明文时不会发送
@PostMapping(value = "callback", produces = "application/xml;charset=UTF-8")
public String callback(
        @RequestBody String requestBody,
                       @RequestParam("signature") String signature,
                       @RequestParam("timestamp") String timestamp,
                       @RequestParam("nonce") String nonce,
                       @RequestParam(value = "meg_signature", required = false) String msgSignature) {
    log.info("接收到微信的请求:request:{}, signature:{},timestamp:{},nonce:{}", requestBody, signature, timestamp, nonce);
    return "unknown";
}

image.png

2.5 回复消息 -- 初探

我们希望当用户关注后,为用户发送验证码。同样,也应该以XML形式发送,我们要得到两个参数:

ToUserName:发给谁

FromUserName:来自哪(公众号)

回复文本消息格式

<xml>
  <ToUserName><![CDATA[toUser]]></ToUserName>
  <FromUserName><![CDATA[fromUser]]></FromUserName>
  <CreateTime>12345678</CreateTime>
  <MsgType><![CDATA[text]]></MsgType>
  <Content><![CDATA[你好]]></Content>
</xml>
参数是否必须描述
ToUserName接收方账号(收到的OpenID)
FromUserName开发者微信号
CreateTime消息创建时间 (整型)
MsgType消息类型,文本为text
Content回复的消息内容(换行:在content中能够换行,微信客户端就支持换行显示)
@PostMapping(value = "callback", produces = "application/xml;charset=UTF-8")
public String callback(
        @RequestBody String requestBody,
                       @RequestParam("signature") String signature,
                       @RequestParam("timestamp") String timestamp,
                       @RequestParam("nonce") String nonce,
                       @RequestParam(value = "meg_signature", required = false) String msgSignature) {
    log.info("接收到微信的请求:request:{}, signature:{},timestamp:{},nonce:{}", requestBody, signature, timestamp, nonce);

    Map<String, String> msgMap = MessageUtil.parseXml(requestBody);
    String toUserName = msgMap.get("ToUserName"); //公众号
    String fromUserName = msgMap.get("FromUserName"); //发送者

    String msg = "<xml>\n" +
            "  <ToUserName><![CDATA[" + fromUserName + "]]></ToUserName>\n" +
            "  <FromUserName><![CDATA[" + toUserName + "]]></FromUserName>\n" +
            "  <CreateTime>12345678</CreateTime>\n" +
            "  <MsgType><![CDATA[text]]></MsgType>\n" +
            "  <Content><![CDATA[你好,欢迎来到坤坤Club]]></Content>\n" +
            "</xml>";

    return msg;
}