微信小程序生态1-初识小程序
微信小程序生态2-创建一个微信小程序
微信小程序生态3-微信小程序登录流程设计
微信小程序生态4-扫普通二维码进入小程序、打开短链接进入小程序
微信小程序生态5-微信公众号扫码登录PC端网页
微信小程序生态6-微信公众号授权登录(适用于H5小程序)
微信小程序生态7-微信公众号设置IP白名单
微信小程序生态8-基于weixin-java-mp实现微信公众号被动回复消息
微信小程序生态9-微信开放平台unionId机制介绍
微信小程序生态10-微信公众号token验证失败
微信小程序生态11-一个二维码同时支持微信、钉钉、支付宝扫码打开小程序
微信小程序生态12-微信小程序开发设置中服务器域名和业务域名
微信小程序生态13-微信公众号自定义菜单、个性化菜单配置
微信小程序生态14-下线已发布的小程序
微信小程序生态15- 批量提交微信小程序审核的一种方式
为了更好的运营公众号,微信官方支持用户自定义实现公众号功能,这里第一步就是配置服务器回调域名,如下图:
如果是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编码的字符串).
* * - 第三方回复加密消息给公众平台
* - 第三方收到公众平台发送的消息,验证消息的安全性,并对消息进行解密。
*
* 说明:异常java.security.InvalidKeyException:illegal Key Size的解决方案
*
* - 在官方网站下载JCE无限制权限策略文件(JDK7的下载地址: *
* http://www.oracle.com/technetwork/java/javase/downloads/jce-7-download-432124.html
* - 下载后解压,可以看到local_policy.jar和US_export_policy.jar以及readme.txt
* - 如果安装了JRE,将两个jar文件放到%JRE_HOME%\lib\security目录下覆盖原来的文件
* - 如果安装了JDK,将两个jar文件放到%JDK_HOME%\jre\lib\security目录下覆盖原来文件
*
*
*/
@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;
}
/**
* * 检验消息的真实性,并且获取解密后的明文.
*
* - 利用收到的密文生成安全签名,进行签名验证
* - 若验证通过,则提取xml中的加密消息
* - 对消息进行解密
*
*
* @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;
}