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

如果是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;
}

我的小程序之旅十:微信公众号token验证失败的更多相关文章

  1. 微信公众号token验证失败

    我用的是python3+,而官网给的例子是python2的写法.问题就在python版本不同. 下面是截取官方的实例代码的一部分 list = [token, timestamp, nonce] li ...

  2. php:微信公众号token验证失败原因、验证码显示不出来的问题

    ob_clean(); 问题描述: 用微信官方提供的demo验证token是成功的,但是放到自己网站的框架上进行token验证老是提示"token验证失败",经过检查(用生成日志的 ...

  3. 微信公众号token验证失败的一些总结

    这几天准备弄一个微信公众号,在进行服务器配置的时候出现总是出现token验证失败的报错. 实际上,这个问题很好解决.既然微信平台没有给我们很明确的报错提示,那么我们就可以通过跟踪获取到的请求参数进行分 ...

  4. 微信公众平台Token验证失败的解决办法

    微信公众平台Token验证失败的解决办法 1.可查看url和token是否正确 2.查看服务器端口是否为80端口 3.你可以通过记录log日志来判断是否接受到微信提交过来的信息 1.$fp=fopen ...

  5. Thinkphp5 微信公众号token验证不成功的原因

    最近要启动微信项目,上个月就开始了解微信的开发,这个月要启动项目,配置微信公众号信息一直失败.为此,我甚至手工写了微信提交过来的记录,如: &timestamp=1510210523& ...

  6. 服务器通过微信公众号Token验证测试的代码(Python版)

    我在阿里云租了一个云服务器,然后想把这个作为我的微信公众号的后台,启用微信公众号开发者需要正确的响应微信服务器的Token验证,为此把这个验证的Python代码贴出来,只要在服务器上运行这段代码,注意 ...

  7. asp.net mvc 微信公众号token验证

    本人的公众号要申请成为开发者,必须经过token认证.微信公众号的官方代码只列出了PHP代码的实例,明显是歧视.net用户.我用的asp.net mvc中的web api,结果调了好久都没有成功,最后 ...

  8. 微信小程序跳转到微信公众号

    我这里是uniapp里的操作 微信开发者工具配置 微信小程序官网地址:official-account 公众号关注组件. 当用户扫小程序码打开小程序时,开发者可在小程序内配置公众号关注组件,方便用户快 ...

  9. 微信公众账号 token 验证失败 解决办法

    问题:微信公众账号 开发过程中配置  token 提示 验证失败 如下图: 点击修改配置: 填写相关url与token(自定义):点击提交,会出现 出现这种情况,主要是对相关参数不熟悉,要了解url与 ...

  10. 微信公众号token 验证

    1. 首先给出测试项目的整体目录: 2. CoreServlet类: 当get请求的时候会执行get方法,post请求的时候会执行post方法,分别来处理不同的请求 package com.zjn.s ...

随机推荐

  1. SQLServer命令行备份数据库

    SQLServer命令行备份数据库 背景 SQLServer on linux 想设置计划任务自动备份数据库 感觉sqlcmd应该是最简单快捷的方式了 所以进行一下处理. 安装必备软件 sudo cu ...

  2. Harbor修改默认网段以及设置开机启动的方法

    Harbor修改默认网段以及设置开机启动的方法 背景 docker 默认的网段是 172.16.xx.xx 网段. harbor进行设置时会自动加1 设置为 172.17.xx.xx 有时候这个网段是 ...

  3. [转帖]tikv性能参数调优

    https://www.cnblogs.com/FengGeBlog/p/10278368.html#:~:text=max-%20bytes%20-for-level-%20base%20%3D%2 ...

  4. [转帖]没 K8s 用不了 Chaos Mesh?试试 Chaosd

    https://cn.pingcap.com/blog/cannot-use-chaosmesh-without-k8s-then-try-chaosd Chaosd 是什么? 相信大家对 Chaos ...

  5. [转帖]MySQL InnoDB存储引擎大观

      https://baijiahao.baidu.com/s?id=1709263187856706948&wfr=spider&for=pc MySQL InnoDB 引擎现在广为 ...

  6. [转帖]OutOfMemory自动重启程序

    OutOfMemory以后程序已经假死,无法再提供服务,最好的做法是dump内存,发送警告,然后重启服务 我的方案:利用at命令延迟启动 但有一个问题,at最多支持分钟操作,也就是说要1分钟以后才能启 ...

  7. RabbitMQ集成系统文章02---webForm发布 ABP VNext订阅

    一.webForm项目中发布 1.引用RabbitMQ.Client 2.在你想要发布的地方调用如下的方法 public void PublishRabbitMQ() { var data = new ...

  8. 《PalWorld/幻兽帕鲁》旧电脑linux搭建服务器

    关键词: PalWorld, Linux, Natapp, 内网穿透, 幻兽帕鲁 注 意 文 章 时 效 性 最近幻兽帕鲁爆火,steam一上线好友列表一串正在游玩哈哈哈,自己也是蹭一波热度,顺便试用 ...

  9. 【三】tensorboard安装、使用教学以及遇到的问题

    相关文章: [一]tensorflow安装.常用python镜像源.tensorflow 深度学习强化学习教学 [二]tensorflow调试报错.tensorflow 深度学习强化学习教学 [三]t ...

  10. 13.4 DirectX内部劫持绘制

    相对于外部绘图技术的不稳定性,内部绘制则显得更加流程与稳定,在Dx9环境中,函数EndScene是在绘制3D场景后,用于完成将最终的图像渲染到屏幕的一系列操作的函数.它会将缓冲区中的图像清空,设置视口 ...