• Java对接微信公众号事件监听回调


    1. 公众号开启并配置

    2. Java代码实现

    1. 验证加密工具类

    1. package cn.com.baidu.wxopen.util.wx;
    2. import java.security.MessageDigest;
    3. import java.security.NoSuchAlgorithmException;
    4. import java.util.Arrays;
    5. /**
    6. * 验证工具类
    7. * 2023年10月12日09:47:04
    8. * CBC
    9. */
    10. public class WxSignUtil {
    11. /**
    12. * 校验签名
    13. * @param token 公众号定义的token
    14. * @param signature 签名
    15. * @param timestamp 时间戳
    16. * @param nonce 随机数
    17. * @return 布尔值
    18. */
    19. public static boolean checkSignature(String token, String signature, String timestamp, String nonce) {
    20. String checktext = null;
    21. if (null != signature) {
    22. // 对ToKen,timestamp,nonce 按字典排序
    23. String[] paramArr = new String[]{token, timestamp, nonce};
    24. Arrays.sort(paramArr);
    25. // 将排序后的结果拼成一个字符串
    26. String content = paramArr[0].concat(paramArr[1]).concat(paramArr[2]);
    27. try {
    28. MessageDigest md = MessageDigest.getInstance("SHA-1");
    29. // 对接后的字符串进行sha1加密
    30. byte[] digest = md.digest(content.getBytes());
    31. checktext = byteToStr(digest);
    32. } catch (NoSuchAlgorithmException e) {
    33. e.printStackTrace();
    34. }
    35. }
    36. // 将加密后的字符串与signature进行对比
    37. return checktext != null && checktext.equals(signature.toUpperCase());
    38. }
    39. /**
    40. * 将字节数组转化为16进制字符串
    41. *
    42. * @param byteArrays 字符数组
    43. * @return 字符串
    44. */
    45. private static String byteToStr(byte[] byteArrays) {
    46. String str = "";
    47. for (int i = 0; i < byteArrays.length; i++) {
    48. str += byteToHexStr(byteArrays[i]);
    49. }
    50. return str;
    51. }
    52. /**
    53. * 将字节转化为十六进制字符串
    54. *
    55. * @param myByte 字节
    56. * @return 字符串
    57. */
    58. private static String byteToHexStr(byte myByte) {
    59. char[] Digit = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
    60. char[] tampArr = new char[2];
    61. tampArr[0] = Digit[(myByte >>> 4) & 0X0F];
    62. tampArr[1] = Digit[myByte & 0X0F];
    63. String str = new String(tampArr);
    64. return str;
    65. }
    66. }

    1. /**
    2. * 对公众平台发送给公众账号的消息加解密示例代码.
    3. *
    4. * @copyright Copyright (c) 1998-2014 Tencent Inc.
    5. */
    6. // ------------------------------------------------------------------------
    7. /**
    8. * 针对org.apache.commons.codec.binary.Base64,
    9. * 需要导入架包commons-codec-1.9(或commons-codec-1.8等其他版本)
    10. * 官方下载地址:http://commons.apache.org/proper/commons-codec/download_codec.cgi
    11. */
    12. package cn.com.baidu.wxopen.util.wx;
    13. import org.apache.commons.codec.binary.Base64;
    14. import javax.crypto.Cipher;
    15. import javax.crypto.spec.IvParameterSpec;
    16. import javax.crypto.spec.SecretKeySpec;
    17. import java.nio.charset.Charset;
    18. import java.util.Arrays;
    19. import java.util.Random;
    20. /**
    21. * 提供接收和推送给公众平台消息的加解密接口(UTF8编码的字符串).
    22. *
      1. *
      2. 第三方回复加密消息给公众平台
    23. *
    24. 第三方收到公众平台发送的消息,验证消息的安全性,并对消息进行解密。
  • *
  • * 说明:异常java.security.InvalidKeyException:illegal Key Size的解决方案
  • *
    1. *
    2. 在官方网站下载JCE无限制权限策略文件(JDK7的下载地址:
    3. * http://www.oracle.com/technetwork/java/javase/downloads/jce-7-download-432124.html
    4. *
    5. 下载后解压,可以看到local_policy.jar和US_export_policy.jar以及readme.txt
    6. *
    7. 如果安装了JRE,将两个jar文件放到%JRE_HOME%\lib\security目录下覆盖原来的文件
    8. *
    9. 如果安装了JDK,将两个jar文件放到%JDK_HOME%\jre\lib\security目录下覆盖原来文件
    10. *
    11. */
    12. public class WXBizMsgCrypt {
    13. static Charset CHARSET = Charset.forName("utf-8");
    14. Base64 base64 = new Base64();
    15. byte[] aesKey;
    16. String token;
    17. String appId;
    18. /**
    19. * 构造函数
    20. * @param token 公众平台上,开发者设置的token
    21. * @param encodingAesKey 公众平台上,开发者设置的EncodingAESKey
    22. * @param appId 公众平台appid
    23. *
    24. * @throws AesException 执行失败,请查看该异常的错误码和具体的错误信息
    25. */
    26. public WXBizMsgCrypt(String token, String encodingAesKey, String appId) throws AesException {
    27. if (encodingAesKey.length() != 43) {
    28. throw new AesException(AesException.IllegalAesKey);
    29. }
    30. this.token = token;
    31. this.appId = appId;
    32. aesKey = Base64.decodeBase64(encodingAesKey + "=");
    33. }
    34. // 生成4个字节的网络字节序
    35. byte[] getNetworkBytesOrder(int sourceNumber) {
    36. byte[] orderBytes = new byte[4];
    37. orderBytes[3] = (byte) (sourceNumber & 0xFF);
    38. orderBytes[2] = (byte) (sourceNumber >> 8 & 0xFF);
    39. orderBytes[1] = (byte) (sourceNumber >> 16 & 0xFF);
    40. orderBytes[0] = (byte) (sourceNumber >> 24 & 0xFF);
    41. return orderBytes;
    42. }
    43. // 还原4个字节的网络字节序
    44. int recoverNetworkBytesOrder(byte[] orderBytes) {
    45. int sourceNumber = 0;
    46. for (int i = 0; i < 4; i++) {
    47. sourceNumber <<= 8;
    48. sourceNumber |= orderBytes[i] & 0xff;
    49. }
    50. return sourceNumber;
    51. }
    52. // 随机生成16位字符串
    53. String getRandomStr() {
    54. String base = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
    55. Random random = new Random();
    56. StringBuffer sb = new StringBuffer();
    57. for (int i = 0; i < 16; i++) {
    58. int number = random.nextInt(base.length());
    59. sb.append(base.charAt(number));
    60. }
    61. return sb.toString();
    62. }
    63. /**
    64. * 对明文进行加密.
    65. *
    66. * @param text 需要加密的明文
    67. * @return 加密后base64编码的字符串
    68. * @throws AesException aes加密失败
    69. */
    70. String encrypt(String randomStr, String text) throws AesException {
    71. ByteGroup byteCollector = new ByteGroup();
    72. byte[] randomStrBytes = randomStr.getBytes(CHARSET);
    73. byte[] textBytes = text.getBytes(CHARSET);
    74. byte[] networkBytesOrder = getNetworkBytesOrder(textBytes.length);
    75. byte[] appidBytes = appId.getBytes(CHARSET);
    76. // randomStr + networkBytesOrder + text + appid
    77. byteCollector.addBytes(randomStrBytes);
    78. byteCollector.addBytes(networkBytesOrder);
    79. byteCollector.addBytes(textBytes);
    80. byteCollector.addBytes(appidBytes);
    81. // ... + pad: 使用自定义的填充方式对明文进行补位填充
    82. byte[] padBytes = PKCS7Encoder.encode(byteCollector.size());
    83. byteCollector.addBytes(padBytes);
    84. // 获得最终的字节流, 未加密
    85. byte[] unencrypted = byteCollector.toBytes();
    86. try {
    87. // 设置加密模式为AES的CBC模式
    88. Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
    89. SecretKeySpec keySpec = new SecretKeySpec(aesKey, "AES");
    90. IvParameterSpec iv = new IvParameterSpec(aesKey, 0, 16);
    91. cipher.init(Cipher.ENCRYPT_MODE, keySpec, iv);
    92. // 加密
    93. byte[] encrypted = cipher.doFinal(unencrypted);
    94. // 使用BASE64对加密后的字符串进行编码
    95. String base64Encrypted = base64.encodeToString(encrypted);
    96. return base64Encrypted;
    97. } catch (Exception e) {
    98. e.printStackTrace();
    99. throw new AesException(AesException.EncryptAESError);
    100. }
    101. }
    102. /**
    103. * 对密文进行解密.
    104. *
    105. * @param text 需要解密的密文
    106. * @return 解密得到的明文
    107. * @throws AesException aes解密失败
    108. */
    109. String decrypt(String text) throws AesException {
    110. byte[] original;
    111. try {
    112. // 设置解密模式为AES的CBC模式
    113. Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
    114. SecretKeySpec key_spec = new SecretKeySpec(aesKey, "AES");
    115. IvParameterSpec iv = new IvParameterSpec(Arrays.copyOfRange(aesKey, 0, 16));
    116. cipher.init(Cipher.DECRYPT_MODE, key_spec, iv);
    117. // 使用BASE64对密文进行解码
    118. byte[] encrypted = Base64.decodeBase64(text);
    119. // 解密
    120. original = cipher.doFinal(encrypted);
    121. } catch (Exception e) {
    122. e.printStackTrace();
    123. throw new AesException(AesException.DecryptAESError);
    124. }
    125. String xmlContent, from_appid;
    126. try {
    127. // 去除补位字符
    128. byte[] bytes = PKCS7Encoder.decode(original);
    129. // 分离16位随机字符串,网络字节序和AppId
    130. byte[] networkOrder = Arrays.copyOfRange(bytes, 16, 20);
    131. int xmlLength = recoverNetworkBytesOrder(networkOrder);
    132. xmlContent = new String(Arrays.copyOfRange(bytes, 20, 20 + xmlLength), CHARSET);
    133. from_appid = new String(Arrays.copyOfRange(bytes, 20 + xmlLength, bytes.length),
    134. CHARSET);
    135. } catch (Exception e) {
    136. e.printStackTrace();
    137. throw new AesException(AesException.IllegalBuffer);
    138. }
    139. // appid不相同的情况
    140. if (!from_appid.equals(appId)) {
    141. throw new AesException(AesException.ValidateAppidError);
    142. }
    143. return xmlContent;
    144. }
    145. /**
    146. * 将公众平台回复用户的消息加密打包.
    147. *
      1. *
      2. 对要发送的消息进行AES-CBC加密
      3. *
      4. 生成安全签名
      5. *
      6. 将消息密文和安全签名打包成xml格式
      7. *
      8. *
      9. * @param replyMsg 公众平台待回复用户的消息,xml格式的字符串
      10. * @param timeStamp 时间戳,可以自己生成,也可以用URL参数的timestamp
      11. * @param nonce 随机串,可以自己生成,也可以用URL参数的nonce
      12. *
      13. * @return 加密后的可以直接回复用户的密文,包括msg_signature, timestamp, nonce, encrypt的xml格式的字符串
      14. * @throws AesException 执行失败,请查看该异常的错误码和具体的错误信息
      15. */
      16. public String encryptMsg(String replyMsg, String timeStamp, String nonce) throws AesException {
      17. // 加密
      18. String encrypt = encrypt(getRandomStr(), replyMsg);
      19. // 生成安全签名
      20. if (timeStamp == "") {
      21. timeStamp = Long.toString(System.currentTimeMillis());
      22. }
      23. String signature = SHA1.getSHA1(token, timeStamp, nonce, encrypt);
      24. // System.out.println("发送给平台的签名是: " + signature[1].toString());
      25. // 生成发送的xml
      26. String result = XMLParse.generate(encrypt, signature, timeStamp, nonce);
      27. return result;
      28. }
      29. /**
      30. * 检验消息的真实性,并且获取解密后的明文.
      31. *
        1. *
        2. 利用收到的密文生成安全签名,进行签名验证
        3. *
        4. 若验证通过,则提取xml中的加密消息
        5. *
        6. 对消息进行解密
        7. *
        8. *
        9. * @param msgSignature 签名串,对应URL参数的msg_signature
        10. * @param timeStamp 时间戳,对应URL参数的timestamp
        11. * @param nonce 随机串,对应URL参数的nonce
        12. * @param postData 密文,对应POST请求的数据
        13. *
        14. * @return 解密后的原文
        15. * @throws AesException 执行失败,请查看该异常的错误码和具体的错误信息
        16. */
        17. public String decryptMsg(String msgSignature, String timeStamp, String nonce, String postData)
        18. throws AesException {
        19. // 密钥,公众账号的app secret
        20. // 提取密文
        21. Object[] encrypt = XMLParse.extract(postData);
        22. // 验证安全签名
        23. String signature = SHA1.getSHA1(token, timeStamp, nonce, encrypt[1].toString());
        24. // 和URL中的签名比较是否相等
        25. // System.out.println("第三方收到URL中的签名:" + msg_sign);
        26. // System.out.println("第三方校验签名:" + signature);
        27. if (!signature.equals(msgSignature)) {
        28. throw new AesException(AesException.ValidateSignatureError);
        29. }
        30. // 解密
        31. String result = decrypt(encrypt[1].toString());
        32. return result;
        33. }
        34. /**
        35. * 验证URL
        36. * @param msgSignature 签名串,对应URL参数的msg_signature
        37. * @param timeStamp 时间戳,对应URL参数的timestamp
        38. * @param nonce 随机串,对应URL参数的nonce
        39. * @param echoStr 随机串,对应URL参数的echostr
        40. *
        41. * @return 解密之后的echostr
        42. * @throws AesException 执行失败,请查看该异常的错误码和具体的错误信息
        43. */
        44. public String verifyUrl(String msgSignature, String timeStamp, String nonce, String echoStr)
        45. throws AesException {
        46. String signature = SHA1.getSHA1(token, timeStamp, nonce, echoStr);
        47. if (!signature.equals(msgSignature)) {
        48. throw new AesException(AesException.ValidateSignatureError);
        49. }
        50. String result = decrypt(echoStr);
        51. return result;
        52. }
        53. }
        54. 2. 启用时的验证接口

          1. /**
          2. * 公共户服务器配置在点击启用后会请求该验证接口
          3. * @param request
          4. * @param response
          5. * @throws IOException
          6. */
          7. @GetMapping("auth" + ISystemConstant.RELEASE_SUFFIX )
          8. public void authGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
          9. if (StringUtils.isNotBlank(request.getParameter("signature"))) {
          10. String signature = request.getParameter("signature");
          11. String timestamp = request.getParameter("timestamp");
          12. String nonce = request.getParameter("nonce");
          13. String echostr = request.getParameter("echostr");
          14. if (WxSignUtil.checkSignature('你的Token', signature, timestamp, nonce)) {
          15. response.getOutputStream().println(echostr);
          16. }
          17. }
          18. }

          3. 公众号事件触发接口

          1. /**
          2. * 公众号事件触发接口
          3. * 消息封装参考官方文档 https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Receiving_standard_messages.html
          4. * @param request
          5. * @param response
          6. * @param pw
          7. * @throws Exception
          8. */
          9. @PostMapping("auth" + ISystemConstant.RELEASE_SUFFIX )
          10. public void authPost(HttpServletRequest request, HttpServletResponse response, PrintWriter pw) throws Exception{
          11. WXBizMsgCrypt pc = new WXBizMsgCrypt("你的Token", "你的AesKey", "你的AppId");
          12. String timestamp = request.getParameter("timestamp");
          13. String nonce = request.getParameter("nonce");
          14. String msgSignature = request.getParameter("msg_signature");
          15. Document doc = getDocument(request);
          16. String result2 = pc.decryptMsg(msgSignature, timestamp, nonce, doc.asXML());
          17. Map map = docToMap(DocumentHelper.parseText(result2));
          18. System.out.println("解密后明文: " + map);
          19. String messageType = map.get("MsgType"); //这个值如果是event表示是事件推送
          20. if(Objects.equals("event", messageType)) {
          21. String event = map.get("Event"); //这个是事件的具体类型
          22. if(Objects.equals(event, "subscribe")) { //公众号订阅
          23. String eventKey = map.get("EventKey"); //如果是扫码关注,同时二维码有参数,会有这个值,qrscene_开头
          24. String qrCodeParams = null;
          25. if(eventKey != null && eventKey.length() > 0) {
          26. System.out.println("二维码参数:" + eventKey.replaceFirst("qrscene_", ""));
          27. qrCodeParams = eventKey.replaceFirst("qrscene_", "");
          28. }
          29. String message = "欢迎您的关注";
          30. String openId = map.get("FromUserName");
          31. String result = "" + " + map.get("FromUserName") + "]]>"
          32. + " + map.get("ToUserName") + "]]>" + ""
          33. + System.currentTimeMillis() + "" + ""
          34. + " + message + "]]>";
          35. result = pc.encryptMsg(result, timestamp, nonce);
          36. pw.write(result);
          37. // 如果是扫码登录,可以根据携带的参数与openId自己再做一些业务处理
          38. if(!StringUtils.isEmpty(qrCodeParams)) {
          39. }
          40. }
          41. if(Objects.equals(event, "unsubscribe")) { //公众号订取消阅
          42. String openId = map.get("FromUserName");
          43. System.out.println(openId + "取消了订阅");
          44. pw.write(nonce);
          45. }
          46. if(Objects.equals(event, "SCAN")) { //已关注的用户扫码
          47. String eventKey = map.get("EventKey"); //qrscene_开头
          48. String qrCodeParams = null;
          49. if(eventKey != null && eventKey.length() > 0) {
          50. System.out.println("二维码参数:" + eventKey.replaceFirst("qrscene_", ""));
          51. qrCodeParams = eventKey.replaceFirst("qrscene_", "");
          52. }
          53. pw.write("");
          54. // 如果是扫码登录,可以根据携带的参数与openId自己再做一些业务处理
          55. if(!StringUtils.isEmpty(qrCodeParams)) {
          56. }
          57. }
          58. if(Objects.equals(event, "LOCATION")) { //上报地理位置事件
          59. String latitude = map.get("Latitude"); //纬度
          60. String longitude = map.get("Longitude"); //经度
          61. String precision = map.get("Precision"); //定位精度
          62. System.out.println(openId + "上报了位置:" + latitude + "," + longitude + "," + precision);
          63. pw.write("");
          64. }
          65. }else {
          66. String msgType = map.get("MsgType"); //消息类型
          67. if(msgType != null && msgType.equals("text")) { //文本内容
          68. String content = map.get("Content");
          69. // 自己定义 根据用户发送内容回复不同信息的对象
          70. WxopenKeywordsDTO keywordsDTO = iWxopenKeywordsService.getQuery(content);
          71. String result = null;
          72. if(keywordsDTO != null) {
          73. if (keywordsDTO.getKeywordsResultType().equals("1")) {// getKeywordsResultType为我自己定义的类型1=回复文本,2=图片,6=图文
          74. result = getXmlReturnMsg(map.get("FromUserName"), map.get("ToUserName"), keywordsDTO.getKeywordsResult());
          75. }
          76. if (keywordsDTO.getKeywordsResultType().equals("2")) {
          77. result = getXmlReturnImageMsg(map.get("FromUserName"), map.get("ToUserName"), keywordsDTO.getKeywordsResultMediaId());
          78. }
          79. if (keywordsDTO.getKeywordsResultType().equals("6")) {
          80. result = getXmlReturnImageMsg(map.get("FromUserName"), map.get("ToUserName"), keywordsDTO.getKeywordsTitle(), keywordsDTO.getKeywordsResult(), keywordsDTO.getKeywordsResultMediaUrl(), keywordsDTO.getKeywordsResultUrl());
          81. }
          82. }
          83. if(result != null) {
          84. result = pc.encryptMsg(result, timestamp, nonce);
          85. response.setHeader("Content-type", "application/xml");
          86. response.setCharacterEncoding("UTF-8");
          87. response.getWriter().write(result);
          88. }
          89. }
          90. }
          91. }
          92. /**
          93. * 构建普通消息
          94. * @param toUser 接收方账号(openId)
          95. * @param fromUser 开发者账号
          96. * @param content 内容
          97. * @return 回复消息
          98. */
          99. public String getXmlReturnMsg(String toUser, String fromUser, String content) {
          100. return "\n" +
          101. " +toUser+"]]>\n" +
          102. " +fromUser+"]]>\n" +
          103. " "+System.currentTimeMillis()+"\n" +
          104. " \n" +
          105. " +content+"]]>\n" +
          106. "";
          107. }
          108. /**
          109. * 构建图文消息
          110. * @param toUser 接收方账号(openId)
          111. * @param fromUser 开发者账号
          112. * @param mediaId 图片
          113. * @return 回复消息
          114. */
          115. public String getXmlReturnImageMsg(String toUser, String fromUser, String title, String content, String mediaId, String url) {
          116. return "\n" +
          117. " +toUser+"]]>\n" +
          118. " +fromUser+"]]>\n" +
          119. " "+System.currentTimeMillis()+"\n" +
          120. " \n" +
          121. " 1\n" +
          122. " \n" +
          123. " \n" +
          124. " <![CDATA["</span> + title + <span class="hljs-string">"]]>\n" +
          125. " + content + "]]>\n" +
          126. " + mediaId + "]]>\n" +
          127. " + url + "]]>\n" +
          128. " \n" +
          129. " "+
          130. "";
          131. }
          132. /**
          133. * 构建图片消息
          134. * @param toUser 接收方账号(openId)
          135. * @param fromUser 开发者账号
          136. * @param mediaId 图片
          137. * @return 回复消息
          138. */
          139. public String getXmlReturnImageMsg(String toUser, String fromUser, String mediaId) {
          140. return "\n" +
          141. " +toUser+"]]>\n" +
          142. " +fromUser+"]]>\n" +
          143. " "+System.currentTimeMillis()+"\n" +
          144. " \n" +
          145. " \n" +
          146. " + mediaId + "]]>\n" +
          147. " \n" +
          148. "";
          149. }
          150. public static Map docToMap(Document doc) {
          151. Map map = new HashMap();
          152. Element root = doc.getRootElement();
          153. @SuppressWarnings("unchecked")
          154. List list = root.elements();
          155. for (Element element : list) {
          156. map.put(element.getName(), element.getText());
          157. }
          158. return map;
          159. }
          160. public static Document getDocument(HttpServletRequest request) {
          161. SAXReader reader = new SAXReader();
          162. try {
          163. InputStream ins = request.getInputStream();
          164. Document doc = reader.read(ins);
          165. return doc;
          166. } catch (Exception e) {
          167. e.printStackTrace();
          168. }
          169. return null;
          170. }

        55. 相关阅读:
          神经网络研究主要内容,神经网络的起源和发展
          中文语法纠错开源大模型
          自动驾驶中的数据安全和隐私
          论文笔记:一分类及其在大数据中的潜在应用综述
          详解--Hash(中文也称散列、哈希)
          剑指 Offer II 079+080+081+082
          Eureka细节详解
          「动态规划」如何求乘积最大子数组?
          Visual Studio Code 1.73发布!
          推荐系统实践读书笔记-03冷启动问题
        56. 原文地址:https://blog.csdn.net/dongyan3595/article/details/133783790