微信小程序生态10-微信公众号token验证失败

文章导航

微信小程序生态1-初识小程序
微信小程序生态2-创建一个微信小程序
微信小程序生态3-微信小程序登录流程设计
微信小程序生态4-扫普通二维码进入小程序、打开短链接进入小程序
微信小程序生态5-微信公众号扫码登录PC端网页
微信小程序生态6-微信公众号授权登录(适用于H5小程序)
微信小程序生态7-微信公众号设置IP白名单
微信小程序生态8-基于weixin-java-mp实现微信公众号被动回复消息
微信小程序生态9-微信开放平台unionId机制介绍
微信小程序生态10-微信公众号token验证失败
微信小程序生态11-一个二维码同时支持微信、钉钉、支付宝扫码打开小程序
微信小程序生态12-微信小程序开发设置中服务器域名和业务域名
微信小程序生态13-微信公众号自定义菜单、个性化菜单配置
微信小程序生态14-下线已发布的小程序
微信小程序生态15- 批量提交微信小程序审核的一种方式

正文

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

微信小程序生态10-微信公众号token验证失败_第1张图片

如果是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编码的字符串).
 * 
    *
  1. 第三方回复加密消息给公众平台
  2. *
  3. 第三方收到公众平台发送的消息,验证消息的安全性,并对消息进行解密。
  4. *
* 说明:异常java.security.InvalidKeyException:illegal Key Size的解决方案 *
    *
  1. 在官方网站下载JCE无限制权限策略文件(JDK7的下载地址: * * http://www.oracle.com/technetwork/java/javase/downloads/jce-7-download-432124.html
  2. *
  3. 下载后解压,可以看到local_policy.jar和US_export_policy.jar以及readme.txt
  4. *
  5. 如果安装了JRE,将两个jar文件放到%JRE_HOME%\lib\security目录下覆盖原来的文件
  6. *
  7. 如果安装了JDK,将两个jar文件放到%JDK_HOME%\jre\lib\security目录下覆盖原来文件
  8. * *
*/
@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; } /** * * 检验消息的真实性,并且获取解密后的明文. *
    *
  1. 利用收到的密文生成安全签名,进行签名验证
  2. *
  3. 若验证通过,则提取xml中的加密消息
  4. *
  5. 对消息进行解密
  6. *
* * @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;
}        

你可能感兴趣的:(小程序生态,微信,java,spring,boot)