新增获取微信服务器IP地址接口以及消息AES加密解密实现

This commit is contained in:
jy.hu
2014-11-15 20:45:13 +08:00
parent 717b9c7abf
commit 282699d95c
38 changed files with 893 additions and 218 deletions
@@ -8,4 +8,7 @@ public final class Consts {
public static final String PKCS12 = "PKCS12";
public static final String TLS = "TLS";
public static final String X509 = "X.509";
public static final String AES = "AES";
public static final String PROTOCOL_FILE = "file";
public static final String PROTOCOL_JAR = "jar";
}
@@ -25,6 +25,8 @@ public class WeixinAccount implements Serializable {
private String appSecret;
// 公众号支付请求中用于加密的密钥 Key,可验证商户唯一身份,PaySignKey 对应于支付场景中的 appKey 值
private String paySignKey;
// 安全模式下的加密密钥
private String encodingAesKey;
// 财付通商户身份的标识
private String partnerId;
// 财付通商户权限密钥Key
@@ -75,6 +77,14 @@ public class WeixinAccount implements Serializable {
this.appSecret = appSecret;
}
public String getEncodingAesKey() {
return encodingAesKey;
}
public void setEncodingAesKey(String encodingAesKey) {
this.encodingAesKey = encodingAesKey;
}
public String getPaySignKey() {
return paySignKey;
}
@@ -195,10 +205,10 @@ public class WeixinAccount implements Serializable {
public String toString() {
return "WeixinAccount [token=" + token + ", openId=" + openId
+ ", appId=" + appId + ", appSecret=" + appSecret
+ ", paySignKey=" + paySignKey + ", partnerId=" + partnerId
+ ", partnerKey=" + partnerKey + ", mchId=" + mchId
+ ", deviceInfo=" + deviceInfo + ", isAlive=" + isAlive
+ ", isService=" + isService + ", isSubscribe=" + isSubscribe
+ "]";
+ ", encodingAesKey=" + encodingAesKey + ", paySignKey="
+ paySignKey + ", partnerId=" + partnerId + ", partnerKey="
+ partnerKey + ", mchId=" + mchId + ", deviceInfo="
+ deviceInfo + ", isAlive=" + isAlive + ", isService="
+ isService + ", isSubscribe=" + isSubscribe + "]";
}
}
@@ -56,16 +56,6 @@ public class BaseMessage extends BaseMsg {
this.msgType = msgType;
}
public BaseMessage(MessageType msgType, BaseMessage inMessage) {
this(msgType, inMessage.getFromUserName(), inMessage.getToUserName());
}
public BaseMessage(MessageType msgType, String toUserName,
String fromUserName) {
super(toUserName, fromUserName);
this.msgType = msgType;
}
public MessageType getMsgType() {
return msgType;
}
@@ -25,11 +25,6 @@ public class TextMessage extends BaseMessage {
super(MessageType.text);
}
public TextMessage(String content, BaseMessage inMessage) {
super(MessageType.text, inMessage);
this.content = content;
}
@XStreamAlias("Content")
private String content; // 消息内容
@@ -20,7 +20,7 @@ public enum MessageType {
text(TextMessage.class), image(ImageMessage.class), voice(
VoiceMessage.class), video(VideoMessage.class), location(
LocationMessage.class), link(LinkMessage.class), event(
EventMessage.class), signature(null);
EventMessage.class);
private Class<? extends BaseMessage> messageClass;
MessageType(Class<? extends BaseMessage> messageClass) {
@@ -2,12 +2,21 @@ package com.foxinmy.weixin4j.util;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.net.JarURLConnection;
import java.net.URL;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import com.alibaba.fastjson.JSON;
import com.foxinmy.weixin4j.model.Consts;
/**
* 对class的获取
*
* @className ClassUtil
* @author jy
* @date 2014年10月31日
@@ -17,15 +26,28 @@ import java.util.Set;
public class ClassUtil {
public static Set<Class<?>> getClasses(Package _package) {
ClassLoader classLoader = Thread.currentThread()
.getContextClassLoader();
String subPath = _package.getName().replace(".", File.separator);
URL fullPath = classLoader.getResource(subPath);
File dir = new File(fullPath.getPath());
return findClasses(dir, _package.getName());
String packageName = _package.getName();
String packageFileName = packageName.replace(".", File.separator);
URL fullPath = Thread.currentThread().getContextClassLoader()
.getResource(packageFileName);
String protocol = fullPath.getProtocol();
if (protocol.equals(Consts.PROTOCOL_FILE)) {
File dir = new File(fullPath.getPath());
return findClassesByFile(dir, packageName);
} else if (protocol.equals(Consts.PROTOCOL_JAR)) {
try {
return findClassesByJar(
((JarURLConnection) fullPath.openConnection())
.getJarFile(),
packageFileName);
} catch (IOException e) {
;
}
}
return null;
}
private static Set<Class<?>> findClasses(File dir, String packageName) {
private static Set<Class<?>> findClassesByFile(File dir, String packageName) {
Set<Class<?>> classes = new HashSet<Class<?>>();
File[] files = dir.listFiles(new FilenameFilter() {
@Override
@@ -35,7 +57,7 @@ public class ClassUtil {
});
for (File file : files) {
if (file.isDirectory()) {
classes.addAll(findClasses(file,
classes.addAll(findClassesByFile(file,
packageName + "." + file.getName()));
} else {
try {
@@ -48,9 +70,43 @@ public class ClassUtil {
} catch (ClassNotFoundException e) {
;
}
}
}
return classes;
}
private static Set<Class<?>> findClassesByJar(JarFile jar,
String packageName) {
Set<Class<?>> classes = new HashSet<Class<?>>();
Enumeration<JarEntry> jarEntries = jar.entries();
while (jarEntries.hasMoreElements()) {
JarEntry jarEntry = jarEntries.nextElement();
if (jarEntry.isDirectory()) {
continue;
}
String entryName = jarEntry.getName();
if (!entryName.startsWith(packageName)) {
continue;
}
if (!entryName.endsWith(".class")) {
continue;
}
try {
Class<?> clazz = Class.forName(entryName.replaceAll("/", ".")
.replace(".class", ""));
if (clazz.isInterface()) {
continue;
}
classes.add(clazz);
} catch (ClassNotFoundException e) {
;
}
}
return classes;
}
public static void main(String[] args) {
Package _package = JSON.class.getPackage();
System.out.println(getClasses(_package));
}
}
@@ -3,15 +3,20 @@ package com.foxinmy.weixin4j.util;
import java.io.InputStream;
import java.util.Arrays;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.DocumentHelper;
import org.dom4j.io.SAXReader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.foxinmy.weixin4j.exception.WeixinException;
import com.foxinmy.weixin4j.model.Consts;
import com.foxinmy.weixin4j.msg.BaseMessage;
import com.foxinmy.weixin4j.type.EventType;
import com.foxinmy.weixin4j.type.MessageType;
@@ -19,6 +24,7 @@ import com.foxinmy.weixin4j.xml.XStream;
/**
* 消息工具类
*
* @className MessageUtil
* @author jy
* @date 2014年10月31日
@@ -27,20 +33,9 @@ import com.foxinmy.weixin4j.xml.XStream;
*/
public class MessageUtil {
private final static Logger log = LoggerFactory
.getLogger(MessageUtil.class);
/**
* 验证微信签名
*
* @param token
* 开发者填写的token
* @param echostr
* 随机字符串
* @param timestamp
* 时间戳
* @param nonce
* 随机数
* @param signature
* 微信加密签名,signature结合了开发者填写的token参数和请求中的timestamp参数、nonce参数
* @return 开发者通过检验signature对请求进行相关校验。若确认此次GET请求来自微信服务器
@@ -48,35 +43,126 @@ public class MessageUtil {
* @see <a
* href="http://mp.weixin.qq.com/wiki/index.php?title=%E6%8E%A5%E5%85%A5%E6%8C%87%E5%8D%97">接入指南</a>
*/
public static String signature(String token, String echostr,
String timestamp, String nonce, String signature) {
if (StringUtils.isBlank(token)) {
log.error("signature fail : token is null!");
return null;
public static String signature(String... para) {
Arrays.sort(para);
StringBuilder sb = new StringBuilder();
for (String str : para) {
sb.append(str);
}
if (StringUtils.isBlank(echostr) || StringUtils.isBlank(timestamp)
|| StringUtils.isBlank(nonce)) {
log.error("signature fail : invalid parameter!");
return null;
}
String _signature = null;
return DigestUtils.sha1Hex(sb.toString());
}
/**
* 对xml消息加密
*
* @param appId
* @param encodingAesKey
* 加密密钥
* @param xmlContent
* 原始消息体
* @return aes加密后的消息体
* @throws WeixinException
*/
public static String aesEncrypt(String appId, String encodingAesKey,
String xmlContent) throws WeixinException {
byte[] randomBytes = RandomUtil.generateString(16).getBytes(
org.apache.http.Consts.UTF_8);
byte[] xmlBytes = xmlContent.getBytes(org.apache.http.Consts.UTF_8);
int xmlLength = xmlBytes.length;
byte[] orderBytes = new byte[4];
orderBytes[3] = (byte) (xmlLength & 0xFF);
orderBytes[2] = (byte) (xmlLength >> 8 & 0xFF);
orderBytes[1] = (byte) (xmlLength >> 16 & 0xFF);
orderBytes[0] = (byte) (xmlLength >> 24 & 0xFF);
byte[] appidBytes = appId.getBytes(org.apache.http.Consts.UTF_8);
int byteLength = randomBytes.length + xmlLength + orderBytes.length
+ appidBytes.length;
// ... + pad: 使用自定义的填充方式对明文进行补位填充
byte[] padBytes = PKCS7Encoder.encode(byteLength);
// random + endian + xml + appid + pad 获得最终的字节流
byte[] unencrypted = new byte[byteLength + padBytes.length];
byteLength = 0;
// src:源数组;srcPos:源数组要复制的起始位置;dest:目的数组;destPos:目的数组放置的起始位置;length:复制的长度
System.arraycopy(randomBytes, 0, unencrypted, byteLength,
randomBytes.length);
byteLength += randomBytes.length;
System.arraycopy(orderBytes, 0, unencrypted, byteLength,
orderBytes.length);
byteLength += orderBytes.length;
System.arraycopy(xmlBytes, 0, unencrypted, byteLength, xmlBytes.length);
byteLength += xmlBytes.length;
System.arraycopy(appidBytes, 0, unencrypted, byteLength,
appidBytes.length);
byteLength += appidBytes.length;
System.arraycopy(padBytes, 0, unencrypted, byteLength, padBytes.length);
try {
String[] a = { token, timestamp, nonce };
Arrays.sort(a);
StringBuilder sb = new StringBuilder(3);
for (String str : a) {
sb.append(str);
}
_signature = DigestUtils.sha1Hex(sb.toString());
byte[] aesKey = Base64.decodeBase64(encodingAesKey + "=");
// 设置加密模式为AES的CBC模式
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
SecretKeySpec keySpec = new SecretKeySpec(aesKey, Consts.AES);
IvParameterSpec iv = new IvParameterSpec(aesKey, 0, 16);
cipher.init(Cipher.ENCRYPT_MODE, keySpec, iv);
// 加密
byte[] encrypted = cipher.doFinal(unencrypted);
// 使用BASE64对加密后的字符串进行编码
return Base64.encodeBase64String(encrypted);
} catch (Exception e) {
log.error("signature error", e);
throw new WeixinException("-40006", "AES加密失败");
}
if (signature.equals(_signature)) {
return echostr;
} else {
log.error("signature fail : invalid signature!");
return null;
}
/**
* 对xml消息解密
*
* @param appId
* @param encodingAesKey
* aes加密的密钥
* @param encryptContent
* 加密的消息体
* @return 解密后的xml
* @throws WeixinException
*/
public static String aesDecrypt(String appId, String encodingAesKey,
String encryptContent) throws WeixinException {
byte[] aesKey = Base64.decodeBase64(encodingAesKey + "=");
byte[] original;
try {
// 设置解密模式为AES的CBC模式
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
SecretKeySpec key_spec = new SecretKeySpec(aesKey, Consts.AES);
IvParameterSpec iv = new IvParameterSpec(Arrays.copyOfRange(aesKey,
0, 16));
cipher.init(Cipher.DECRYPT_MODE, key_spec, iv);
// 使用BASE64对密文进行解码
byte[] encrypted = Base64.decodeBase64(encryptContent);
// 解密
original = cipher.doFinal(encrypted);
} catch (Exception e) {
throw new WeixinException("-40007", "AES解密失败");
}
String xmlContent, fromAppId;
try {
// 去除补位字符
byte[] bytes = PKCS7Encoder.decode(original);
// 获取表示xml长度的字节数组
byte[] lengthByte = Arrays.copyOfRange(bytes, 16, 20);
// 获取xml消息主体的长度(byte[]2int)
// http://my.oschina.net/u/169390/blog/97495
int xmlLength = lengthByte[3] & 0xff | (lengthByte[2] & 0xff) << 8
| (lengthByte[1] & 0xff) << 16
| (lengthByte[0] & 0xff) << 24;
xmlContent = new String(Arrays.copyOfRange(bytes, 20,
20 + xmlLength), org.apache.http.Consts.UTF_8);
fromAppId = new String(Arrays.copyOfRange(bytes, 20 + xmlLength,
bytes.length), org.apache.http.Consts.UTF_8);
} catch (Exception e) {
throw new WeixinException("-40008", "公众平台发送的xml不合法");
}
// 校验appId是否一致
if (!fromAppId.trim().equals(appId)) {
throw new WeixinException("-40005", "校验AppID失败");
}
return xmlContent;
}
/**
@@ -0,0 +1,72 @@
/**
* 对公众平台发送给公众账号的消息加解密示例代码.
*
* @copyright Copyright (c) 1998-2014 Tencent Inc.
*/
// ------------------------------------------------------------------------
package com.foxinmy.weixin4j.util;
import java.util.Arrays;
import org.apache.http.Consts;
/**
* 提供基于PKCS7算法的加解密接口<br/>
* 提供接收和推送给公众平台消息的加解密接口(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>
*/
public class PKCS7Encoder {
private final static int BLOCK_SIZE = 32;
/**
* 获得对明文进行补位填充的字节.
*
* @param count
* 需要进行填充补位操作的明文字节个数
* @return 补齐用的字节数组
*/
public static byte[] encode(int count) {
// 计算需要填充的位数
int amountToPad = BLOCK_SIZE - (count % BLOCK_SIZE);
if (amountToPad == 0) {
amountToPad = BLOCK_SIZE;
}
// 获得补位所用的字符
byte target = (byte) (amountToPad & 0xFF);
char padChr = (char) target;
String tmp = new String();
for (int index = 0; index < amountToPad; index++) {
tmp += padChr;
}
return tmp.getBytes(Consts.UTF_8);
}
/**
* 删除解密后明文的补位字符
*
* @param decrypted
* 解密后的明文
* @return 删除补位字符后的明文
*/
public static byte[] decode(byte[] decrypted) {
int pad = (int) decrypted[decrypted.length - 1];
if (pad < 1 || pad > 32) {
pad = 0;
}
return Arrays.copyOfRange(decrypted, 0, decrypted.length - pad);
}
}