09、微信客服消息

https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1458557405
https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140547

当用户和公众号产生特定动作的交互时(具体动作列表请见下方说明),微信将会把消息数据推送给开发者,开发者可以在一段时间内(目前修改为48小时)调用客服接口,通过POST一个JSON数据包来发送消息给普通用户。此接口主要用于客服等有人工消息处理环节的功能,方便开发者为用户提供更加优质的服务。

1、用户发送信息
2、点击自定义菜单(仅有点击推事件、扫码推事件、扫码推事件且弹出“消息接收中”提示框这3种菜单类型是会触发客服接口的)
3、关注公众号
4、扫描二维码
5、支付成功
6、用户维权
  • 添加客服帐号
    • 开发者可以通过本接口为公众号添加客服账号,每个公众号最多添加10个客服账号。
  • 删除客服帐号
  • 修改客服帐号
  • 设置客服帐号的头像
    • 开发者可调用本接口来上传图片作为客服人员的头像,头像图片文件必须是jpg格式,推荐使用640*640大小的图片以达到最佳效果。
  • 获取所有客服账号
  • 接口的统一参数说明
  • 客服接口-发消息
    • 发送小程序卡片(要求小程序与公众号已关联)
  • 客服输入状态
    这些没什么好备注的,直接根据官方文档来写就OK

触发微信客服功能

注:发送指定的消息接入微信客服功能(将消息转发到客服)

如果公众号处于开发模式,普通微信用户向公众号发消息时,微信服务器会先将消息POST到开发者填写的url上,如果希望将消息转发到客服系统,则需要开发者在响应包中返回MsgType为transfer_customer_service的消息,微信服务器收到响应后会把当次发送的消息转发至客服系统。您也可以在返回transfer_customer_service消息时,在XML中附上TransInfo信息指定分配给某个客服帐号。
用户被客服接入以后,客服关闭会话以前,处于会话过程中时,用户发送的消息均会被直接转发至客服系统。当会话超过30分钟客服没有关闭时,微信服务器会自动停止转发至客服,而将消息恢复发送至开发者填写的url上。
用户在等待队列中时,用户发送的消息仍然会被推送至开发者填写的url上。
这里特别要注意,只针对微信用户发来的消息才进行转发,而对于其他任何事件(比如菜单点击、地理位置上报等)都不应该转接,否则客服在客服系统上就会看到一些无意义的消息了。
1、即当用户接入客服后,用户发送的消息将不会在发送到开发者填写的url上,只有断开后才会发送到开发者填写的url上
2、如果公众号处于开发模式,普通微信用户向公众号发消息时,微信服务器会先将消息POST到开发者填写的url上,想接入客服,则需开发者在响应包中返回MsgType为transfer_customer_service的消息

package com.lm.action.accessToken;

import com.lm.conf.WechatConf;
import com.lm.util.AesException;
import com.lm.util.SHA1;
import com.lm.util.WXBizMsgCrypt;
import com.lm.util.XmlUtil;
import org.dom4j.DocumentException;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Date;
import java.util.Map;

/**
 * 微信服务器接入
 * 开发者通过检验signature对请求进行校验(下面有校验方式)。
 * 若确认此次GET请求来自微信服务器,请原样返回echostr参数内容,则接入生效,成为开发者成功,
 * 否则接入失败。加密/校验流程如下:
 *
 * 1)将token、timestamp、nonce三个参数进行字典序排序
 * 2)将三个参数字符串拼接成一个字符串进行sha1加密
 * 3)开发者获得加密后的字符串可与signature对比,标识该请求来源于微信
 *
 *signature 微信加密签名,signature结合了开发者填写的token参数和请求中的timestamp参数、nonce参数。
 *timestamp 时间戳
 *nonce 随机数
 *echostr   随机字符串
 */
@Controller
public class KfAction {

    @GetMapping("/wechat/server/config")
    @ResponseBody
    public String get(HttpServletRequest request, HttpServletResponse response,
                    String signature, String timestamp, String nonce, String echostr) throws IOException {
        return echostr;
    }

