• 我的小程序之旅十:微信公众号token验证失败


    为了更好的运营公众号,微信官方支持用户自定义实现公众号功能,这里第一步就是配置服务器回调域名,如下图:

    如果是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;
    }        
  • 相关阅读:
    自制电脑红外遥控接收器(PC软解码) 转
    .NET Micro Framework介绍
    如何测试移动web?
    自行开发高效精简的二进制序列化库(支持精简框架集) 转
    35个优秀的电子商务网站界面
    .Net Micro Framework中的线程
    《肖申克的救赎》 阅后小记
    分享 MSDN 下载工具(Word/PDF)
    OEA ORM中的分页支持
    OEA 中的多国语言实现
  • 原文地址:https://www.cnblogs.com/wlovet/p/16048566.html
Copyright © 2020-2023  润新知