为了更好的运营公众号,微信官方支持用户自定义实现公众号功能,这里第一步就是配置服务器回调域名,如下图:
如果是SpringBoot项目,我们会写一个如下的Controller类
import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ResponseBody; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Controller @Slf4j public class WxGzhEventController1 { private final static String gzhToken = "公众号那边自定义的令牌(Token)"; /*** * 微信服务器触发get请求用于检测签名 * @return */ @SneakyThrows @GetMapping(value = "/callback/handleWxCheckSignature") @ResponseBody public String handleWxCheckSignature(HttpServletRequest request, HttpServletResponse response) { //微信加密签名 String signature = request.getParameter("signature"); //时间戳 String timestamp = request.getParameter("timestamp"); //随机数 String nonce = request.getParameter("nonce"); //随机字符串 String echostr = request.getParameter("echostr"); //接入验证 if (WXBizMsgCrypt.checkSignature(signature, timestamp, nonce, gzhToken)) { log.info("微信公众号校验完成echostr:[{}]", echostr);
return echostr; } throw new RuntimeException("解析签名发生异常"); }
import lombok.extern.slf4j.Slf4j; import me.chanjar.weixin.common.util.crypto.PKCS7Encoder; import org.apache.commons.codec.binary.Base64; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NodeList; import org.xml.sax.InputSource; import javax.crypto.Cipher; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import java.io.StringReader; import java.nio.charset.Charset; import java.security.MessageDigest; import java.util.Arrays; /** * 提供接收和推送给公众平台消息的加解密接口(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> */ @Slf4j 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 WxAesException 执行失败,请查看该异常的错误码和具体的错误信息 */ public WXBizMsgCrypt(String token, String encodingAesKey, String appId) throws WxAesException { if (encodingAesKey.length() != 43) { throw new WxAesException(WxAesException.IllegalAesKey); } this.token = token; this.appId = appId; aesKey = Base64.decodeBase64(encodingAesKey + "="); } // 还原4个字节的网络字节序 int recoverNetworkBytesOrder(byte[] orderBytes) { int sourceNumber = 0; for (int i = 0; i < 4; i++) { sourceNumber <<= 8; sourceNumber |= orderBytes[i] & 0xff; } return sourceNumber; } /** * 对密文进行解密. * * @param text 需要解密的密文 * @return 解密得到的明文 * @throws WxAesException aes解密失败 */ String decrypt(String text) throws WxAesException { 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 WxAesException(WxAesException.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 WxAesException(WxAesException.IllegalBuffer); } // appid不相同的情况 if (!from_appid.equals(appId)) { throw new WxAesException(WxAesException.ValidateSignatureError); } return xmlContent; } /** * * 检验消息的真实性,并且获取解密后的明文. * <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 WxAesException 执行失败,请查看该异常的错误码和具体的错误信息 */ public String decryptMsg(String msgSignature, String timeStamp, String nonce, String postData) throws WxAesException { // 密钥,公众账号的app secret // 提取密文 Object[] encrypt = extract(postData); // 验证安全签名 String signature = getSHA1(token, timeStamp, nonce, encrypt[1].toString()); // 和URL中的签名比较是否相等 // System.out.println("第三方收到URL中的签名:" + msg_sign); // System.out.println("第三方校验签名:" + signature); if (!signature.equals(msgSignature)) { throw new WxAesException(WxAesException.ValidateSignatureError); } // 解密 String result = decrypt(encrypt[1].toString()); return result; } /** * 提取出xml数据包中的加密消息 * * @param xmltext 待提取的xml字符串 * @return 提取出的加密消息字符串 * @throws WxAesException */ public static Object[] extract(String xmltext) throws WxAesException { Object[] result = new Object[3]; try { DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); dbf.setFeature("http://xml.org/sax/features/external-general-entities", false); dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false); dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); dbf.setXIncludeAware(false); dbf.setExpandEntityReferences(false); DocumentBuilder db = dbf.newDocumentBuilder(); StringReader sr = new StringReader(xmltext); InputSource is = new InputSource(sr); Document document = db.parse(is); Element root = document.getDocumentElement(); NodeList nodelist1 = root.getElementsByTagName("Encrypt"); NodeList nodelist2 = root.getElementsByTagName("ToUserName"); result[0] = 0; result[1] = nodelist1.item(0).getTextContent(); //注意这里,获取ticket中的xml里面没有ToUserName这个元素,官网原示例代码在这里会报空 //空指针,所以需要处理一下 if (nodelist2 != null) { if (nodelist2.item(0) != null) { result[2] = nodelist2.item(0).getTextContent(); } } return result; } catch (Exception e) { e.printStackTrace(); throw new WxAesException(WxAesException.ParseXmlError); } } /** * 用SHA1算法生成安全签名 * * @param token 票据 * @param timestamp 时间戳 * @param nonce 随机字符串 * @param encrypt 密文 * @return 安全签名 * @throws WxAesException */ public static String getSHA1(String token, String timestamp, String nonce, String encrypt) throws WxAesException { 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) { throw new WxAesException(WxAesException.ComputeSignatureError); } } /** * 校验签名 * * @param signature 签名 * @param timestamp 时间戳 * @param nonce 随机数 * @return 布尔值 */ public static boolean checkSignature(String signature, String timestamp, String nonce, String token) { String checkText = null; if (null != signature) { //对ToKen,timestamp,nonce 按字典排序 String[] paramArr = new String[]{token, timestamp, nonce}; Arrays.sort(paramArr); //将排序后的结果拼成一个字符串 String content = paramArr[0].concat(paramArr[1]).concat(paramArr[2]); try { MessageDigest md = MessageDigest.getInstance("SHA-1"); //对接后的字符串进行sha1加密 byte[] digest = md.digest(content.toString().getBytes()); checkText = byteToStr(digest); } catch (Exception e) { log.error("解码发生异常", e); } } //将加密后的字符串与signature进行对比 return checkText != null ? checkText.equals(signature.toUpperCase()) : false; } /** * 将字节数组转化我16进制字符串 * @param byteArrays 字符数组 * @return 字符串 */ private static String byteToStr(byte[] byteArrays){ String str = ""; for (int i = 0; i < byteArrays.length; i++) { str += byteToHexStr(byteArrays[i]); } return str; } /** * 将字节转化为十六进制字符串 * @param myByte 字节 * @return 字符串 */ private static String byteToHexStr(byte myByte) { char[] Digit = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; char[] tampArr = new char[2]; tampArr[0] = Digit[(myByte >>> 4) & 0X0F]; tampArr[1] = Digit[myByte & 0X0F]; String str = new String(tampArr); return str; } }
@SuppressWarnings("serial") public class WxAesException extends Exception { public final static int OK = 0; public final static int ValidateSignatureError = -40001; public final static int ParseXmlError = -40002; public final static int ComputeSignatureError = -40003; public final static int IllegalAesKey = -40004; public final static int ValidateCorpidError = -40005; public final static int EncryptAESError = -40006; public final static int DecryptAESError = -40007; public final static int IllegalBuffer = -40008; //public final static int EncodeBase64Error = -40009; //public final static int DecodeBase64Error = -40010; //public final static int GenReturnXmlError = -40011; private int code; private static String getMessage(int code) { switch (code) { case ValidateSignatureError: return "签名验证错误"; case ParseXmlError: return "xml解析失败"; case ComputeSignatureError: return "sha加密生成签名失败"; case IllegalAesKey: return "SymmetricKey非法"; case ValidateCorpidError: return "corpid校验失败"; case EncryptAESError: return "aes加密失败"; case DecryptAESError: return "aes解密失败"; case IllegalBuffer: return "解密后得到的buffer非法"; // case EncodeBase64Error: // return "base64加密错误"; // case DecodeBase64Error: // return "base64解密错误"; // case GenReturnXmlError: // return "xml生成失败"; default: return null; // cannot be } } public int getCode() { return code; } WxAesException(int code) { super(getMessage(code)); this.code = code; } }
正常情况下,这样写是没有问题的。但是很多时候我们都会使用FastJSON,还会加一个FastJsonHttpMessageConverter用来格式化返回的json数据。
加FastJsonHttpMessageConverter本意上是好的,但是这玩意会在我们返回的字符串上加上双引号,但是又不能不加。
网上解决这个问题,一般都是加一个StringHttpMessageConverter,但是我发现加上之后双引号确实没了,但是不知道为啥还是不行。
所以最好的方式是不要直接返回String类型,直接使用response.getWriter().print(echostr); 这样返回的值才不会被我们的工具类处理。
if (WXBizMsgCrypt.checkSignature(signature, timestamp, nonce, gzhToken)) { log.info("微信公众号校验完成echostr:[{}]", echostr); try { response.getWriter().print(echostr); } catch (IOException e) { log.error("输出返回值异常", e); } return; }