From a3a496f8527c30bb51492b371713c91b1e5655eb Mon Sep 17 00:00:00 2001 From: Daniel Qian Date: Sat, 18 Oct 2014 22:28:10 +0800 Subject: [PATCH] =?UTF-8?q?issue=20#16=20=E6=B7=BB=E5=8A=A0=E6=B6=88?= =?UTF-8?q?=E6=81=AF=E5=8A=A0=E8=A7=A3=E5=AF=86=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 22 ++ .../chanjar/weixin/api/WxConfigStorage.java | 6 +- .../weixin/api/WxInMemoryConfigStorage.java | 10 + .../me/chanjar/weixin/api/WxServiceImpl.java | 24 +- .../me/chanjar/weixin/bean/WxXmlMessage.java | 233 ++++++++----- .../chanjar/weixin/bean/WxXmlOutMessage.java | 16 +- .../chanjar/weixin/util/crypto/ByteGroup.java | 26 ++ .../weixin/util/crypto/PKCS7Encoder.java | 68 ++++ .../me/chanjar/weixin/util/crypto/SHA1.java | 42 +++ .../weixin/util/crypto/WxCryptUtil.java | 322 ++++++++++++++++++ .../me/chanjar/weixin/api/ApiTestModule.java | 16 +- .../me/chanjar/weixin/api/WxBaseAPITest.java | 6 +- .../weixin/api/WxMessageRouterTest.java | 2 +- .../weixin/demo/WxTestConfigStorage.java | 35 ++ .../me/chanjar/weixin/demo/WxTestServer.java | 20 ++ .../me/chanjar/weixin/demo/WxTestServlet.java | 117 +++++++ .../weixin/util/crypto/WxCryptUtilTest.java | 94 +++++ src/test/resources/testng.xml | 1 + 18 files changed, 945 insertions(+), 115 deletions(-) create mode 100755 src/main/java/me/chanjar/weixin/util/crypto/ByteGroup.java create mode 100755 src/main/java/me/chanjar/weixin/util/crypto/PKCS7Encoder.java create mode 100644 src/main/java/me/chanjar/weixin/util/crypto/SHA1.java create mode 100755 src/main/java/me/chanjar/weixin/util/crypto/WxCryptUtil.java create mode 100644 src/test/java/me/chanjar/weixin/demo/WxTestConfigStorage.java create mode 100644 src/test/java/me/chanjar/weixin/demo/WxTestServer.java create mode 100644 src/test/java/me/chanjar/weixin/demo/WxTestServlet.java create mode 100755 src/test/java/me/chanjar/weixin/util/crypto/WxCryptUtilTest.java diff --git a/pom.xml b/pom.xml index 529763447..2c5ab6571 100644 --- a/pom.xml +++ b/pom.xml @@ -96,6 +96,28 @@ commons-lang3 3.1 + + commons-codec + commons-codec + 1.9 + + + commons-io + commons-io + 2.4 + + + org.eclipse.jetty + jetty-server + 9.3.0.M0 + test + + + org.eclipse.jetty + jetty-servlet + 9.3.0.M0 + test + diff --git a/src/main/java/me/chanjar/weixin/api/WxConfigStorage.java b/src/main/java/me/chanjar/weixin/api/WxConfigStorage.java index 105b965be..57bdcd3ba 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 783da6c46..1fe66b381 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 beb580530..90ed8eacb 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 f2f64c53c..67fe1bbd8 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 55558d752..1b46901ad 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 000000000..b0bda3cb1 --- /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 000000000..e02b509cf --- /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 000000000..eec3c53ba --- /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 000000000..764d851b5 --- /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 + "="); + } + + /** + * 将公众平台回复用户的消息加密打包. + *
    + *
  1. 对要发送的消息进行AES-CBC加密
  2. + *
  3. 生成安全签名
  4. + *
  5. 将消息密文和安全签名打包成xml格式
  6. + *
+ * + * @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); + } + } + + /** + * 检验消息的真实性,并且获取解密后的明文. + *
    + *
  1. 利用收到的密文生成安全签名,进行签名验证
  2. + *
  3. 若验证通过,则提取xml中的加密消息
  4. + *
  5. 对消息进行解密
  6. + *
+ * + * @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/test/java/me/chanjar/weixin/api/ApiTestModule.java b/src/test/java/me/chanjar/weixin/api/ApiTestModule.java index 369a7dbd2..68a9f8a27 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 e4d84678e..5ef5f3c7e 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 e7319a624..50423a1ad 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/demo/WxTestConfigStorage.java b/src/test/java/me/chanjar/weixin/demo/WxTestConfigStorage.java new file mode 100644 index 000000000..d5921e0c0 --- /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 000000000..e9c6d9d09 --- /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 000000000..40f8ad7c3 --- /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 000000000..0c61d3d08 --- /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 7bfed9b73..2e15e3416 100644 --- a/src/test/resources/testng.xml +++ b/src/test/resources/testng.xml @@ -29,6 +29,7 @@ +