新增获取微信服务器IP地址接口以及消息AES加密解密实现
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user