    @PostMapping("/wechat/server/config")
    @ResponseBody
    public String post(HttpServletRequest request, @RequestBody String requestBody, HttpServletResponse response,
                     String signature, String timestamp, String nonce, String msg_signature) throws IOException, DocumentException, AesException {
        //服务器为加密状态时候-----start---
        WXBizMsgCrypt pc = new WXBizMsgCrypt(WechatConf.TOKEN, WechatConf.AESKEY, WechatConf.APPID);
        // 第三方收到公众号平台发送的消息
        String result2 = pc.decryptMsg(msg_signature, timestamp, nonce, requestBody);
        System.out.println("解密后明文: " + result2);
        Map<String, String> map = XmlUtil.xmlToMap(result2);
        //微信客服消息
        String text = "<xml><ToUserName><![CDATA[" + map.get("FromUserName") + "]]>" +
            "</ToUserName><FromUserName><![CDATA[" + map.get("ToUserName") + "]]>" +
            "</FromUserName><CreateTime>" + new Date().getTime() + "</CreateTime> " +
            "<MsgType><![CDATA[transfer_customer_service]]></MsgType> " +
            "<Content><![CDATA[你好]]></Content> " +
            "</xml>";
        System.out.println(text);
        String mingwen = pc.encryptMsg(text, timestamp, nonce);
        System.out.println("加密后: " + mingwen);
        return mingwen;
//        return "";
    }

}

/**
 * 对公众平台发送给公众账号的消息加解密示例代码.
 * 
 * @copyright Copyright (c) 1998-2014 Tencent Inc.
 */

// ------------------------------------------------------------------------

/**
 * 针对org.apache.commons.codec.binary.Base64,
 * 需要导入架包commons-codec-1.9(或commons-codec-1.8等其他版本)
 * 官方下载地址:http://commons.apache.org/proper/commons-codec/download_codec.cgi
 */
package com.lm.util;

import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.Random;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import org.apache.commons.codec.binary.Base64;

/**
 * 提供接收和推送给公众平台消息的加解密接口(UTF8编码的字符串).
 * <ol>
 *  <li>第三方回复加密消息给公众平台</li>
 *  <li>第三方收到公众平台发送的消息,验证消息的安全性,并对消息进行解密。</li>
 * </ol>
 * 说明:异常java.security.InvalidKeyException:illegal Key Size的解决方案
 * <ol>
 *  <li>在官方网站下载JCE无限制权限策略文件(JDK7的下载地址:
 *      http://www.oracle.com/technetwork/java/javase/downloads/jce-7-download-432124.html</li>
 *  <li>下载后解压,可以看到local_policy.jar和US_export_policy.jar以及readme.txt</li>
 *  <li>如果安装了JRE,将两个jar文件放到%JRE_HOME%\lib\security目录下覆盖原来的文件</li>
 *  <li>如果安装了JDK,将两个jar文件放到%JDK_HOME%\jre\lib\security目录下覆盖原来文件</li>
 * </ol>
 */
public class WXBizMsgCrypt {
    static Charset CHARSET = Charset.forName("utf-8");
    Base64 base64 = new Base64();
    byte[] aesKey;
    String token;
    String appId;

    /**
     * 构造函数
     * @param token 公众平台上,开发者设置的token
     * @param encodingAesKey 公众平台上,开发者设置的EncodingAESKey
     * @param appId 公众平台appid
     * 
     * @throws AesException 执行失败,请查看该异常的错误码和具体的错误信息
     */
    public WXBizMsgCrypt(String token, String encodingAesKey, String appId) throws AesException {
        if (encodingAesKey.length() != 43) {
            throw new AesException(AesException.IllegalAesKey);
        }

        this.token = token;
        this.appId = appId;
        aesKey = Base64.decodeBase64(encodingAesKey + "=");
    }

    // 生成4个字节的网络字节序
    byte[] getNetworkBytesOrder(int sourceNumber) {
        byte[] orderBytes = new byte[4];
        orderBytes[3] = (byte) (sourceNumber & 0xFF);
        orderBytes[2] = (byte) (sourceNumber >> 8 & 0xFF);
        orderBytes[1] = (byte) (sourceNumber >> 16 & 0xFF);
        orderBytes[0] = (byte) (sourceNumber >> 24 & 0xFF);
        return orderBytes;
    }

