diff --git a/src/main/java/me/chanjar/weixin/api/WxConfigStorage.java b/src/main/java/me/chanjar/weixin/api/WxConfigStorage.java
index 105b965b..57bdcd3b 100644
--- a/src/main/java/me/chanjar/weixin/api/WxConfigStorage.java
+++ b/src/main/java/me/chanjar/weixin/api/WxConfigStorage.java
@@ -20,7 +20,9 @@ public interface WxConfigStorage {
public String getSecret();
public String getToken();
-
+
+ public String getAesKey();
+
public int getExpiresIn();
-
+
}
diff --git a/src/main/java/me/chanjar/weixin/api/WxInMemoryConfigStorage.java b/src/main/java/me/chanjar/weixin/api/WxInMemoryConfigStorage.java
index 783da6c4..1fe66b38 100644
--- a/src/main/java/me/chanjar/weixin/api/WxInMemoryConfigStorage.java
+++ b/src/main/java/me/chanjar/weixin/api/WxInMemoryConfigStorage.java
@@ -13,6 +13,7 @@ public class WxInMemoryConfigStorage implements WxConfigStorage {
protected String secret;
protected String token;
protected String accessToken;
+ protected String aesKey;
protected int expiresIn;
public void updateAccessToken(WxAccessToken accessToken) {
@@ -56,6 +57,14 @@ public class WxInMemoryConfigStorage implements WxConfigStorage {
this.token = token;
}
+ public String getAesKey() {
+ return aesKey;
+ }
+
+ public void setAesKey(String aesKey) {
+ this.aesKey = aesKey;
+ }
+
public void setAccessToken(String accessToken) {
this.accessToken = accessToken;
}
@@ -64,4 +73,5 @@ public class WxInMemoryConfigStorage implements WxConfigStorage {
this.expiresIn = expiresIn;
}
+
}
diff --git a/src/main/java/me/chanjar/weixin/api/WxServiceImpl.java b/src/main/java/me/chanjar/weixin/api/WxServiceImpl.java
index beb58053..90ed8eac 100644
--- a/src/main/java/me/chanjar/weixin/api/WxServiceImpl.java
+++ b/src/main/java/me/chanjar/weixin/api/WxServiceImpl.java
@@ -11,6 +11,7 @@ import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
import me.chanjar.weixin.bean.WxCustomMessage;
+import me.chanjar.weixin.util.crypto.SHA1;
import me.chanjar.weixin.util.json.GsonHelper;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.client.ClientProtocolException;
@@ -67,33 +68,12 @@ public class WxServiceImpl implements WxService {
public boolean checkSignature(String timestamp, String nonce, String signature) {
try {
- String token = wxConfigStorage.getToken();
- MessageDigest sha1 = MessageDigest.getInstance("SHA1");
- String[] arr = new String[] { token, timestamp, nonce };
- Arrays.sort(arr);
- StringBuilder sb = new StringBuilder();
- for(String a : arr) {
- sb.append(a);
- }
- sha1.update(sb.toString().getBytes());
- byte[] output = sha1.digest();
- return bytesToHex(output).equals(signature);
+ return SHA1.gen(wxConfigStorage.getToken(), timestamp, nonce).equals(signature);
} catch (Exception e) {
return false;
}
}
- protected String bytesToHex(byte[] b) {
- char hexDigit[] = {'0', '1', '2', '3', '4', '5', '6', '7',
- '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
- StringBuffer buf = new StringBuffer();
- for (int j = 0; j < b.length; j++) {
- buf.append(hexDigit[(b[j] >> 4) & 0x0f]);
- buf.append(hexDigit[b[j] & 0x0f]);
- }
- return buf.toString();
- }
-
public void accessTokenRefresh() throws WxErrorException {
if (!GLOBAL_ACCESS_TOKEN_REFRESH_FLAG.getAndSet(true)) {
try {
diff --git a/src/main/java/me/chanjar/weixin/bean/WxXmlMessage.java b/src/main/java/me/chanjar/weixin/bean/WxXmlMessage.java
index f2f64c53..67fe1bbd 100644
--- a/src/main/java/me/chanjar/weixin/bean/WxXmlMessage.java
+++ b/src/main/java/me/chanjar/weixin/bean/WxXmlMessage.java
@@ -1,6 +1,11 @@
package me.chanjar.weixin.bean;
-import java.io.InputStream;
+import me.chanjar.weixin.api.WxConfigStorage;
+import me.chanjar.weixin.api.WxConsts;
+import me.chanjar.weixin.util.crypto.WxCryptUtil;
+import me.chanjar.weixin.util.xml.AdapterCDATA;
+import me.chanjar.weixin.util.xml.XmlTransformer;
+import org.apache.commons.io.IOUtils;
import javax.xml.bind.JAXBException;
import javax.xml.bind.annotation.XmlAccessType;
@@ -8,10 +13,8 @@ import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
-
-import me.chanjar.weixin.api.WxConsts;
-import me.chanjar.weixin.util.xml.AdapterCDATA;
-import me.chanjar.weixin.util.xml.XmlTransformer;
+import java.io.IOException;
+import java.io.InputStream;
/**
*
@@ -21,8 +24,8 @@ import me.chanjar.weixin.util.xml.XmlTransformer;
* http://mp.weixin.qq.com/wiki/index.php?title=接收事件推送
* http://mp.weixin.qq.com/wiki/index.php?title=接收语音识别结果
*
- * @author chanjarster
*
+ * @author chanjarster
*/
@XmlRootElement(name = "xml")
@XmlAccessorType(XmlAccessType.FIELD)
@@ -31,102 +34,102 @@ public class WxXmlMessage {
///////////////////////
// 以下都是微信推送过来的消息的xml的element所对应的属性
///////////////////////
-
- @XmlElement(name="ToUserName")
+
+ @XmlElement(name = "ToUserName")
@XmlJavaTypeAdapter(AdapterCDATA.class)
private String ToUserName;
-
- @XmlElement(name="FromUserName")
+
+ @XmlElement(name = "FromUserName")
@XmlJavaTypeAdapter(AdapterCDATA.class)
private String FromUserName;
-
- @XmlElement(name="CreateTime")
+
+ @XmlElement(name = "CreateTime")
private Long CreateTime;
-
- @XmlElement(name="MsgType")
+
+ @XmlElement(name = "MsgType")
@XmlJavaTypeAdapter(AdapterCDATA.class)
private String MsgType;
-
- @XmlElement(name="Content")
+
+ @XmlElement(name = "Content")
@XmlJavaTypeAdapter(AdapterCDATA.class)
private String Content;
- @XmlElement(name="MsgId")
+ @XmlElement(name = "MsgId")
private Long MsgId;
-
- @XmlElement(name="PicUrl")
+
+ @XmlElement(name = "PicUrl")
@XmlJavaTypeAdapter(AdapterCDATA.class)
private String PicUrl;
-
- @XmlElement(name="MediaId")
+
+ @XmlElement(name = "MediaId")
@XmlJavaTypeAdapter(AdapterCDATA.class)
private String MediaId;
-
- @XmlElement(name="Format")
+
+ @XmlElement(name = "Format")
@XmlJavaTypeAdapter(AdapterCDATA.class)
private String Format;
-
- @XmlElement(name="ThumbMediaId")
+
+ @XmlElement(name = "ThumbMediaId")
@XmlJavaTypeAdapter(AdapterCDATA.class)
private String ThumbMediaId;
-
- @XmlElement(name="Location_X")
+
+ @XmlElement(name = "Location_X")
private Double Location_X;
-
- @XmlElement(name="Location_Y")
+
+ @XmlElement(name = "Location_Y")
private Double Location_Y;
-
- @XmlElement(name="Scale")
+
+ @XmlElement(name = "Scale")
private Double Scale;
-
- @XmlElement(name="Label")
+
+ @XmlElement(name = "Label")
@XmlJavaTypeAdapter(AdapterCDATA.class)
private String Label;
-
- @XmlElement(name="Title")
+
+ @XmlElement(name = "Title")
@XmlJavaTypeAdapter(AdapterCDATA.class)
private String Title;
-
- @XmlElement(name="Description")
+
+ @XmlElement(name = "Description")
@XmlJavaTypeAdapter(AdapterCDATA.class)
private String Description;
-
- @XmlElement(name="Url")
+
+ @XmlElement(name = "Url")
@XmlJavaTypeAdapter(AdapterCDATA.class)
private String Url;
-
- @XmlElement(name="Event")
+
+ @XmlElement(name = "Event")
@XmlJavaTypeAdapter(AdapterCDATA.class)
private String Event;
-
- @XmlElement(name="EventKey")
+
+ @XmlElement(name = "EventKey")
@XmlJavaTypeAdapter(AdapterCDATA.class)
private String EventKey;
-
- @XmlElement(name="Ticket")
+
+ @XmlElement(name = "Ticket")
@XmlJavaTypeAdapter(AdapterCDATA.class)
private String Ticket;
-
- @XmlElement(name="Latitude")
+
+ @XmlElement(name = "Latitude")
private Double Latitude;
-
- @XmlElement(name="Longitude")
+
+ @XmlElement(name = "Longitude")
private Double Longitude;
-
- @XmlElement(name="Precision")
+
+ @XmlElement(name = "Precision")
private Double Precision;
-
- @XmlElement(name="Recognition")
+
+ @XmlElement(name = "Recognition")
@XmlJavaTypeAdapter(AdapterCDATA.class)
private String Recognition;
-
+
///////////////////////////////////////
// 群发消息返回的结果
///////////////////////////////////////
/**
* 群发的结果
*/
- @XmlElement(name="Status")
+ @XmlElement(name = "Status")
@XmlJavaTypeAdapter(AdapterCDATA.class)
private String Status;
/**
@@ -134,7 +137,7 @@ public class WxXmlMessage {
*/
private Integer TotalCount;
/**
- * 过滤(过滤是指特定地区、性别的过滤、用户设置拒收的过滤,用户接收已超4条的过滤)后,准备发送的粉丝数,原则上,FilterCount = SentCount + ErrorCount
+ * 过滤(过滤是指特定地区、性别的过滤、用户设置拒收的过滤,用户接收已超4条的过滤)后,准备发送的粉丝数,原则上,FilterCount = SentCount + ErrorCount
*/
private Integer FilterCount;
/**
@@ -145,21 +148,23 @@ public class WxXmlMessage {
* 发送失败的粉丝数
*/
private Integer ErrorCount;
-
+
public String getToUserName() {
return ToUserName;
}
+
public void setToUserName(String toUserName) {
ToUserName = toUserName;
}
-
+
public Long getCreateTime() {
return CreateTime;
}
+
public void setCreateTime(Long createTime) {
CreateTime = createTime;
}
-
+
/**
*
* 当接受用户消息时,可能会获得以下值:
@@ -171,12 +176,13 @@ public class WxXmlMessage {
* {@link WxConsts#XML_MSG_LINK}
* {@link WxConsts#XML_MSG_EVENT}
*
+ *
* @return
*/
public String getMsgType() {
return MsgType;
}
-
+
/**
*
* 当发送消息的时候使用:
@@ -187,148 +193,169 @@ public class WxXmlMessage {
* {@link WxConsts#XML_MSG_NEWS}
* {@link WxConsts#XML_MSG_MUSIC}
*
+ *
* @param msgType
*/
public void setMsgType(String msgType) {
MsgType = msgType;
}
-
+
public String getContent() {
return Content;
}
+
public void setContent(String content) {
Content = content;
}
-
+
public Long getMsgId() {
return MsgId;
}
+
public void setMsgId(Long msgId) {
MsgId = msgId;
}
-
+
public String getPicUrl() {
return PicUrl;
}
+
public void setPicUrl(String picUrl) {
PicUrl = picUrl;
}
-
+
public String getMediaId() {
return MediaId;
}
+
public void setMediaId(String mediaId) {
MediaId = mediaId;
}
-
+
public String getFormat() {
return Format;
}
+
public void setFormat(String format) {
Format = format;
}
-
+
public String getThumbMediaId() {
return ThumbMediaId;
}
+
public void setThumbMediaId(String thumbMediaId) {
ThumbMediaId = thumbMediaId;
}
-
+
public Double getLocation_X() {
return Location_X;
}
+
public void setLocation_X(Double location_X) {
Location_X = location_X;
}
-
+
public Double getLocation_Y() {
return Location_Y;
}
+
public void setLocation_Y(Double location_Y) {
Location_Y = location_Y;
}
-
+
public Double getScale() {
return Scale;
}
+
public void setScale(Double scale) {
Scale = scale;
}
-
+
public String getLabel() {
return Label;
}
+
public void setLabel(String label) {
Label = label;
}
-
+
public String getTitle() {
return Title;
}
+
public void setTitle(String title) {
Title = title;
}
-
+
public String getDescription() {
return Description;
}
+
public void setDescription(String description) {
Description = description;
}
-
+
public String getUrl() {
return Url;
}
+
public void setUrl(String url) {
Url = url;
}
-
+
public String getEvent() {
return Event;
}
+
public void setEvent(String event) {
Event = event;
}
-
+
public String getEventKey() {
return EventKey;
}
+
public void setEventKey(String eventKey) {
EventKey = eventKey;
}
-
+
public String getTicket() {
return Ticket;
}
+
public void setTicket(String ticket) {
Ticket = ticket;
}
-
+
public Double getLatitude() {
return Latitude;
}
+
public void setLatitude(Double latitude) {
Latitude = latitude;
}
-
+
public Double getLongitude() {
return Longitude;
}
+
public void setLongitude(Double longitude) {
Longitude = longitude;
}
-
+
public Double getPrecision() {
return Precision;
}
+
public void setPrecision(Double precision) {
Precision = precision;
}
-
+
public String getRecognition() {
return Recognition;
}
+
public void setRecognition(String recognition) {
Recognition = recognition;
}
@@ -336,10 +363,11 @@ public class WxXmlMessage {
public String getFromUserName() {
return FromUserName;
}
+
public void setFromUserName(String fromUserName) {
FromUserName = fromUserName;
}
-
+
public static WxXmlMessage fromXml(String xml) {
try {
return XmlTransformer.fromXml(WxXmlMessage.class, xml);
@@ -347,7 +375,7 @@ public class WxXmlMessage {
throw new RuntimeException(e);
}
}
-
+
public static WxXmlMessage fromXml(InputStream is) {
try {
return XmlTransformer.fromXml(WxXmlMessage.class, is);
@@ -355,34 +383,73 @@ public class WxXmlMessage {
throw new RuntimeException(e);
}
}
-
+
+ /**
+ * 从加密字符串转换
+ *
+ * @param encryptedXml
+ * @param wxConfigStorage
+ * @param timestamp
+ * @param nonce
+ * @param msgSignature
+ * @return
+ */
+ public static WxXmlMessage fromEncryptedXml(
+ String encryptedXml,
+ WxConfigStorage wxConfigStorage,
+ String timestamp, String nonce, String msgSignature) {
+ WxCryptUtil cryptUtil = new WxCryptUtil(wxConfigStorage);
+ String plainText = cryptUtil.decrypt(msgSignature, timestamp, nonce, encryptedXml);
+ return fromXml(plainText);
+ }
+
+ public static WxXmlMessage fromEncryptedXml(
+ InputStream is,
+ WxConfigStorage wxConfigStorage,
+ String timestamp, String nonce, String msgSignature) {
+ try {
+ return fromEncryptedXml(IOUtils.toString(is, "UTF-8"), wxConfigStorage, timestamp, nonce, msgSignature);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
public String getStatus() {
return Status;
}
+
public void setStatus(String status) {
Status = status;
}
+
public Integer getTotalCount() {
return TotalCount;
}
+
public void setTotalCount(Integer totalCount) {
TotalCount = totalCount;
}
+
public Integer getFilterCount() {
return FilterCount;
}
+
public void setFilterCount(Integer filterCount) {
FilterCount = filterCount;
}
+
public Integer getSentCount() {
return SentCount;
}
+
public void setSentCount(Integer sentCount) {
SentCount = sentCount;
}
+
public Integer getErrorCount() {
return ErrorCount;
}
+
public void setErrorCount(Integer errorCount) {
ErrorCount = errorCount;
}
diff --git a/src/main/java/me/chanjar/weixin/bean/WxXmlOutMessage.java b/src/main/java/me/chanjar/weixin/bean/WxXmlOutMessage.java
index 55558d75..1b46901a 100644
--- a/src/main/java/me/chanjar/weixin/bean/WxXmlOutMessage.java
+++ b/src/main/java/me/chanjar/weixin/bean/WxXmlOutMessage.java
@@ -7,12 +7,14 @@ import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
+import me.chanjar.weixin.api.WxConfigStorage;
import me.chanjar.weixin.bean.outxmlbuilder.ImageBuilder;
import me.chanjar.weixin.bean.outxmlbuilder.MusicBuilder;
import me.chanjar.weixin.bean.outxmlbuilder.NewsBuilder;
import me.chanjar.weixin.bean.outxmlbuilder.TextBuilder;
import me.chanjar.weixin.bean.outxmlbuilder.VideoBuilder;
import me.chanjar.weixin.bean.outxmlbuilder.VoiceBuilder;
+import me.chanjar.weixin.util.crypto.WxCryptUtil;
import me.chanjar.weixin.util.xml.AdapterCDATA;
import me.chanjar.weixin.util.xml.XmlTransformer;
@@ -71,10 +73,20 @@ public class WxXmlOutMessage {
try {
return XmlTransformer.toXml((Class)this.getClass(), this);
} catch (JAXBException e) {
+ throw new RuntimeException(e);
}
- return null;
}
-
+
+ /**
+ * 转换成加密的xml格式
+ * @return
+ */
+ public String toEncryptedXml(WxConfigStorage wxConfigStorage) {
+ String plainXml = toXml();
+ WxCryptUtil pc = new WxCryptUtil(wxConfigStorage);
+ return pc.encrypt(plainXml);
+ }
+
/**
* 获得文本消息builder
* @return
diff --git a/src/main/java/me/chanjar/weixin/util/crypto/ByteGroup.java b/src/main/java/me/chanjar/weixin/util/crypto/ByteGroup.java
new file mode 100755
index 00000000..b0bda3cb
--- /dev/null
+++ b/src/main/java/me/chanjar/weixin/util/crypto/ByteGroup.java
@@ -0,0 +1,26 @@
+package me.chanjar.weixin.util.crypto;
+
+import java.util.ArrayList;
+
+class ByteGroup {
+ ArrayList byteContainer = new ArrayList();
+
+ byte[] toBytes() {
+ byte[] bytes = new byte[byteContainer.size()];
+ for (int i = 0; i < byteContainer.size(); i++) {
+ bytes[i] = byteContainer.get(i);
+ }
+ return bytes;
+ }
+
+ ByteGroup addBytes(byte[] bytes) {
+ for (byte b : bytes) {
+ byteContainer.add(b);
+ }
+ return this;
+ }
+
+ int size() {
+ return byteContainer.size();
+ }
+}
diff --git a/src/main/java/me/chanjar/weixin/util/crypto/PKCS7Encoder.java b/src/main/java/me/chanjar/weixin/util/crypto/PKCS7Encoder.java
new file mode 100755
index 00000000..e02b509c
--- /dev/null
+++ b/src/main/java/me/chanjar/weixin/util/crypto/PKCS7Encoder.java
@@ -0,0 +1,68 @@
+/**
+ * 对公众平台发送给公众账号的消息加解密示例代码.
+ *
+ * @copyright Copyright (c) 1998-2014 Tencent Inc.
+ */
+
+// ------------------------------------------------------------------------
+
+package me.chanjar.weixin.util.crypto;
+
+import java.nio.charset.Charset;
+import java.util.Arrays;
+
+/**
+ * 提供基于PKCS7算法的加解
+ */
+class PKCS7Encoder {
+
+ private static final Charset CHARSET = Charset.forName("utf-8");
+ private static final int BLOCK_SIZE = 32;
+
+ /**
+ * 获得对明文进行补位填充的字节.
+ *
+ * @param count 需要进行填充补位操作的明文字节个数
+ * @return 补齐用的字节数组
+ */
+ static byte[] encode(int count) {
+ // 计算需要填充的位数
+ int amountToPad = BLOCK_SIZE - (count % BLOCK_SIZE);
+ if (amountToPad == 0) {
+ amountToPad = BLOCK_SIZE;
+ }
+ // 获得补位所用的字符
+ char padChr = chr(amountToPad);
+ String tmp = new String();
+ for (int index = 0; index < amountToPad; index++) {
+ tmp += padChr;
+ }
+ return tmp.getBytes(CHARSET);
+ }
+
+ /**
+ * 删除解密后明文的补位字符
+ *
+ * @param decrypted 解密后的明文
+ * @return 删除补位字符后的明文
+ */
+ 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);
+ }
+
+ /**
+ * 将数字转化成ASCII码对应的字符,用于对明文进行补码
+ *
+ * @param a 需要转化的数字
+ * @return 转化得到的字符
+ */
+ static char chr(int a) {
+ byte target = (byte) (a & 0xFF);
+ return (char) target;
+ }
+
+}
diff --git a/src/main/java/me/chanjar/weixin/util/crypto/SHA1.java b/src/main/java/me/chanjar/weixin/util/crypto/SHA1.java
new file mode 100644
index 00000000..eec3c53b
--- /dev/null
+++ b/src/main/java/me/chanjar/weixin/util/crypto/SHA1.java
@@ -0,0 +1,42 @@
+package me.chanjar.weixin.util.crypto;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+
+/**
+ * Created by qianjia on 14/10/19.
+ */
+public class SHA1 {
+
+ /**
+ * 生成SHA1签名
+ * @param arr
+ * @return
+ */
+ public static String gen(String... arr) throws NoSuchAlgorithmException {
+ Arrays.sort(arr);
+ StringBuilder sb = new StringBuilder();
+ for(String a : arr) {
+ sb.append(a);
+ }
+
+ MessageDigest sha1 = MessageDigest.getInstance("SHA1");
+ sha1.update(sb.toString().getBytes());
+ byte[] output = sha1.digest();
+ return bytesToHex(output);
+ }
+
+
+ protected static String bytesToHex(byte[] b) {
+ char hexDigit[] = {'0', '1', '2', '3', '4', '5', '6', '7',
+ '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
+ StringBuffer buf = new StringBuffer();
+ for (int j = 0; j < b.length; j++) {
+ buf.append(hexDigit[(b[j] >> 4) & 0x0f]);
+ buf.append(hexDigit[b[j] & 0x0f]);
+ }
+ return buf.toString();
+ }
+
+}
diff --git a/src/main/java/me/chanjar/weixin/util/crypto/WxCryptUtil.java b/src/main/java/me/chanjar/weixin/util/crypto/WxCryptUtil.java
new file mode 100755
index 00000000..764d851b
--- /dev/null
+++ b/src/main/java/me/chanjar/weixin/util/crypto/WxCryptUtil.java
@@ -0,0 +1,322 @@
+/**
+ * 对公众平台发送给公众账号的消息加解密示例代码.
+ *
+ * @copyright Copyright (c) 1998-2014 Tencent Inc.
+ */
+
+// ------------------------------------------------------------------------
+
+/**
+ * 针对org.apache.commons.codec.binary.Base64,
+ * 需要导入架包commons-codec-1.9(或commons-codec-1.8等其他版本)
+ * 官方下载地址:http://commons.apache.org/proper/commons-codec/download_codec.cgi
+ */
+package me.chanjar.weixin.util.crypto;
+
+import java.io.StringReader;
+import java.nio.charset.Charset;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+import java.util.Random;
+
+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 javax.xml.parsers.ParserConfigurationException;
+
+import me.chanjar.weixin.api.WxConfigStorage;
+import org.apache.commons.codec.binary.Base64;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.xml.sax.InputSource;
+
+public class WxCryptUtil {
+
+ private static final Base64 base64 = new Base64();
+ private static final Charset CHARSET = Charset.forName("utf-8");
+
+ private static final ThreadLocal builderLocal =
+ new ThreadLocal() {
+ @Override protected DocumentBuilder initialValue() {
+ try {
+ return DocumentBuilderFactory.newInstance().newDocumentBuilder();
+ } catch (ParserConfigurationException exc) {
+ throw new IllegalArgumentException(exc);
+ }
+ }
+ };
+
+ private byte[] aesKey;
+ private String token;
+ private String appId;
+
+ /**
+ * 构造函数
+ *
+ * @param wxConfigStorage
+ */
+ public WxCryptUtil(WxConfigStorage wxConfigStorage) {
+ /*
+ * @param token 公众平台上,开发者设置的token
+ * @param encodingAesKey 公众平台上,开发者设置的EncodingAESKey
+ * @param appId 公众平台appid
+ */
+ String encodingAesKey = wxConfigStorage.getAesKey();
+ String token = wxConfigStorage.getToken();
+ String appId = wxConfigStorage.getAppId();
+
+ this.token = token;
+ this.appId = appId;
+ this.aesKey = Base64.decodeBase64(encodingAesKey + "=");
+
+ }
+
+ /**
+ * 构造函数
+ *
+ * @param token 公众平台上,开发者设置的token
+ * @param encodingAesKey 公众平台上,开发者设置的EncodingAESKey
+ * @param appId 公众平台appid
+ */
+ public WxCryptUtil(String token, String encodingAesKey, String appId) {
+ this.token = token;
+ this.appId = appId;
+ this.aesKey = Base64.decodeBase64(encodingAesKey + "=");
+ }
+
+ /**
+ * 将公众平台回复用户的消息加密打包.
+ *
+ * - 对要发送的消息进行AES-CBC加密
+ * - 生成安全签名
+ * - 将消息密文和安全签名打包成xml格式
+ *
+ *
+ * @param plainText 公众平台待回复用户的消息,xml格式的字符串
+ * @return 加密后的可以直接回复用户的密文,包括msg_signature, timestamp, nonce, encrypt的xml格式的字符串
+ */
+ public String encrypt(String plainText) {
+ // 加密
+ String encryptedXml = encrypt(genRandomStr(), plainText);
+
+ // 生成安全签名
+ String timeStamp = timeStamp = Long.toString(System.currentTimeMillis() / 1000l);
+ String nonce = genRandomStr();
+
+ try {
+ String signature = SHA1.gen(token, timeStamp, nonce, encryptedXml);
+ String result = generateXml(encryptedXml, signature, timeStamp, nonce);
+ return result;
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * 对明文进行加密.
+ *
+ * @param plainText 需要加密的明文
+ * @return 加密后base64编码的字符串
+ */
+ protected String encrypt(String randomStr, String plainText) {
+ ByteGroup byteCollector = new ByteGroup();
+ byte[] randomStringBytes = randomStr.getBytes(CHARSET);
+ byte[] plainTextBytes = plainText.getBytes(CHARSET);
+ byte[] bytesOfSizeInNetworkOrder = number2BytesInNetworkOrder(plainTextBytes.length);
+ byte[] appIdBytes = appId.getBytes(CHARSET);
+
+ // randomStr + networkBytesOrder + text + appid
+ byteCollector.addBytes(randomStringBytes);
+ byteCollector.addBytes(bytesOfSizeInNetworkOrder);
+ byteCollector.addBytes(plainTextBytes);
+ byteCollector.addBytes(appIdBytes);
+
+ // ... + pad: 使用自定义的填充方式对明文进行补位填充
+ byte[] padBytes = PKCS7Encoder.encode(byteCollector.size());
+ byteCollector.addBytes(padBytes);
+
+ // 获得最终的字节流, 未加密
+ byte[] unencrypted = byteCollector.toBytes();
+
+ try {
+ // 设置加密模式为AES的CBC模式
+ Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
+ SecretKeySpec keySpec = new SecretKeySpec(aesKey, "AES");
+ IvParameterSpec iv = new IvParameterSpec(aesKey, 0, 16);
+ cipher.init(Cipher.ENCRYPT_MODE, keySpec, iv);
+
+ // 加密
+ byte[] encrypted = cipher.doFinal(unencrypted);
+
+ // 使用BASE64对加密后的字符串进行编码
+ String base64Encrypted = base64.encodeToString(encrypted);
+
+ return base64Encrypted;
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * 检验消息的真实性,并且获取解密后的明文.
+ *
+ * - 利用收到的密文生成安全签名,进行签名验证
+ * - 若验证通过,则提取xml中的加密消息
+ * - 对消息进行解密
+ *
+ *
+ * @param msgSignature 签名串,对应URL参数的msg_signature
+ * @param timeStamp 时间戳,对应URL参数的timestamp
+ * @param nonce 随机串,对应URL参数的nonce
+ * @param encryptedXml 密文,对应POST请求的数据
+ * @return 解密后的原文
+ */
+ public String decrypt(String msgSignature, String timeStamp, String nonce, String encryptedXml) {
+ // 密钥,公众账号的app secret
+ // 提取密文
+ String cipherText = extractEncryptPart(encryptedXml);
+
+ try {
+ // 验证安全签名
+ String signature = SHA1.gen(token, timeStamp, nonce, cipherText);
+ if (!signature.equals(msgSignature)) {
+ throw new RuntimeException("加密消息签名校验失败");
+ }
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException(e);
+ }
+
+ // 解密
+ String result = decrypt(cipherText);
+ return result;
+ }
+
+ /**
+ * 对密文进行解密.
+ *
+ * @param cipherText 需要解密的密文
+ * @return 解密得到的明文
+ */
+ protected String decrypt(String cipherText) {
+ 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(cipherText);
+
+ // 解密
+ original = cipher.doFinal(encrypted);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+
+ String xmlContent, from_appid;
+ try {
+ // 去除补位字符
+ byte[] bytes = PKCS7Encoder.decode(original);
+
+ // 分离16位随机字符串,网络字节序和AppId
+ byte[] networkOrder = Arrays.copyOfRange(bytes, 16, 20);
+
+ int xmlLength = bytesNetworkOrder2Number(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) {
+ throw new RuntimeException(e);
+ }
+
+ // appid不相同的情况
+ if (!from_appid.equals(appId)) {
+ throw new RuntimeException("AppID不正确");
+ }
+
+ return xmlContent;
+
+ }
+
+ /**
+ * 将一个数字转换成生成4个字节的网络字节序bytes数组
+ *
+ * @param number
+ */
+ private byte[] number2BytesInNetworkOrder(int number) {
+ byte[] orderBytes = new byte[4];
+ orderBytes[3] = (byte) (number & 0xFF);
+ orderBytes[2] = (byte) (number >> 8 & 0xFF);
+ orderBytes[1] = (byte) (number >> 16 & 0xFF);
+ orderBytes[0] = (byte) (number >> 24 & 0xFF);
+ return orderBytes;
+ }
+
+ /**
+ * 4个字节的网络字节序bytes数组还原成一个数字
+ *
+ * @param bytesInNetworkOrder
+ */
+ private int bytesNetworkOrder2Number(byte[] bytesInNetworkOrder) {
+ int sourceNumber = 0;
+ for (int i = 0; i < 4; i++) {
+ sourceNumber <<= 8;
+ sourceNumber |= bytesInNetworkOrder[i] & 0xff;
+ }
+ return sourceNumber;
+ }
+
+ /**
+ * 随机生成16位字符串
+ *
+ * @return
+ */
+ private String genRandomStr() {
+ String base = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+ Random random = new Random();
+ StringBuffer sb = new StringBuffer();
+ for (int i = 0; i < 16; i++) {
+ int number = random.nextInt(base.length());
+ sb.append(base.charAt(number));
+ }
+ return sb.toString();
+ }
+
+ /**
+ * 生成xml消息
+ *
+ * @param encrypt 加密后的消息密文
+ * @param signature 安全签名
+ * @param timestamp 时间戳
+ * @param nonce 随机字符串
+ * @return 生成的xml字符串
+ */
+ private String generateXml(String encrypt, String signature, String timestamp, String nonce) {
+ String format =
+ "\n"
+ + "\n"
+ + "\n"
+ + "%3$s\n"
+ + "\n"
+ + "";
+ return String.format(format, encrypt, signature, timestamp, nonce);
+ }
+
+ static String extractEncryptPart(String xml) {
+ try {
+ DocumentBuilder db = builderLocal.get();
+ Document document = db.parse(new InputSource(new StringReader(xml)));
+
+ Element root = document.getDocumentElement();
+ return root.getElementsByTagName("Encrypt").item(0).getTextContent();
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+}
diff --git a/src/main/java/me/chanjar/weixin/util/xml/XmlTransformer.java b/src/main/java/me/chanjar/weixin/util/xml/XmlTransformer.java
index b6fb5f2b..026cc0a5 100644
--- a/src/main/java/me/chanjar/weixin/util/xml/XmlTransformer.java
+++ b/src/main/java/me/chanjar/weixin/util/xml/XmlTransformer.java
@@ -6,66 +6,88 @@ import java.io.StringReader;
import java.io.StringWriter;
import java.io.Writer;
-import javax.xml.bind.JAXBContext;
-import javax.xml.bind.JAXBException;
-import javax.xml.bind.Marshaller;
-import javax.xml.bind.Unmarshaller;
+import javax.xml.bind.*;
+import me.chanjar.weixin.bean.*;
import org.xml.sax.InputSource;
import com.sun.xml.bind.marshaller.CharacterEscapeHandler;
public class XmlTransformer {
+ protected static final JAXBContext JAXB_CONTEXT = initJAXBContext();
+
/**
* xml -> pojo
+ *
* @param clazz
- * @param object
+ * @param xml
* @return
- * @throws JAXBException
+ * @throws JAXBException
*/
+ @SuppressWarnings("unchecked")
public static T fromXml(Class clazz, String xml) throws JAXBException {
- JAXBContext context = JAXBContext.newInstance(clazz);
- Unmarshaller um = context.createUnmarshaller();
+ Unmarshaller um = JAXB_CONTEXT.createUnmarshaller();
T object = (T) um.unmarshal(new StringReader(xml));
return object;
}
-
+
+ @SuppressWarnings("unchecked")
public static T fromXml(Class clazz, InputStream is) throws JAXBException {
- JAXBContext context = JAXBContext.newInstance(clazz);
- Unmarshaller um = context.createUnmarshaller();
+ Unmarshaller um = JAXB_CONTEXT.createUnmarshaller();
InputSource inputSource = new InputSource(is);
inputSource.setEncoding("utf-8");
T object = (T) um.unmarshal(inputSource);
return object;
}
-
+
/**
* pojo -> xml
+ *
* @param clazz
+ * @param object
* @return
- * @throws JAXBException
+ * @throws JAXBException
*/
public static String toXml(Class clazz, T object) throws JAXBException {
StringWriter stringWriter = new StringWriter();
toXml(clazz, object, stringWriter);
return stringWriter.getBuffer().toString();
}
-
+
public static void toXml(Class clazz, T object, Writer writer) throws JAXBException {
- JAXBContext context = JAXBContext.newInstance(clazz);
- Marshaller m = context.createMarshaller();
+ Marshaller m = JAXB_CONTEXT.createMarshaller();
m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
- m.setProperty(CharacterEscapeHandler.class.getName(), characterUnescapeHandler);
+ m.setProperty(CharacterEscapeHandler.class.getName(), CHAR_ESCAPE_HANDLER);
m.marshal(object, writer);
}
-
- protected static CharacterEscapeHandler characterUnescapeHandler = new CharacterUnescapeHandler();
-
+
+ protected static final CharacterEscapeHandler CHAR_ESCAPE_HANDLER = new CharacterUnescapeHandler();
+
protected static class CharacterUnescapeHandler implements CharacterEscapeHandler {
public void escape(char[] ac, int i, int j, boolean flag, Writer writer) throws IOException {
writer.write(ac, i, j);
}
}
-
+
+ private static JAXBContext initJAXBContext() {
+ /*
+ * JAXBContext对象是线程安全的,根据官方文档的建议将对象作为全局实例
+ * https://jaxb.java.net/guide/Performance_and_thread_safety.html
+ */
+ try {
+ return JAXBContext.newInstance(
+ WxXmlOutMessage.class,
+ WxXmlOutImageMessage.class,
+ WxXmlOutMewsMessage.class,
+ WxXmlOutMusicMessage.class,
+ WxXmlOutTextMessage.class,
+ WxXmlOutVideoMessage.class,
+ WxXmlOutVoiceMessage.class,
+ WxXmlMessage.class);
+ } catch (JAXBException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
}
diff --git a/src/test/java/me/chanjar/weixin/api/ApiTestModule.java b/src/test/java/me/chanjar/weixin/api/ApiTestModule.java
index 369a7dbd..68a9f8a2 100644
--- a/src/test/java/me/chanjar/weixin/api/ApiTestModule.java
+++ b/src/test/java/me/chanjar/weixin/api/ApiTestModule.java
@@ -2,7 +2,9 @@ package me.chanjar.weixin.api;
import java.io.InputStream;
+import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
+import javax.xml.bind.Unmarshaller;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
@@ -13,6 +15,7 @@ import me.chanjar.weixin.util.xml.XmlTransformer;
import com.google.inject.Binder;
import com.google.inject.Module;
+import org.xml.sax.InputSource;
public class ApiTestModule implements Module {
@@ -20,22 +23,31 @@ public class ApiTestModule implements Module {
public void configure(Binder binder) {
try {
InputStream is1 = ClassLoader.getSystemResourceAsStream("test-config.xml");
- WxXmlConfigStorage config = XmlTransformer.fromXml(WxXmlConfigStorage.class, is1);
+ WxXmlConfigStorage config = fromXml(WxXmlConfigStorage.class, is1);
WxServiceImpl wxService = new WxServiceImpl();
wxService.setWxConfigStorage(config);
binder.bind(WxServiceImpl.class).toInstance(wxService);
+ binder.bind(WxConfigStorage.class).toInstance(config);
} catch (JAXBException e) {
throw new RuntimeException(e);
}
}
+ public static T fromXml(Class clazz, InputStream is) throws JAXBException {
+ Unmarshaller um = JAXBContext.newInstance(clazz).createUnmarshaller();
+ InputSource inputSource = new InputSource(is);
+ inputSource.setEncoding("utf-8");
+ T object = (T) um.unmarshal(inputSource);
+ return object;
+ }
+
@XmlRootElement(name = "xml")
@XmlAccessorType(XmlAccessType.FIELD)
public static class WxXmlConfigStorage extends WxInMemoryConfigStorage {
protected String openId;
-
+
public String getOpenId() {
return openId;
}
diff --git a/src/test/java/me/chanjar/weixin/api/WxBaseAPITest.java b/src/test/java/me/chanjar/weixin/api/WxBaseAPITest.java
index e4d84678..5ef5f3c7 100644
--- a/src/test/java/me/chanjar/weixin/api/WxBaseAPITest.java
+++ b/src/test/java/me/chanjar/weixin/api/WxBaseAPITest.java
@@ -33,9 +33,9 @@ public class WxBaseAPITest {
}
public void testCheckSignature() throws WxErrorException {
- String timestamp = "23234235423246";
- String nonce = "y7didfkcmvnbd90sdofjkiefhsd";
- String signature = "77b6651628dfb9a64bfb0d3432ee053ac566a459";
+ String timestamp = "1413729506";
+ String nonce = "1753282854";
+ String signature = "af210121811dce2d6f306612cb133cba490e818b";
Assert.assertTrue(wxService.checkSignature(timestamp, nonce, signature));
}
diff --git a/src/test/java/me/chanjar/weixin/api/WxMessageRouterTest.java b/src/test/java/me/chanjar/weixin/api/WxMessageRouterTest.java
index e7319a62..50423a1a 100644
--- a/src/test/java/me/chanjar/weixin/api/WxMessageRouterTest.java
+++ b/src/test/java/me/chanjar/weixin/api/WxMessageRouterTest.java
@@ -71,7 +71,7 @@ public class WxMessageRouterTest {
router.rule().handler(new WxMessageHandler() {
@Override
public WxXmlOutMessage handle(WxXmlMessage wxMessage, Map context) {
- // TODO Auto-generated method stub
+ System.out.println(wxMessage.toString());
return null;
}
}).end();
diff --git a/src/test/java/me/chanjar/weixin/bean/WxXmlMessageTest.java b/src/test/java/me/chanjar/weixin/bean/WxXmlMessageTest.java
index 6db20bd1..79ad7634 100644
--- a/src/test/java/me/chanjar/weixin/bean/WxXmlMessageTest.java
+++ b/src/test/java/me/chanjar/weixin/bean/WxXmlMessageTest.java
@@ -1,11 +1,9 @@
package me.chanjar.weixin.bean;
-import me.chanjar.weixin.bean.WxXmlMessage;
+import me.chanjar.weixin.api.WxConsts;
import org.testng.Assert;
import org.testng.annotations.Test;
-import me.chanjar.weixin.api.WxConsts;
-
@Test
public class WxXmlMessageTest {
diff --git a/src/test/java/me/chanjar/weixin/demo/WxTestConfigStorage.java b/src/test/java/me/chanjar/weixin/demo/WxTestConfigStorage.java
new file mode 100644
index 00000000..d5921e0c
--- /dev/null
+++ b/src/test/java/me/chanjar/weixin/demo/WxTestConfigStorage.java
@@ -0,0 +1,35 @@
+package me.chanjar.weixin.demo;
+
+import me.chanjar.weixin.api.WxInMemoryConfigStorage;
+import org.xml.sax.InputSource;
+
+import javax.xml.bind.JAXBContext;
+import javax.xml.bind.JAXBException;
+import javax.xml.bind.Unmarshaller;
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlRootElement;
+import java.io.InputStream;
+
+/**
+ * @author Daniel Qian
+ */
+@XmlRootElement(name = "xml")
+@XmlAccessorType(XmlAccessType.FIELD)
+class WxTestConfigStorage extends WxInMemoryConfigStorage {
+
+ @Override
+ public String toString() {
+ return "SimpleWxConfigProvider [appId=" + appId + ", secret=" + secret + ", accessToken=" + accessToken
+ + ", expiresIn=" + expiresIn + ", token=" + token + ", aesKey=" + aesKey + "]";
+ }
+
+
+ public static WxTestConfigStorage fromXml(InputStream is) throws JAXBException {
+ Unmarshaller um = JAXBContext.newInstance(WxTestConfigStorage.class).createUnmarshaller();
+ InputSource inputSource = new InputSource(is);
+ inputSource.setEncoding("utf-8");
+ return (WxTestConfigStorage) um.unmarshal(inputSource);
+ }
+
+}
diff --git a/src/test/java/me/chanjar/weixin/demo/WxTestServer.java b/src/test/java/me/chanjar/weixin/demo/WxTestServer.java
new file mode 100644
index 00000000..e9c6d9d0
--- /dev/null
+++ b/src/test/java/me/chanjar/weixin/demo/WxTestServer.java
@@ -0,0 +1,20 @@
+package me.chanjar.weixin.demo;
+
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.servlet.ServletHandler;
+
+/**
+ * @author Daniel Qian
+ */
+public class WxTestServer {
+ public static void main(String[] args) throws Exception {
+ Server server = new Server(8080);
+
+ ServletHandler handler = new ServletHandler();
+ server.setHandler(handler);
+
+ handler.addServletWithMapping(WxTestServlet.class, "/*");
+ server.start();
+ server.join();
+ }
+}
diff --git a/src/test/java/me/chanjar/weixin/demo/WxTestServlet.java b/src/test/java/me/chanjar/weixin/demo/WxTestServlet.java
new file mode 100644
index 00000000..40f8ad7c
--- /dev/null
+++ b/src/test/java/me/chanjar/weixin/demo/WxTestServlet.java
@@ -0,0 +1,117 @@
+package me.chanjar.weixin.demo;
+
+import me.chanjar.weixin.api.*;
+import me.chanjar.weixin.bean.WxXmlMessage;
+import me.chanjar.weixin.bean.WxXmlOutMessage;
+import me.chanjar.weixin.bean.WxXmlOutTextMessage;
+import org.apache.commons.lang3.StringUtils;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.xml.bind.JAXBException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Map;
+
+/**
+ * @author Daniel Qian
+ */
+public class WxTestServlet extends HttpServlet {
+
+ protected WxService wxService;
+ protected WxConfigStorage wxConfigStorage;
+ protected WxMessageRouter wxMessageRouter;
+
+ @Override public void init() throws ServletException {
+ //
+ super.init();
+ try {
+ InputStream is1 = ClassLoader.getSystemResourceAsStream("test-config.xml");
+ WxTestConfigStorage config = WxTestConfigStorage.fromXml(is1);
+
+ wxConfigStorage = config;
+ wxService = new WxServiceImpl();
+ wxService.setWxConfigStorage(config);
+
+ WxMessageHandler handler = new WxMessageHandler() {
+ @Override public WxXmlOutMessage handle(WxXmlMessage wxMessage, Map context) {
+ WxXmlOutTextMessage m
+ = WxXmlOutMessage.TEXT().content("测试加密消息").fromUser(wxMessage.getToUserName())
+ .touser(wxMessage.getFromUserName()).build();
+ return m;
+ }
+ };
+
+ wxMessageRouter = new WxMessageRouter();
+ wxMessageRouter
+ .rule()
+ .async(false)
+ .content("哈哈") // 拦截内容为“哈哈”的消息
+ .handler(handler)
+ .end();
+
+ } catch (JAXBException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override protected void service(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+
+ String signature = request.getParameter("signature");
+ String nonce = request.getParameter("nonce");
+ String timestamp = request.getParameter("timestamp");
+
+ response.setContentType("text/html;charset=utf-8");
+ response.setStatus(HttpServletResponse.SC_OK);
+
+ if (!wxService.checkSignature(timestamp, nonce, signature)) {
+ // 消息签名不正确,说明不是公众平台发过来的消息
+ response.getWriter().println("非法请求");
+ return;
+ }
+
+ String echostr = request.getParameter("echostr");
+ if (StringUtils.isNotBlank(echostr)) {
+ // 说明是一个仅仅用来验证的请求,回显echostr
+ response.getWriter().println(echostr);
+ return;
+ }
+
+ String encryptType = StringUtils.isBlank(request.getParameter("encrypt_type")) ?
+ "raw" :
+ request.getParameter("encrypt_type");
+
+ WxXmlMessage inMessage = null;
+
+ if ("raw".equals(encryptType)) {
+ // 明文传输的消息
+ inMessage = WxXmlMessage.fromXml(request.getInputStream());
+ } else if ("aes".equals(encryptType)) {
+ // 是aes加密的消息
+ String msgSignature = request.getParameter("msg_signature");
+ inMessage = WxXmlMessage.fromEncryptedXml(
+ request.getInputStream(),
+ wxConfigStorage,
+ timestamp, nonce, msgSignature);
+ } else {
+ response.getWriter().println("不可识别的加密类型");
+ return;
+ }
+
+ WxXmlOutMessage outMessage = wxMessageRouter.route(inMessage);
+
+ if (outMessage != null) {
+ if ("raw".equals(encryptType)) {
+ response.getWriter().write(outMessage.toXml());
+ } else if ("aes".equals(encryptType)) {
+ response.getWriter().write(outMessage.toEncryptedXml(wxConfigStorage));
+ }
+ return;
+ }
+
+ }
+
+}
diff --git a/src/test/java/me/chanjar/weixin/util/crypto/WxCryptUtilTest.java b/src/test/java/me/chanjar/weixin/util/crypto/WxCryptUtilTest.java
new file mode 100755
index 00000000..0c61d3d0
--- /dev/null
+++ b/src/test/java/me/chanjar/weixin/util/crypto/WxCryptUtilTest.java
@@ -0,0 +1,94 @@
+package me.chanjar.weixin.util.crypto;
+
+import org.testng.annotations.Test;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NodeList;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import java.io.IOException;
+import java.io.StringReader;
+
+import static org.testng.Assert.*;
+
+@Test
+public class WxCryptUtilTest {
+ String encodingAesKey = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG";
+ String token = "pamtest";
+ String timestamp = "1409304348";
+ String nonce = "xxxxxx";
+ String appId = "wxb11529c136998cb6";
+ String randomStr = "aaaabbbbccccdddd";
+
+ String xmlFormat = "";
+ String replyMsg = "我是中文abcd123";
+
+ String afterAesEncrypt = "jn1L23DB+6ELqJ+6bruv21Y6MD7KeIfP82D6gU39rmkgczbWwt5+3bnyg5K55bgVtVzd832WzZGMhkP72vVOfg==";
+
+ String replyMsg2 = "1407743423";
+ String afterAesEncrypt2 = "jn1L23DB+6ELqJ+6bruv23M2GmYfkv0xBh2h+XTBOKVKcgDFHle6gqcZ1cZrk3e1qjPQ1F4RsLWzQRG9udbKWesxlkupqcEcW7ZQweImX9+wLMa0GaUzpkycA8+IamDBxn5loLgZpnS7fVAbExOkK5DYHBmv5tptA9tklE/fTIILHR8HLXa5nQvFb3tYPKAlHF3rtTeayNf0QuM+UW/wM9enGIDIJHF7CLHiDNAYxr+r+OrJCmPQyTy8cVWlu9iSvOHPT/77bZqJucQHQ04sq7KZI27OcqpQNSto2OdHCoTccjggX5Z9Mma0nMJBU+jLKJ38YB1fBIz+vBzsYjrTmFQ44YfeEuZ+xRTQwr92vhA9OxchWVINGC50qE/6lmkwWTwGX9wtQpsJKhP+oS7rvTY8+VdzETdfakjkwQ5/Xka042OlUb1/slTwo4RscuQ+RdxSGvDahxAJ6+EAjLt9d8igHngxIbf6YyqqROxuxqIeIch3CssH/LqRs+iAcILvApYZckqmA7FNERspKA5f8GoJ9sv8xmGvZ9Yrf57cExWtnX8aCMMaBropU/1k+hKP5LVdzbWCG0hGwx/dQudYR/eXp3P0XxjlFiy+9DMlaFExWUZQDajPkdPrEeOwofJb";
+
+ public void testNormal() throws ParserConfigurationException, SAXException, IOException {
+ WxCryptUtil pc = new WxCryptUtil(token, encodingAesKey, appId);
+ String encryptedXml = pc.encrypt(replyMsg);
+
+ System.out.println(encryptedXml);
+
+ DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
+ DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
+ Document document = documentBuilder.parse(new InputSource(new StringReader(encryptedXml)));
+
+ Element root = document.getDocumentElement();
+ String cipherText = root.getElementsByTagName("Encrypt").item(0).getTextContent();
+ String msgSignature = root.getElementsByTagName("MsgSignature").item(0).getTextContent();
+ String timestamp = root.getElementsByTagName("TimeStamp").item(0).getTextContent();
+ String nonce = root.getElementsByTagName("Nonce").item(0).getTextContent();
+
+ String messageText = String.format(xmlFormat, cipherText);
+
+ // 第三方收到公众号平台发送的消息
+ String plainMessage = pc.decrypt(cipherText);
+
+ System.out.println(plainMessage);
+ assertEquals(plainMessage, replyMsg);
+ }
+
+ public void testAesEncrypt() {
+ WxCryptUtil pc = new WxCryptUtil(token, encodingAesKey, appId);
+ assertEquals(pc.encrypt(randomStr, replyMsg), afterAesEncrypt);
+ }
+
+ public void testAesEncrypt2() {
+ WxCryptUtil pc = new WxCryptUtil(token, encodingAesKey, appId);
+ assertEquals(pc.encrypt(randomStr, replyMsg2), afterAesEncrypt2);
+ }
+
+ public void testValidateSignatureError() throws ParserConfigurationException, SAXException,
+ IOException {
+ try {
+ WxCryptUtil pc = new WxCryptUtil(token, encodingAesKey, appId);
+ String afterEncrpt = pc.encrypt(replyMsg);
+ DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
+ DocumentBuilder db = dbf.newDocumentBuilder();
+ StringReader sr = new StringReader(afterEncrpt);
+ InputSource is = new InputSource(sr);
+ Document document = db.parse(is);
+
+ Element root = document.getDocumentElement();
+ NodeList nodelist1 = root.getElementsByTagName("Encrypt");
+
+ String encrypt = nodelist1.item(0).getTextContent();
+ String fromXML = String.format(xmlFormat, encrypt);
+ pc.decrypt("12345", timestamp, nonce, fromXML); // 这里签名错误
+ } catch (RuntimeException e) {
+ assertEquals(e.getMessage(), "加密消息签名校验失败");
+ return;
+ }
+ fail("错误流程不抛出异常???");
+ }
+
+}
diff --git a/src/test/resources/testng.xml b/src/test/resources/testng.xml
index 7bfed9b7..2e15e341 100644
--- a/src/test/resources/testng.xml
+++ b/src/test/resources/testng.xml
@@ -29,6 +29,7 @@
+