    // 还原4个字节的网络字节序
    int recoverNetworkBytesOrder(byte[] orderBytes) {
        int sourceNumber = 0;
        for (int i = 0; i < 4; i++) {
            sourceNumber <<= 8;
            sourceNumber |= orderBytes[i] & 0xff;
        }
        return sourceNumber;
    }

    // 随机生成16位字符串
    String getRandomStr() {
        String base = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
        Random random = new Random();
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < 16; i++) {
            int number = random.nextInt(base.length());
            sb.append(base.charAt(number));
        }
        return sb.toString();
    }

    /**
     * 对明文进行加密.
     * 
     * @param text 需要加密的明文
     * @return 加密后base64编码的字符串
     * @throws AesException aes加密失败
     */
    String encrypt(String randomStr, String text) throws AesException {
        ByteGroup byteCollector = new ByteGroup();
        byte[] randomStrBytes = randomStr.getBytes(CHARSET);
        byte[] textBytes = text.getBytes(CHARSET);
        byte[] networkBytesOrder = getNetworkBytesOrder(textBytes.length);
        byte[] appidBytes = appId.getBytes(CHARSET);

        // randomStr + networkBytesOrder + text + appid
        byteCollector.addBytes(randomStrBytes);
        byteCollector.addBytes(networkBytesOrder);
        byteCollector.addBytes(textBytes);
        byteCollector.addBytes(appidBytes);

        // ... + pad: 使用自定义的填充方式对明文进行补位填充
        byte[] padBytes = PKCS7Encoder.encode(byteCollector.size());
        byteCollector.addBytes(padBytes);

        // 获得最终的字节流, 未加密
        byte[] unencrypted = byteCollector.toBytes();

        try {
            // 设置加密模式为AES的CBC模式
            Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
            SecretKeySpec keySpec = new SecretKeySpec(aesKey, "AES");
            IvParameterSpec iv = new IvParameterSpec(aesKey, 0, 16);
            cipher.init(Cipher.ENCRYPT_MODE, keySpec, iv);

            // 加密
            byte[] encrypted = cipher.doFinal(unencrypted);

            // 使用BASE64对加密后的字符串进行编码
            String base64Encrypted = base64.encodeToString(encrypted);

            return base64Encrypted;
        } catch (Exception e) {
            e.printStackTrace();
            throw new AesException(AesException.EncryptAESError);
        }
    }

    /**
     * 对密文进行解密.
     * 
     * @param text 需要解密的密文
     * @return 解密得到的明文
     * @throws AesException aes解密失败
     */
    String decrypt(String text) throws AesException {
        byte[] original;
        try {
            // 设置解密模式为AES的CBC模式
            Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
            SecretKeySpec key_spec = new SecretKeySpec(aesKey, "AES");
            IvParameterSpec iv = new IvParameterSpec(Arrays.copyOfRange(aesKey, 0, 16));
            cipher.init(Cipher.DECRYPT_MODE, key_spec, iv);

            // 使用BASE64对密文进行解码
            byte[] encrypted = Base64.decodeBase64(text);

            // 解密
            original = cipher.doFinal(encrypted);
        } catch (Exception e) {
            e.printStackTrace();
            throw new AesException(AesException.DecryptAESError);
        }

        String xmlContent, from_appid;
        try {
            // 去除补位字符
            byte[] bytes = PKCS7Encoder.decode(original);

            // 分离16位随机字符串,网络字节序和AppId
            byte[] networkOrder = Arrays.copyOfRange(bytes, 16, 20);

            int xmlLength = recoverNetworkBytesOrder(networkOrder);

            xmlContent = new String(Arrays.copyOfRange(bytes, 20, 20 + xmlLength), CHARSET);
            from_appid = new String(Arrays.copyOfRange(bytes, 20 + xmlLength, bytes.length),
                    CHARSET);
        } catch (Exception e) {
            e.printStackTrace();
            throw new AesException(AesException.IllegalBuffer);
        }

        // appid不相同的情况
        if (!from_appid.equals(appId)) {
            throw new AesException(AesException.ValidateAppidError);
        }
        return xmlContent;

    }

    /**
     * 将公众平台回复用户的消息加密打包.
     * <ol>
     *  <li>对要发送的消息进行AES-CBC加密</li>
     *  <li>生成安全签名</li>
     *  <li>将消息密文和安全签名打包成xml格式</li>
     * </ol>
     * 
     * @param replyMsg 公众平台待回复用户的消息,xml格式的字符串
     * @param timeStamp 时间戳,可以自己生成,也可以用URL参数的timestamp
     * @param nonce 随机串,可以自己生成,也可以用URL参数的nonce
     * 
     * @return 加密后的可以直接回复用户的密文,包括msg_signature, timestamp, nonce, encrypt的xml格式的字符串
     * @throws AesException 执行失败,请查看该异常的错误码和具体的错误信息
     */
    public String encryptMsg(String replyMsg, String timeStamp, String nonce) throws AesException {
        // 加密
        String encrypt = encrypt(getRandomStr(), replyMsg);

        // 生成安全签名
        if (timeStamp == "") {
            timeStamp = Long.toString(System.currentTimeMillis());
        }

        String signature = SHA1.getSHA1(token, timeStamp, nonce, encrypt);

        // System.out.println("发送给平台的签名是: " + signature[1].toString());
        // 生成发送的xml
        String result = XMLParse.generate(encrypt, signature, timeStamp, nonce);
        return result;
    }

    /**
     * 检验消息的真实性,并且获取解密后的明文.
     * <ol>
     *  <li>利用收到的密文生成安全签名,进行签名验证</li>
     *  <li>若验证通过,则提取xml中的加密消息</li>
     *  <li>对消息进行解密</li>
     * </ol>
     * 
     * @param msgSignature 签名串,对应URL参数的msg_signature
     * @param timeStamp 时间戳,对应URL参数的timestamp
     * @param nonce 随机串,对应URL参数的nonce
     * @param postData 密文,对应POST请求的数据
     * 
     * @return 解密后的原文
     * @throws AesException 执行失败,请查看该异常的错误码和具体的错误信息
     */
    public String decryptMsg(String msgSignature, String timeStamp, String nonce, String postData)
            throws AesException {

        // 密钥,公众账号的app secret
        // 提取密文
        Object[] encrypt = XMLParse.extract(postData);

        // 验证安全签名
        String signature = SHA1.getSHA1(token, timeStamp, nonce, encrypt[1].toString());

        // 和URL中的签名比较是否相等
        // System.out.println("第三方收到URL中的签名:" + msg_sign);
        // System.out.println("第三方校验签名:" + signature);
        if (!signature.equals(msgSignature)) {
            throw new AesException(AesException.ValidateSignatureError);
        }

        // 解密
        String result = decrypt(encrypt[1].toString());
        return result;
    }

    /**
     * 验证URL
     * @param msgSignature 签名串,对应URL参数的msg_signature
     * @param timeStamp 时间戳,对应URL参数的timestamp
     * @param nonce 随机串,对应URL参数的nonce
     * @param echoStr 随机串,对应URL参数的echostr
     * 
     * @return 解密之后的echostr
     * @throws AesException 执行失败,请查看该异常的错误码和具体的错误信息
     */
    public String verifyUrl(String msgSignature, String timeStamp, String nonce, String echoStr)
            throws AesException {
        String signature = SHA1.getSHA1(token, timeStamp, nonce, echoStr);

        if (!signature.equals(msgSignature)) {
            throw new AesException(AesException.ValidateSignatureError);
        }

        String result = decrypt(echoStr);
        return result;
    }

}

package com.lm.util;

import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class XmlUtil {

    /**
     * xml文本转成map
     * @param str
     * @return
     */
    public static Map<String, String> xmlToMap(String str) throws DocumentException {
        Document document = DocumentHelper.parseText(str);
        Element element = document.getRootElement();
        List<Element> childrenList = element.elements();
        Map<String, String> map = new HashMap<>();
        childrenList.forEach( item -> {
            map.put(item.getName(), item.getTextTrim());
        });
        return map;
    }

}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值