新增获取微信服务器IP地址接口以及消息AES加密解密实现
This commit is contained in:
parent
717b9c7abf
commit
282699d95c
25
README.md
25
README.md
@ -40,9 +40,9 @@ weixin4j
|
|||||||
|
|
||||||
* 2014-11-03
|
* 2014-11-03
|
||||||
|
|
||||||
+ **weixin-mp**: 分离为`weixin-mp-api`和`weixin-mp-server`两个工程
|
+ **weixin4j-mp**: 分离为`weixin-mp-api`和`weixin-mp-server`两个工程
|
||||||
|
|
||||||
+ **weixin-mp**: 加入支付模块
|
+ **weixin4j-mp**: 加入支付模块
|
||||||
|
|
||||||
* 2014-11-05
|
* 2014-11-05
|
||||||
|
|
||||||
@ -50,26 +50,35 @@ weixin4j
|
|||||||
|
|
||||||
* 2014-11-06
|
* 2014-11-06
|
||||||
|
|
||||||
+ **weixin-base**: 删除`WeixinConfig`类只保留`WeixinAccount`类
|
+ **weixin4j-base**: 删除`WeixinConfig`类只保留`WeixinAccount`类
|
||||||
|
|
||||||
+ **weixin-mp**: 新增V3版本`退款接口`
|
+ **weixin4j-mp**: 新增V3版本`退款接口`
|
||||||
|
|
||||||
* 2014-11-08
|
* 2014-11-08
|
||||||
|
|
||||||
+ **weixin-mp**: 新增V2版本`退款申请`、`退款查询`、`对账单下载`三个接口
|
+ **weixin4j-mp**: 新增V2版本`退款申请`、`退款查询`、`对账单下载`三个接口
|
||||||
|
|
||||||
+ **weixin-mp**: 新增一个简单的`语义理解`接口
|
+ **weixin-mp**: 新增一个简单的`语义理解`接口
|
||||||
|
|
||||||
* 2014-11-11
|
* 2014-11-11
|
||||||
|
|
||||||
+ **weixin-mp**: 自定义`assembly`将`weixin4j-base`工程也一起打包(`weixin4j-mp-api-full.jar`)
|
+ **weixin4j-mp**: 自定义`assembly`将`weixin4j-base`工程也一起打包(`weixin4j-mp-api-full.jar`)
|
||||||
|
|
||||||
|
* 2014-11-15
|
||||||
|
|
||||||
|
+ **weixin4j-base**: 新增aes加密解密函数
|
||||||
|
|
||||||
|
+ **weixin4j-mp**: 新增获取`微信服务器IP地址`接口
|
||||||
|
|
||||||
|
+ **weixin4j-mp**: 解决`server工程`打包不能运行问题(`ClassUtil`无法获取jar包里面的类)
|
||||||
|
|
||||||
|
+ **weixin4j-mp**: 新增被动消息的`加密`以及回复消息的`解密`
|
||||||
|
|
||||||
|
|
||||||
接下来
|
接下来
|
||||||
------
|
------
|
||||||
* 公众号智能接口
|
* 公众号智能接口
|
||||||
|
|
||||||
* 微信消息加密
|
|
||||||
|
|
||||||
* 被扫支付
|
* 被扫支付
|
||||||
|
|
||||||
* 企业号API封装
|
* 企业号API封装
|
||||||
|
|||||||
@ -22,3 +22,7 @@ weixin4j-base
|
|||||||
* 2014-11-06
|
* 2014-11-06
|
||||||
|
|
||||||
+ 删除`WeixinConfig`类只保留`WeixinAccount`类
|
+ 删除`WeixinConfig`类只保留`WeixinAccount`类
|
||||||
|
|
||||||
|
* 2014-11-15
|
||||||
|
|
||||||
|
+ 新增`aes加密解密`函数
|
||||||
@ -8,4 +8,7 @@ public final class Consts {
|
|||||||
public static final String PKCS12 = "PKCS12";
|
public static final String PKCS12 = "PKCS12";
|
||||||
public static final String TLS = "TLS";
|
public static final String TLS = "TLS";
|
||||||
public static final String X509 = "X.509";
|
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;
|
private String appSecret;
|
||||||
// 公众号支付请求中用于加密的密钥 Key,可验证商户唯一身份,PaySignKey 对应于支付场景中的 appKey 值
|
// 公众号支付请求中用于加密的密钥 Key,可验证商户唯一身份,PaySignKey 对应于支付场景中的 appKey 值
|
||||||
private String paySignKey;
|
private String paySignKey;
|
||||||
|
// 安全模式下的加密密钥
|
||||||
|
private String encodingAesKey;
|
||||||
// 财付通商户身份的标识
|
// 财付通商户身份的标识
|
||||||
private String partnerId;
|
private String partnerId;
|
||||||
// 财付通商户权限密钥Key
|
// 财付通商户权限密钥Key
|
||||||
@ -75,6 +77,14 @@ public class WeixinAccount implements Serializable {
|
|||||||
this.appSecret = appSecret;
|
this.appSecret = appSecret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getEncodingAesKey() {
|
||||||
|
return encodingAesKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEncodingAesKey(String encodingAesKey) {
|
||||||
|
this.encodingAesKey = encodingAesKey;
|
||||||
|
}
|
||||||
|
|
||||||
public String getPaySignKey() {
|
public String getPaySignKey() {
|
||||||
return paySignKey;
|
return paySignKey;
|
||||||
}
|
}
|
||||||
@ -195,10 +205,10 @@ public class WeixinAccount implements Serializable {
|
|||||||
public String toString() {
|
public String toString() {
|
||||||
return "WeixinAccount [token=" + token + ", openId=" + openId
|
return "WeixinAccount [token=" + token + ", openId=" + openId
|
||||||
+ ", appId=" + appId + ", appSecret=" + appSecret
|
+ ", appId=" + appId + ", appSecret=" + appSecret
|
||||||
+ ", paySignKey=" + paySignKey + ", partnerId=" + partnerId
|
+ ", encodingAesKey=" + encodingAesKey + ", paySignKey="
|
||||||
+ ", partnerKey=" + partnerKey + ", mchId=" + mchId
|
+ paySignKey + ", partnerId=" + partnerId + ", partnerKey="
|
||||||
+ ", deviceInfo=" + deviceInfo + ", isAlive=" + isAlive
|
+ partnerKey + ", mchId=" + mchId + ", deviceInfo="
|
||||||
+ ", isService=" + isService + ", isSubscribe=" + isSubscribe
|
+ deviceInfo + ", isAlive=" + isAlive + ", isService="
|
||||||
+ "]";
|
+ isService + ", isSubscribe=" + isSubscribe + "]";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -56,16 +56,6 @@ public class BaseMessage extends BaseMsg {
|
|||||||
this.msgType = msgType;
|
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() {
|
public MessageType getMsgType() {
|
||||||
return msgType;
|
return msgType;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,11 +25,6 @@ public class TextMessage extends BaseMessage {
|
|||||||
super(MessageType.text);
|
super(MessageType.text);
|
||||||
}
|
}
|
||||||
|
|
||||||
public TextMessage(String content, BaseMessage inMessage) {
|
|
||||||
super(MessageType.text, inMessage);
|
|
||||||
this.content = content;
|
|
||||||
}
|
|
||||||
|
|
||||||
@XStreamAlias("Content")
|
@XStreamAlias("Content")
|
||||||
private String content; // 消息内容
|
private String content; // 消息内容
|
||||||
|
|
||||||
|
|||||||
@ -20,7 +20,7 @@ public enum MessageType {
|
|||||||
text(TextMessage.class), image(ImageMessage.class), voice(
|
text(TextMessage.class), image(ImageMessage.class), voice(
|
||||||
VoiceMessage.class), video(VideoMessage.class), location(
|
VoiceMessage.class), video(VideoMessage.class), location(
|
||||||
LocationMessage.class), link(LinkMessage.class), event(
|
LocationMessage.class), link(LinkMessage.class), event(
|
||||||
EventMessage.class), signature(null);
|
EventMessage.class);
|
||||||
private Class<? extends BaseMessage> messageClass;
|
private Class<? extends BaseMessage> messageClass;
|
||||||
|
|
||||||
MessageType(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.File;
|
||||||
import java.io.FilenameFilter;
|
import java.io.FilenameFilter;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.JarURLConnection;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
|
import java.util.Enumeration;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Set;
|
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的获取
|
* 对class的获取
|
||||||
|
*
|
||||||
* @className ClassUtil
|
* @className ClassUtil
|
||||||
* @author jy
|
* @author jy
|
||||||
* @date 2014年10月31日
|
* @date 2014年10月31日
|
||||||
@ -17,15 +26,28 @@ import java.util.Set;
|
|||||||
public class ClassUtil {
|
public class ClassUtil {
|
||||||
|
|
||||||
public static Set<Class<?>> getClasses(Package _package) {
|
public static Set<Class<?>> getClasses(Package _package) {
|
||||||
ClassLoader classLoader = Thread.currentThread()
|
String packageName = _package.getName();
|
||||||
.getContextClassLoader();
|
String packageFileName = packageName.replace(".", File.separator);
|
||||||
String subPath = _package.getName().replace(".", File.separator);
|
URL fullPath = Thread.currentThread().getContextClassLoader()
|
||||||
URL fullPath = classLoader.getResource(subPath);
|
.getResource(packageFileName);
|
||||||
|
String protocol = fullPath.getProtocol();
|
||||||
|
if (protocol.equals(Consts.PROTOCOL_FILE)) {
|
||||||
File dir = new File(fullPath.getPath());
|
File dir = new File(fullPath.getPath());
|
||||||
return findClasses(dir, _package.getName());
|
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<?>>();
|
Set<Class<?>> classes = new HashSet<Class<?>>();
|
||||||
File[] files = dir.listFiles(new FilenameFilter() {
|
File[] files = dir.listFiles(new FilenameFilter() {
|
||||||
@Override
|
@Override
|
||||||
@ -35,7 +57,7 @@ public class ClassUtil {
|
|||||||
});
|
});
|
||||||
for (File file : files) {
|
for (File file : files) {
|
||||||
if (file.isDirectory()) {
|
if (file.isDirectory()) {
|
||||||
classes.addAll(findClasses(file,
|
classes.addAll(findClassesByFile(file,
|
||||||
packageName + "." + file.getName()));
|
packageName + "." + file.getName()));
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
@ -48,9 +70,43 @@ public class ClassUtil {
|
|||||||
} catch (ClassNotFoundException e) {
|
} catch (ClassNotFoundException e) {
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return classes;
|
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.io.InputStream;
|
||||||
import java.util.Arrays;
|
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.codec.digest.DigestUtils;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.dom4j.Document;
|
import org.dom4j.Document;
|
||||||
import org.dom4j.DocumentException;
|
import org.dom4j.DocumentException;
|
||||||
import org.dom4j.DocumentHelper;
|
import org.dom4j.DocumentHelper;
|
||||||
import org.dom4j.io.SAXReader;
|
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.msg.BaseMessage;
|
||||||
import com.foxinmy.weixin4j.type.EventType;
|
import com.foxinmy.weixin4j.type.EventType;
|
||||||
import com.foxinmy.weixin4j.type.MessageType;
|
import com.foxinmy.weixin4j.type.MessageType;
|
||||||
@ -19,6 +24,7 @@ import com.foxinmy.weixin4j.xml.XStream;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 消息工具类
|
* 消息工具类
|
||||||
|
*
|
||||||
* @className MessageUtil
|
* @className MessageUtil
|
||||||
* @author jy
|
* @author jy
|
||||||
* @date 2014年10月31日
|
* @date 2014年10月31日
|
||||||
@ -27,20 +33,9 @@ import com.foxinmy.weixin4j.xml.XStream;
|
|||||||
*/
|
*/
|
||||||
public class MessageUtil {
|
public class MessageUtil {
|
||||||
|
|
||||||
private final static Logger log = LoggerFactory
|
|
||||||
.getLogger(MessageUtil.class);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 验证微信签名
|
* 验证微信签名
|
||||||
*
|
*
|
||||||
* @param token
|
|
||||||
* 开发者填写的token
|
|
||||||
* @param echostr
|
|
||||||
* 随机字符串
|
|
||||||
* @param timestamp
|
|
||||||
* 时间戳
|
|
||||||
* @param nonce
|
|
||||||
* 随机数
|
|
||||||
* @param signature
|
* @param signature
|
||||||
* 微信加密签名,signature结合了开发者填写的token参数和请求中的timestamp参数、nonce参数
|
* 微信加密签名,signature结合了开发者填写的token参数和请求中的timestamp参数、nonce参数
|
||||||
* @return 开发者通过检验signature对请求进行相关校验。若确认此次GET请求来自微信服务器
|
* @return 开发者通过检验signature对请求进行相关校验。若确认此次GET请求来自微信服务器
|
||||||
@ -48,35 +43,126 @@ public class MessageUtil {
|
|||||||
* @see <a
|
* @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>
|
* 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,
|
public static String signature(String... para) {
|
||||||
String timestamp, String nonce, String signature) {
|
Arrays.sort(para);
|
||||||
if (StringUtils.isBlank(token)) {
|
StringBuilder sb = new StringBuilder();
|
||||||
log.error("signature fail : token is null!");
|
for (String str : para) {
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (StringUtils.isBlank(echostr) || StringUtils.isBlank(timestamp)
|
|
||||||
|| StringUtils.isBlank(nonce)) {
|
|
||||||
log.error("signature fail : invalid parameter!");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String _signature = null;
|
|
||||||
try {
|
|
||||||
String[] a = { token, timestamp, nonce };
|
|
||||||
Arrays.sort(a);
|
|
||||||
StringBuilder sb = new StringBuilder(3);
|
|
||||||
for (String str : a) {
|
|
||||||
sb.append(str);
|
sb.append(str);
|
||||||
}
|
}
|
||||||
_signature = DigestUtils.sha1Hex(sb.toString());
|
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 {
|
||||||
|
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) {
|
} 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -48,18 +48,26 @@ weixin4j-mp
|
|||||||
|
|
||||||
+ `weixin-mp`分离为`weixin-mp-api`和`weixin-mp-server`两个工程
|
+ `weixin-mp`分离为`weixin-mp-api`和`weixin-mp-server`两个工程
|
||||||
|
|
||||||
+ **weixin-mp**: 新增`支付`模块
|
+ **weixin4j-mp**: 新增`支付`模块
|
||||||
|
|
||||||
* 2014-11-06
|
* 2014-11-06
|
||||||
|
|
||||||
+ **weixin-mp-api**: 新增V3版本`退款接口`
|
+ **weixin4j-mp-api**: 新增V3版本`退款接口`
|
||||||
|
|
||||||
* 2014-11-08
|
* 2014-11-08
|
||||||
|
|
||||||
+ **weixin-mp-api**: 新增V2版本`退款申请`、`退款查询`、`对账单下载`三个接口
|
+ **weixin4j-mp-api**: 新增V2版本`退款申请`、`退款查询`、`对账单下载`三个接口
|
||||||
|
|
||||||
+ **weixin-mp-api**: 新增一个简单的`语义理解`接口
|
+ **weixin4j-mp-api**: 新增一个简单的`语义理解`接口
|
||||||
|
|
||||||
* 2014-11-11
|
* 2014-11-11
|
||||||
|
|
||||||
+ **weixin-mp-api**: 自定义`assembly`将`weixin4j-base`工程也一起打包(`weixin4j-mp-api-full.jar`)
|
+ **weixin4j-mp-api**: 自定义`assembly`将`weixin4j-base`工程也一起打包(`weixin4j-mp-api-full.jar`)
|
||||||
|
|
||||||
|
* 2014-11-15
|
||||||
|
|
||||||
|
+ **weixin4j-mp-api**: 新增获取`微信服务器IP地址接口`
|
||||||
|
|
||||||
|
+ **weixin4j-mp-server**: 解决`server工程`打包不能运行问题(`ClassUtil`无法获取jar包里面的类)
|
||||||
|
|
||||||
|
+ **weixin4j-mp-server**: 新增被动消息的`加密`以及回复消息的`解密`
|
||||||
@ -46,6 +46,7 @@ weixin.properties说明
|
|||||||
|
|
||||||
> account={"appId":"appId","appSecret":"appSecret",
|
> account={"appId":"appId","appSecret":"appSecret",
|
||||||
> "token":"开放者的token 非必须","openId":"公众号的openid 非必须",
|
> "token":"开放者的token 非必须","openId":"公众号的openid 非必须",
|
||||||
|
> "encodingAesKey":"公众号设置了加密方式且为「安全模式」需要填入",
|
||||||
> "mchId":"V3.x版本下的微信商户号",
|
> "mchId":"V3.x版本下的微信商户号",
|
||||||
> "partnerId":"财付通的商户号","partnerKey":"财付通商户权限密钥Key",
|
> "partnerId":"财付通的商户号","partnerKey":"财付通商户权限密钥Key",
|
||||||
> "version":"针对微信支付的版本号(2,3),如果不填则按照mchId非空与否来判断",
|
> "version":"针对微信支付的版本号(2,3),如果不填则按照mchId非空与否来判断",
|
||||||
@ -104,3 +105,7 @@ weixin.properties说明
|
|||||||
* 2014-11-11
|
* 2014-11-11
|
||||||
|
|
||||||
+ 自定义`assembly`将`weixin4j-base`工程也一起打包(`weixin4j-mp-api-full.jar`)
|
+ 自定义`assembly`将`weixin4j-base`工程也一起打包(`weixin4j-mp-api-full.jar`)
|
||||||
|
|
||||||
|
* 2014-11-15
|
||||||
|
|
||||||
|
+ 新增获取`微信服务器IP地址接口`
|
||||||
@ -1,5 +1,8 @@
|
|||||||
package com.foxinmy.weixin4j.mp.api;
|
package com.foxinmy.weixin4j.mp.api;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson.JSON;
|
||||||
import com.alibaba.fastjson.JSONObject;
|
import com.alibaba.fastjson.JSONObject;
|
||||||
import com.alibaba.fastjson.TypeReference;
|
import com.alibaba.fastjson.TypeReference;
|
||||||
import com.foxinmy.weixin4j.exception.WeixinException;
|
import com.foxinmy.weixin4j.exception.WeixinException;
|
||||||
@ -72,4 +75,21 @@ public class HelperApi extends BaseApi {
|
|||||||
return response.getAsObject(new TypeReference<SemResult>() {
|
return response.getAsObject(new TypeReference<SemResult>() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取微信服务器IP地址
|
||||||
|
*
|
||||||
|
* @return IP地址
|
||||||
|
* @see <a
|
||||||
|
* href="http://mp.weixin.qq.com/wiki/index.php?title=%E8%8E%B7%E5%8F%96%E5%BE%AE%E4%BF%A1%E6%9C%8D%E5%8A%A1%E5%99%A8IP%E5%9C%B0%E5%9D%80">获取IP地址</a>
|
||||||
|
* @throws WeixinException
|
||||||
|
*/
|
||||||
|
public List<String> getcallbackip() throws WeixinException {
|
||||||
|
String getcallbackip_uri = getRequestUri("getcallbackip_uri");
|
||||||
|
Token token = tokenHolder.getToken();
|
||||||
|
Response response = request.post(String.format(getcallbackip_uri,
|
||||||
|
token.getAccessToken()));
|
||||||
|
return JSON.parseArray(response.getAsJson().getString("ip_list"),
|
||||||
|
String.class);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -72,6 +72,8 @@ updateremark_uri={api_cgi_url}/user/info/updateremark?access_token=%s
|
|||||||
template_send_uri={api_cgi_url}/message/template/send?access_token=%s
|
template_send_uri={api_cgi_url}/message/template/send?access_token=%s
|
||||||
# \u8bed\u4e49\u7406\u89e3
|
# \u8bed\u4e49\u7406\u89e3
|
||||||
semantic_uri={api_base_url}/semantic/semproxy/search?access_token=%s
|
semantic_uri={api_base_url}/semantic/semproxy/search?access_token=%s
|
||||||
|
# \u5fae\u4fe1\u670d\u52a1\u5730\u5740
|
||||||
|
getcallbackip_uri={api_cgi_url}/getcallbackip?access_token=%s
|
||||||
|
|
||||||
# \u8ba2\u5355\u67e5\u8be2
|
# \u8ba2\u5355\u67e5\u8be2
|
||||||
orderquery_uri={api_base_url}/pay/orderquery?access_token=%s
|
orderquery_uri={api_base_url}/pay/orderquery?access_token=%s
|
||||||
|
|||||||
@ -49,7 +49,6 @@ public class PayAction {
|
|||||||
// V3 支付
|
// V3 支付
|
||||||
// 此处的openid为微信用户的openid
|
// 此处的openid为微信用户的openid
|
||||||
WeixinAccount weixinAccount = ConfigUtil.getWeixinAccount();
|
WeixinAccount weixinAccount = ConfigUtil.getWeixinAccount();
|
||||||
weixinAccount.setOpenId("用户的openId");
|
|
||||||
payPackage = new PayPackageV3(weixinAccount, "用户openid", "商品描述",
|
payPackage = new PayPackageV3(weixinAccount, "用户openid", "商品描述",
|
||||||
"系统内部订单号", 1d, "IP地址", TradeType.JSAPI);
|
"系统内部订单号", 1d, "IP地址", TradeType.JSAPI);
|
||||||
// V2 支付
|
// V2 支付
|
||||||
@ -147,7 +146,7 @@ public class PayAction {
|
|||||||
*
|
*
|
||||||
*
|
*
|
||||||
* @param inputStream
|
* @param inputStream
|
||||||
* 订单细腻
|
* 订单回调
|
||||||
* @return <xml><br>
|
* @return <xml><br>
|
||||||
* <return_code>SUCCESS/FAIL</return_code><br>
|
* <return_code>SUCCESS/FAIL</return_code><br>
|
||||||
* <return_msg>如非空,为错误 原因签名失败参数格式校验错误</return_msg><br>
|
* <return_msg>如非空,为错误 原因签名失败参数格式校验错误</return_msg><br>
|
||||||
|
|||||||
@ -0,0 +1,5 @@
|
|||||||
|
package com.foxinmy.weixin4j.mp.type;
|
||||||
|
|
||||||
|
public enum EncryptType {
|
||||||
|
RAW, AES
|
||||||
|
}
|
||||||
@ -2,6 +2,7 @@
|
|||||||
# \u516c\u4f17\u53f7\u4fe1\u606f
|
# \u516c\u4f17\u53f7\u4fe1\u606f
|
||||||
account={"appId":"wx4ab8f8de58159a57","appSecret":"1d4eb0f4bf556aaed539f30ed05ca795",\
|
account={"appId":"wx4ab8f8de58159a57","appSecret":"1d4eb0f4bf556aaed539f30ed05ca795",\
|
||||||
"token":"\u5f00\u653e\u8005\u7684token \u975e\u5fc5\u987b","openId":"\u516c\u4f17\u53f7\u7684openid \u975e\u5fc5\u987b",\
|
"token":"\u5f00\u653e\u8005\u7684token \u975e\u5fc5\u987b","openId":"\u516c\u4f17\u53f7\u7684openid \u975e\u5fc5\u987b",\
|
||||||
|
"encodingAesKey":"\u516c\u4f17\u53f7\u8bbe\u7f6e\u4e86\u52a0\u5bc6\u65b9\u5f0f\u4e14\u4e3a\u300c\u5b89\u5168\u6a21\u5f0f\u300d\u9700\u8981\u586b\u5165",\
|
||||||
"mchId":"V3.x\u7248\u672c\u4e0b\u7684\u5fae\u4fe1\u5546\u6237\u53f7",\
|
"mchId":"V3.x\u7248\u672c\u4e0b\u7684\u5fae\u4fe1\u5546\u6237\u53f7",\
|
||||||
"version":3,\
|
"version":3,\
|
||||||
"partnerId":"\u8d22\u4ed8\u901a\u7684\u5546\u6237\u53f7","partnerKey":"\u8d22\u4ed8\u901a\u5546\u6237\u6743\u9650\u5bc6\u94a5Key",\
|
"partnerId":"\u8d22\u4ed8\u901a\u7684\u5546\u6237\u53f7","partnerKey":"\u8d22\u4ed8\u901a\u5546\u6237\u6743\u9650\u5bc6\u94a5Key",\
|
||||||
|
|||||||
@ -0,0 +1,25 @@
|
|||||||
|
package com.foxinmy.weixin4j.mp.test;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import com.foxinmy.weixin4j.exception.WeixinException;
|
||||||
|
import com.foxinmy.weixin4j.mp.api.HelperApi;
|
||||||
|
|
||||||
|
public class HelpTest extends TokenTest {
|
||||||
|
private HelperApi helperApi;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void init() {
|
||||||
|
helperApi = new HelperApi(tokenHolder);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getcallbackip() throws WeixinException {
|
||||||
|
List<String> ipList = helperApi.getcallbackip();
|
||||||
|
Assert.assertFalse(ipList.isEmpty());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,64 @@
|
|||||||
|
package com.foxinmy.weixin4j.mp.test.msg;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import com.foxinmy.weixin4j.exception.WeixinException;
|
||||||
|
|
||||||
|
public class AesMsgTest extends MessagePush {
|
||||||
|
StringBuilder xmlSb = new StringBuilder();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testValidate() throws WeixinException, IOException {
|
||||||
|
String para = "?signature=0d2366aedb4f3531cfa4297c1e4ea7eece2311d9&echostr=2143641595566077626×tamp=1415951914&nonce=165976363";
|
||||||
|
xmlSb.delete(0, xmlSb.length());
|
||||||
|
String response = get(para);
|
||||||
|
Assert.assertEquals("2143641595566077626", response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testType1() throws WeixinException, IOException {
|
||||||
|
String para = "?signature=6dd806a20a314723e78bc58742a1b98a7adfd151×tamp=1415979366&nonce=1865915590";
|
||||||
|
xmlSb.delete(0, xmlSb.length());
|
||||||
|
xmlSb.append("<xml>");
|
||||||
|
xmlSb.append("<ToUserName><![CDATA[gh_248c6f91d64f]]></ToUserName>");
|
||||||
|
xmlSb.append("<FromUserName><![CDATA[oyFLst1bqtuTcxK-ojF8hOGtLQao]]></FromUserName>");
|
||||||
|
xmlSb.append("<CreateTime>1415979365</CreateTime>");
|
||||||
|
xmlSb.append("<MsgType><![CDATA[event]]></MsgType>");
|
||||||
|
xmlSb.append("<Event><![CDATA[CLICK]]></Event>");
|
||||||
|
xmlSb.append("<EventKey><![CDATA[CHECKIN]]></EventKey>");
|
||||||
|
xmlSb.append("</xml>");
|
||||||
|
String response = push(para, xmlSb.toString());
|
||||||
|
Assert.assertNotNull(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testType2() throws WeixinException, IOException {
|
||||||
|
String para = "?signature=ad05f836772d1cbba1ff2edb7be4b9c9fb3a43d5×tamp=1415980001&nonce=1803216865&encrypt_type=raw&msg_signature=c0d38e9ca00548f7142627ec2908a4fe8481025e";
|
||||||
|
xmlSb.delete(0, xmlSb.length());
|
||||||
|
xmlSb.append("<xml>");
|
||||||
|
xmlSb.append("<ToUserName><![CDATA[gh_248c6f91d64f]]></ToUserName>");
|
||||||
|
xmlSb.append("<FromUserName><![CDATA[oyFLst1bqtuTcxK-ojF8hOGtLQao]]></FromUserName>");
|
||||||
|
xmlSb.append("<CreateTime>1415980001</CreateTime>");
|
||||||
|
xmlSb.append("<MsgType><![CDATA[event]]></MsgType>");
|
||||||
|
xmlSb.append("<Event><![CDATA[CLICK]]></Event>");
|
||||||
|
xmlSb.append("<EventKey><![CDATA[CHECKIN]]></EventKey>");
|
||||||
|
xmlSb.append("</xml>");
|
||||||
|
String response = push(para, xmlSb.toString());
|
||||||
|
Assert.assertNotNull(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testType3() throws WeixinException, IOException {
|
||||||
|
String para = "?signature=ad05f836772d1cbba1ff2edb7be4b9c9fb3a43d5×tamp=1415980001&nonce=1803216865&encrypt_type=aes&msg_signature=c0d38e9ca00548f7142627ec2908a4fe8481025e";
|
||||||
|
xmlSb.delete(0, xmlSb.length());
|
||||||
|
xmlSb.append("<xml>");
|
||||||
|
xmlSb.append("<ToUserName><![CDATA[gh_248c6f91d64f]]></ToUserName>");
|
||||||
|
xmlSb.append("<Encrypt><![CDATA[R6VQIWDR14XgSRLm25zc7V/WJYqK15gsUiMh0u/5GTMZME6jGtHkyfVN079ZPL065b+ZDq3TnoFKKtjtZlzcodY6Fm8+EujvtbTdVMMFSwdo8AwqVViAn09+DDfqPaNvbHUSiYsL3qlxArs1MH6APRUHFo7MU/piY1x2stJc8+kv28xtF+K8Aou0RuPO7PeQ18Zu/GkLnYNiI1E7UG31UYfOgVKcRjeE0PXa18iF5LBS8G/ce/l+/pH/DJWUBw5uXaqSOlo21tctlgLXu3bYUUkIu8tT49QwhHvRZILtO9icoyCNuTA7iTcHIdlAe1bD1S0ncmopIQCGmoU2/AXC2CCi6trONf3EPNKKKfDeQYHadnVZOg6kTX2cnYmHZLviYeLzjCKFSqSNkimoSKQ/Dcutpsq1D82NCwiExUZW4oo=]]></Encrypt>");
|
||||||
|
xmlSb.append("</xml>");
|
||||||
|
String response = push(para, xmlSb.toString());
|
||||||
|
Assert.assertNotNull(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,12 +3,12 @@ package com.foxinmy.weixin4j.mp.test.msg;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.ResourceBundle;
|
|
||||||
|
|
||||||
import org.apache.http.HttpResponse;
|
import org.apache.http.HttpResponse;
|
||||||
import org.apache.http.HttpStatus;
|
import org.apache.http.HttpStatus;
|
||||||
import org.apache.http.StatusLine;
|
import org.apache.http.StatusLine;
|
||||||
import org.apache.http.client.HttpClient;
|
import org.apache.http.client.HttpClient;
|
||||||
|
import org.apache.http.client.methods.HttpGet;
|
||||||
import org.apache.http.client.methods.HttpPost;
|
import org.apache.http.client.methods.HttpPost;
|
||||||
import org.apache.http.entity.StringEntity;
|
import org.apache.http.entity.StringEntity;
|
||||||
import org.apache.http.impl.client.DefaultHttpClient;
|
import org.apache.http.impl.client.DefaultHttpClient;
|
||||||
@ -18,20 +18,40 @@ import com.foxinmy.weixin4j.exception.WeixinException;
|
|||||||
|
|
||||||
public class MessagePush {
|
public class MessagePush {
|
||||||
|
|
||||||
private HttpClient httpClient;
|
private final String server = "http://localhost:8090";
|
||||||
|
private final HttpClient httpClient;
|
||||||
private final HttpPost httpPost;
|
private final HttpPost httpPost;
|
||||||
|
private final HttpGet httpGet;
|
||||||
|
|
||||||
public MessagePush() {
|
public MessagePush() {
|
||||||
this.httpClient = new DefaultHttpClient();
|
httpClient = new DefaultHttpClient();
|
||||||
ResourceBundle config = ResourceBundle.getBundle("netty");
|
|
||||||
httpPost = new HttpPost();
|
httpPost = new HttpPost();
|
||||||
httpPost.setURI(URI.create(String.format("http://localhost:%s",
|
httpPost.setURI(URI.create(server));
|
||||||
Integer.parseInt(config.getString("port")))));
|
|
||||||
|
httpGet = new HttpGet();
|
||||||
|
httpGet.setURI(URI.create(server));
|
||||||
|
}
|
||||||
|
|
||||||
|
public String get(String para) throws WeixinException, IOException {
|
||||||
|
httpGet.setURI(URI.create(server + para));
|
||||||
|
HttpResponse httpResponse = httpClient.execute(httpGet);
|
||||||
|
return entity(httpResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String push(String xml) throws WeixinException, IOException {
|
public String push(String xml) throws WeixinException, IOException {
|
||||||
|
return push("", xml);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String push(String para, String xml) throws WeixinException,
|
||||||
|
IOException {
|
||||||
|
httpPost.setURI(URI.create(server + para));
|
||||||
httpPost.setEntity(new StringEntity(xml, StandardCharsets.UTF_8));
|
httpPost.setEntity(new StringEntity(xml, StandardCharsets.UTF_8));
|
||||||
HttpResponse httpResponse = httpClient.execute(httpPost);
|
HttpResponse httpResponse = httpClient.execute(httpPost);
|
||||||
|
return entity(httpResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String entity(HttpResponse httpResponse) throws WeixinException,
|
||||||
|
IOException {
|
||||||
StatusLine statusLine = httpResponse.getStatusLine();
|
StatusLine statusLine = httpResponse.getStatusLine();
|
||||||
|
|
||||||
int status = statusLine.getStatusCode();
|
int status = statusLine.getStatusCode();
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import org.junit.Test;
|
|||||||
import com.foxinmy.weixin4j.mp.response.ArticleResponse;
|
import com.foxinmy.weixin4j.mp.response.ArticleResponse;
|
||||||
import com.foxinmy.weixin4j.mp.response.ImageResponse;
|
import com.foxinmy.weixin4j.mp.response.ImageResponse;
|
||||||
import com.foxinmy.weixin4j.mp.response.MusicResponse;
|
import com.foxinmy.weixin4j.mp.response.MusicResponse;
|
||||||
|
import com.foxinmy.weixin4j.mp.response.TextResponse;
|
||||||
import com.foxinmy.weixin4j.mp.response.VideoResponse;
|
import com.foxinmy.weixin4j.mp.response.VideoResponse;
|
||||||
import com.foxinmy.weixin4j.mp.response.VoiceResponse;
|
import com.foxinmy.weixin4j.mp.response.VoiceResponse;
|
||||||
import com.foxinmy.weixin4j.msg.BaseMessage;
|
import com.foxinmy.weixin4j.msg.BaseMessage;
|
||||||
@ -31,7 +32,7 @@ public class OutMsgTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void text() throws DocumentException {
|
public void text() throws DocumentException {
|
||||||
TextMessage message = new TextMessage("text", inMessage);
|
TextResponse message = new TextResponse("text", inMessage);
|
||||||
System.out.println(message.toXml());
|
System.out.println(message.toXml());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -28,6 +28,7 @@ weixin4j-mp-server
|
|||||||
|
|
||||||
> account={"appId":"appId","appSecret":"appSecret",
|
> account={"appId":"appId","appSecret":"appSecret",
|
||||||
> "token":"开放者的token 非必须","openId":"公众号的openid 非必须",
|
> "token":"开放者的token 非必须","openId":"公众号的openid 非必须",
|
||||||
|
> "encodingAesKey":"公众号设置了加密方式且为「安全模式」需要填入",
|
||||||
> "mchId":"V3.x版本下的微信商户号",
|
> "mchId":"V3.x版本下的微信商户号",
|
||||||
> "partnerId":"财付通的商户号","partnerKey":"财付通商户权限密钥Key",
|
> "partnerId":"财付通的商户号","partnerKey":"财付通商户权限密钥Key",
|
||||||
> "version":"针对微信支付的版本号(目前可能为2,3),如果不填则按照mchId非空与否来做判断",
|
> "version":"针对微信支付的版本号(目前可能为2,3),如果不填则按照mchId非空与否来做判断",
|
||||||
@ -49,3 +50,9 @@ weixin4j-mp-server
|
|||||||
* 2014-11-03
|
* 2014-11-03
|
||||||
|
|
||||||
+ 得到`weixin-mp-server`工程
|
+ 得到`weixin-mp-server`工程
|
||||||
|
|
||||||
|
* 2014-11-15
|
||||||
|
|
||||||
|
+ 解决`server工程`打包不能运行问题(`ClassUtil`无法获取jar包里面的类)
|
||||||
|
|
||||||
|
+ 新增被动消息的`加密`以及回复消息的`解密`
|
||||||
@ -13,7 +13,7 @@
|
|||||||
<url>https://github.com/foxinmy/weixin4j/tree/master/weixin4j-mp/weixin4j-server</url>
|
<url>https://github.com/foxinmy/weixin4j/tree/master/weixin4j-mp/weixin4j-server</url>
|
||||||
<description>微信公众号服务</description>
|
<description>微信公众号服务</description>
|
||||||
<build>
|
<build>
|
||||||
<finalName>weixin4j-mp-server</finalName>
|
<finalName>weixin-mp-server</finalName>
|
||||||
<plugins>
|
<plugins>
|
||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
<formats>
|
<formats>
|
||||||
<format>zip</format>
|
<format>zip</format>
|
||||||
</formats>
|
</formats>
|
||||||
|
<includeBaseDirectory>true</includeBaseDirectory>
|
||||||
<dependencySets>
|
<dependencySets>
|
||||||
<dependencySet>
|
<dependencySet>
|
||||||
<useProjectArtifact>true</useProjectArtifact>
|
<useProjectArtifact>true</useProjectArtifact>
|
||||||
|
|||||||
@ -17,28 +17,27 @@ import com.foxinmy.weixin4j.xml.XStream;
|
|||||||
* @author jy
|
* @author jy
|
||||||
* @date 2014年10月12日
|
* @date 2014年10月12日
|
||||||
* @since JDK 1.7
|
* @since JDK 1.7
|
||||||
* @see
|
* @see com.foxinmy.weixin4j.mp.action.WeixinAction
|
||||||
*/
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
public abstract class AbstractAction<M extends BaseMessage> implements
|
public abstract class AbstractAction<M extends BaseMessage> implements
|
||||||
WeixinAction {
|
WeixinAction {
|
||||||
|
|
||||||
public abstract BaseResponse execute(M inMessage);
|
public abstract BaseResponse execute(M inMessage);
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
@Override
|
@Override
|
||||||
public String execute(String msg) throws DocumentException {
|
public BaseResponse execute(String msg) throws DocumentException {
|
||||||
BaseMessage message = MessageUtil.xml2msg(msg);
|
BaseMessage message = MessageUtil.xml2msg(msg);
|
||||||
if (message == null) {
|
if (message == null) {
|
||||||
Class<M> messageClass = getGenericType();
|
Class<M> messageClass = getGenericType();
|
||||||
XStream xstream = XStream.get();
|
XStream xstream = XStream.get();
|
||||||
xstream.processAnnotations(messageClass);
|
xstream.processAnnotations(messageClass);
|
||||||
xstream.alias("xml", messageClass);
|
xstream.alias("xml", messageClass);
|
||||||
return execute(xstream.fromXML(msg, messageClass)).toXml();
|
return execute(xstream.fromXML(msg, messageClass));
|
||||||
}
|
}
|
||||||
return execute((M) message).toXml();
|
return execute((M) message);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
private Class<M> getGenericType() {
|
private Class<M> getGenericType() {
|
||||||
Class<M> clazz = null;
|
Class<M> clazz = null;
|
||||||
Type type = getClass().getGenericSuperclass();
|
Type type = getClass().getGenericSuperclass();
|
||||||
|
|||||||
@ -1,21 +1,21 @@
|
|||||||
package com.foxinmy.weixin4j.mp.action;
|
package com.foxinmy.weixin4j.mp.action;
|
||||||
|
|
||||||
import com.foxinmy.weixin4j.mp.response.TextResponse;
|
import com.foxinmy.weixin4j.mp.response.BaseResponse;
|
||||||
import com.foxinmy.weixin4j.msg.BaseMessage;
|
import com.foxinmy.weixin4j.msg.BaseMessage;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 输出空白消息
|
* 回复一个空字符串 而不是一个XML结构体中content字段的内容为空
|
||||||
*
|
*
|
||||||
* @className BlankAction
|
* @className BlankAction
|
||||||
* @author jy.hu
|
* @author jy.hu
|
||||||
* @date 2014年10月2日
|
* @date 2014年10月2日
|
||||||
* @since JDK 1.7
|
* @since JDK 1.7
|
||||||
* @see
|
* @see com.foxinmy.weixin4j.mp.action.AbstractAction
|
||||||
*/
|
*/
|
||||||
public class BlankAction<M extends BaseMessage> extends AbstractAction<M> {
|
public class BlankAction<M extends BaseMessage> extends AbstractAction<M> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public TextResponse execute(M inMessage) {
|
public BaseResponse execute(M inMessage) {
|
||||||
return new TextResponse("", inMessage);
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,48 +0,0 @@
|
|||||||
package com.foxinmy.weixin4j.mp.action;
|
|
||||||
|
|
||||||
import io.netty.handler.codec.http.QueryStringDecoder;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import org.dom4j.DocumentException;
|
|
||||||
|
|
||||||
import com.foxinmy.weixin4j.mp.mapping.Action;
|
|
||||||
import com.foxinmy.weixin4j.type.MessageType;
|
|
||||||
import com.foxinmy.weixin4j.util.ConfigUtil;
|
|
||||||
import com.foxinmy.weixin4j.util.MessageUtil;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 用于校验消息是否来自微信
|
|
||||||
*
|
|
||||||
* @className SignatureAction
|
|
||||||
* @author jy
|
|
||||||
* @date 2014年10月24日
|
|
||||||
* @since JDK 1.7
|
|
||||||
* @see
|
|
||||||
*/
|
|
||||||
@Action(msgType = MessageType.signature)
|
|
||||||
public class SignatureAction implements WeixinAction {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String execute(String uri) throws DocumentException {
|
|
||||||
String[] paths = uri.split("\\?");
|
|
||||||
if (paths == null || paths.length < 2) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
QueryStringDecoder queryDecoder = new QueryStringDecoder(paths[1],
|
|
||||||
false);
|
|
||||||
Map<String, List<String>> parameters = queryDecoder.parameters();
|
|
||||||
String echostr = parameters.containsKey("echostr") ? parameters.get(
|
|
||||||
"echostr").get(0) : null;
|
|
||||||
String timestamp = parameters.containsKey("timestamp") ? parameters
|
|
||||||
.get("timestamp").get(0) : null;
|
|
||||||
String nonce = parameters.containsKey("nonce") ? parameters
|
|
||||||
.get("nonce").get(0) : null;
|
|
||||||
String signature = parameters.containsKey("signature") ? parameters
|
|
||||||
.get("signature").get(0) : null;
|
|
||||||
String token = ConfigUtil.getValue("app_token");
|
|
||||||
return MessageUtil.signature(token, echostr, timestamp, nonce,
|
|
||||||
signature);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -2,6 +2,8 @@ package com.foxinmy.weixin4j.mp.action;
|
|||||||
|
|
||||||
import org.dom4j.DocumentException;
|
import org.dom4j.DocumentException;
|
||||||
|
|
||||||
|
import com.foxinmy.weixin4j.mp.response.BaseResponse;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 消息处理接口
|
* 消息处理接口
|
||||||
*
|
*
|
||||||
@ -14,5 +16,5 @@ import org.dom4j.DocumentException;
|
|||||||
* @see com.foxinmy.weixin4j.mp.action.DebugAction
|
* @see com.foxinmy.weixin4j.mp.action.DebugAction
|
||||||
*/
|
*/
|
||||||
public interface WeixinAction {
|
public interface WeixinAction {
|
||||||
public String execute(String msg) throws DocumentException;
|
public BaseResponse execute(String msg) throws DocumentException;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,133 @@
|
|||||||
|
package com.foxinmy.weixin4j.mp.model;
|
||||||
|
|
||||||
|
import io.netty.handler.codec.http.HttpMethod;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
import com.foxinmy.weixin4j.mp.type.EncryptType;
|
||||||
|
import com.thoughtworks.xstream.annotations.XStreamAlias;
|
||||||
|
|
||||||
|
@XStreamAlias("xml")
|
||||||
|
public class HttpWeixinMessage implements Serializable {
|
||||||
|
private static final long serialVersionUID = -9157395300510879866L;
|
||||||
|
|
||||||
|
// 以下字段是加密方式为「安全模式」时的参数
|
||||||
|
@XStreamAlias("ToUserName")
|
||||||
|
private String toUserName;
|
||||||
|
@XStreamAlias("Encrypt")
|
||||||
|
private String encryptContent;
|
||||||
|
private EncryptType encryptType;
|
||||||
|
private String msgSignature;
|
||||||
|
|
||||||
|
// 以下字段每次被动消息时都会带上
|
||||||
|
private String echoStr;
|
||||||
|
private String timeStamp;
|
||||||
|
private String nonce;
|
||||||
|
private String signature;
|
||||||
|
|
||||||
|
private String token;
|
||||||
|
|
||||||
|
// xml消息主体
|
||||||
|
private String xmlContent;
|
||||||
|
|
||||||
|
// request method
|
||||||
|
private HttpMethod method;
|
||||||
|
|
||||||
|
public String getToUserName() {
|
||||||
|
return toUserName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setToUserName(String toUserName) {
|
||||||
|
this.toUserName = toUserName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getEncryptContent() {
|
||||||
|
return encryptContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEncryptContent(String encryptContent) {
|
||||||
|
this.encryptContent = encryptContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public EncryptType getEncryptType() {
|
||||||
|
return encryptType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEncryptType(EncryptType encryptType) {
|
||||||
|
this.encryptType = encryptType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMsgSignature() {
|
||||||
|
return msgSignature;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMsgSignature(String msgSignature) {
|
||||||
|
this.msgSignature = msgSignature;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getEchoStr() {
|
||||||
|
return echoStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEchoStr(String echoStr) {
|
||||||
|
this.echoStr = echoStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTimeStamp() {
|
||||||
|
return timeStamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTimeStamp(String timeStamp) {
|
||||||
|
this.timeStamp = timeStamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getNonce() {
|
||||||
|
return nonce;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setNonce(String nonce) {
|
||||||
|
this.nonce = nonce;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSignature() {
|
||||||
|
return signature;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSignature(String signature) {
|
||||||
|
this.signature = signature;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getToken() {
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setToken(String token) {
|
||||||
|
this.token = token;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getXmlContent() {
|
||||||
|
return xmlContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setXmlContent(String xmlContent) {
|
||||||
|
this.xmlContent = xmlContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public HttpMethod getMethod() {
|
||||||
|
return method;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMethod(HttpMethod method) {
|
||||||
|
this.method = method;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "HttpMessage [toUserName=" + toUserName + ", encryptContent="
|
||||||
|
+ encryptContent + ", encryptType=" + encryptType
|
||||||
|
+ ", msgSignature=" + msgSignature + ", echoStr=" + echoStr
|
||||||
|
+ ", timeStamp=" + timeStamp + ", nonce=" + nonce
|
||||||
|
+ ", signature=" + signature + ", token=" + token
|
||||||
|
+ ", xmlContent=" + xmlContent + ", method=" + method + "]";
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1 +1,5 @@
|
|||||||
微信服务netty启动类
|
WeixinMessageDecoder:对微信消息进行解码
|
||||||
|
|
||||||
|
WeixinMessageEncoder:对微信消息进行编码
|
||||||
|
|
||||||
|
WeixinServerHandler:微信请求处理类
|
||||||
@ -0,0 +1,81 @@
|
|||||||
|
package com.foxinmy.weixin4j.mp.server;
|
||||||
|
|
||||||
|
import io.netty.channel.ChannelHandlerContext;
|
||||||
|
import io.netty.handler.codec.MessageToMessageDecoder;
|
||||||
|
import io.netty.handler.codec.http.FullHttpRequest;
|
||||||
|
import io.netty.handler.codec.http.QueryStringDecoder;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.apache.http.Consts;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import com.foxinmy.weixin4j.model.WeixinAccount;
|
||||||
|
import com.foxinmy.weixin4j.mp.model.HttpWeixinMessage;
|
||||||
|
import com.foxinmy.weixin4j.mp.type.EncryptType;
|
||||||
|
import com.foxinmy.weixin4j.util.ConfigUtil;
|
||||||
|
import com.foxinmy.weixin4j.util.MessageUtil;
|
||||||
|
import com.foxinmy.weixin4j.xml.XStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信消息解码类
|
||||||
|
*
|
||||||
|
* @className WeixinMessageDecoder
|
||||||
|
* @author jy
|
||||||
|
* @date 2014年11月13日
|
||||||
|
* @since JDK 1.7
|
||||||
|
* @see <a
|
||||||
|
* href="http://mp.weixin.qq.com/wiki/index.php?title=%E6%8E%A5%E5%85%A5%E6%8C%87%E5%BC%95">加密接入指引</a>
|
||||||
|
*/
|
||||||
|
public class WeixinMessageDecoder extends
|
||||||
|
MessageToMessageDecoder<FullHttpRequest> {
|
||||||
|
private final Logger log = LoggerFactory.getLogger(getClass());
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void decode(ChannelHandlerContext ctx, FullHttpRequest req,
|
||||||
|
List<Object> out) throws Exception {
|
||||||
|
WeixinAccount account = ConfigUtil.getWeixinAccount();
|
||||||
|
String xmlContent = req.content().toString(Consts.UTF_8);
|
||||||
|
HttpWeixinMessage message = new HttpWeixinMessage();
|
||||||
|
if (StringUtils.isNotBlank(xmlContent)) {
|
||||||
|
message = XStream.get(xmlContent, HttpWeixinMessage.class);
|
||||||
|
}
|
||||||
|
message.setMethod(req.getMethod());
|
||||||
|
QueryStringDecoder queryDecoder = new QueryStringDecoder(req.getUri(),
|
||||||
|
true);
|
||||||
|
log.info("\n=================receive request=================");
|
||||||
|
log.info("{}", req.getMethod());
|
||||||
|
log.info("{}", req.getUri());
|
||||||
|
log.info("{}", xmlContent);
|
||||||
|
Map<String, List<String>> parameters = queryDecoder.parameters();
|
||||||
|
String encryptType = parameters.containsKey("encrypt_type") ? parameters
|
||||||
|
.get("encrypt_type").get(0) : EncryptType.RAW.name();
|
||||||
|
message.setEncryptType(EncryptType.valueOf(encryptType.toUpperCase()));
|
||||||
|
String msgSignature = parameters.containsKey("msg_signature") ? parameters
|
||||||
|
.get("msg_signature").get(0) : "";
|
||||||
|
message.setMsgSignature(msgSignature);
|
||||||
|
String echoStr = parameters.containsKey("echostr") ? parameters.get(
|
||||||
|
"echostr").get(0) : "";
|
||||||
|
message.setEchoStr(echoStr);
|
||||||
|
String timeStamp = parameters.containsKey("timestamp") ? parameters
|
||||||
|
.get("timestamp").get(0) : "";
|
||||||
|
message.setTimeStamp(timeStamp);
|
||||||
|
String nonce = parameters.containsKey("nonce") ? parameters
|
||||||
|
.get("nonce").get(0) : "";
|
||||||
|
message.setNonce(nonce);
|
||||||
|
String signature = parameters.containsKey("signature") ? parameters
|
||||||
|
.get("signature").get(0) : "";
|
||||||
|
message.setSignature(signature);
|
||||||
|
|
||||||
|
message.setXmlContent(xmlContent);
|
||||||
|
if (message.getEncryptType() == EncryptType.AES) {
|
||||||
|
message.setXmlContent(MessageUtil.aesDecrypt(account.getAppId(),
|
||||||
|
account.getEncodingAesKey(), message.getEncryptContent()));
|
||||||
|
}
|
||||||
|
message.setToken(account.getToken());
|
||||||
|
out.add(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,71 @@
|
|||||||
|
package com.foxinmy.weixin4j.mp.server;
|
||||||
|
|
||||||
|
import io.netty.channel.ChannelHandlerContext;
|
||||||
|
import io.netty.handler.codec.MessageToMessageEncoder;
|
||||||
|
import io.netty.handler.codec.http.HttpResponse;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import com.foxinmy.weixin4j.model.WeixinAccount;
|
||||||
|
import com.foxinmy.weixin4j.mp.response.BaseResponse;
|
||||||
|
import com.foxinmy.weixin4j.mp.util.HttpUtil;
|
||||||
|
import com.foxinmy.weixin4j.util.ConfigUtil;
|
||||||
|
import com.foxinmy.weixin4j.util.DateUtil;
|
||||||
|
import com.foxinmy.weixin4j.util.MessageUtil;
|
||||||
|
import com.foxinmy.weixin4j.util.RandomUtil;
|
||||||
|
import com.foxinmy.weixin4j.xml.Map2ObjectConverter;
|
||||||
|
import com.foxinmy.weixin4j.xml.XStream;
|
||||||
|
import com.thoughtworks.xstream.core.ClassLoaderReference;
|
||||||
|
import com.thoughtworks.xstream.mapper.DefaultMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信消息编码类
|
||||||
|
*
|
||||||
|
* @className WeixinMessageEncoder
|
||||||
|
* @author jy
|
||||||
|
* @date 2014年11月13日
|
||||||
|
* @since JDK 1.7
|
||||||
|
* @see <a
|
||||||
|
* href="http://mp.weixin.qq.com/wiki/index.php?title=%E6%8E%A5%E5%85%A5%E6%8C%87%E5%BC%95">加密接入指引</a>
|
||||||
|
*/
|
||||||
|
public class WeixinMessageEncoder extends MessageToMessageEncoder<BaseResponse> {
|
||||||
|
private final Logger log = LoggerFactory.getLogger(getClass());
|
||||||
|
protected final static XStream mapXstream = XStream.get();
|
||||||
|
static {
|
||||||
|
mapXstream.alias("xml", Map.class);
|
||||||
|
mapXstream.registerConverter(new Map2ObjectConverter(new DefaultMapper(
|
||||||
|
new ClassLoaderReference(XStream.class.getClassLoader()))));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void encode(ChannelHandlerContext ctx, BaseResponse response,
|
||||||
|
List<Object> out) throws Exception {
|
||||||
|
WeixinAccount account = ConfigUtil.getWeixinAccount();
|
||||||
|
String xmlContent = response.toXml();
|
||||||
|
String nonce = RandomUtil.generateString(32);
|
||||||
|
String timestamp = DateUtil.timestamp2string();
|
||||||
|
String encrtypt = MessageUtil.aesEncrypt(account.getAppId(),
|
||||||
|
account.getEncodingAesKey(), xmlContent);
|
||||||
|
String msgSignature = MessageUtil.signature(account.getToken(), nonce,
|
||||||
|
timestamp, encrtypt);
|
||||||
|
Map<String, String> map = new HashMap<String, String>();
|
||||||
|
map.put("Encrypt", encrtypt);
|
||||||
|
map.put("MsgSignature", msgSignature);
|
||||||
|
map.put("TimeStamp", timestamp);
|
||||||
|
map.put("Nonce", nonce);
|
||||||
|
|
||||||
|
String content = mapXstream.toXML(map);
|
||||||
|
HttpResponse httpResponse = HttpUtil
|
||||||
|
.createWeixinMessageResponse(content);
|
||||||
|
out.add(httpResponse);
|
||||||
|
|
||||||
|
log.info("\n=================aes encrtypt out=================");
|
||||||
|
log.info("{}", map);
|
||||||
|
log.info("{}", content);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,33 +1,26 @@
|
|||||||
package com.foxinmy.weixin4j.mp.server;
|
package com.foxinmy.weixin4j.mp.server;
|
||||||
|
|
||||||
import static io.netty.handler.codec.http.HttpHeaders.Names.CONNECTION;
|
|
||||||
import static io.netty.handler.codec.http.HttpHeaders.Names.CONTENT_LENGTH;
|
|
||||||
import static io.netty.handler.codec.http.HttpHeaders.Names.CONTENT_TYPE;
|
|
||||||
import static io.netty.handler.codec.http.HttpResponseStatus.CONTINUE;
|
|
||||||
import static io.netty.handler.codec.http.HttpResponseStatus.OK;
|
|
||||||
import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
|
|
||||||
import io.netty.buffer.Unpooled;
|
|
||||||
import io.netty.channel.ChannelFutureListener;
|
|
||||||
import io.netty.channel.ChannelHandlerContext;
|
import io.netty.channel.ChannelHandlerContext;
|
||||||
import io.netty.channel.ChannelInboundHandlerAdapter;
|
import io.netty.channel.SimpleChannelInboundHandler;
|
||||||
import io.netty.handler.codec.http.DefaultFullHttpResponse;
|
import io.netty.handler.codec.http.DefaultFullHttpResponse;
|
||||||
import io.netty.handler.codec.http.FullHttpRequest;
|
import io.netty.handler.codec.http.HttpMethod;
|
||||||
import io.netty.handler.codec.http.FullHttpResponse;
|
import io.netty.handler.codec.http.HttpResponse;
|
||||||
import io.netty.handler.codec.http.HttpHeaders;
|
|
||||||
import io.netty.handler.codec.http.HttpHeaders.Values;
|
|
||||||
import io.netty.handler.codec.http.HttpResponseStatus;
|
import io.netty.handler.codec.http.HttpResponseStatus;
|
||||||
import io.netty.handler.codec.http.HttpVersion;
|
import io.netty.handler.codec.http.HttpVersion;
|
||||||
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
|
|
||||||
import org.dom4j.DocumentException;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import com.foxinmy.weixin4j.mp.action.WeixinAction;
|
import com.foxinmy.weixin4j.mp.action.WeixinAction;
|
||||||
import com.foxinmy.weixin4j.mp.mapping.ActionMapping;
|
import com.foxinmy.weixin4j.mp.mapping.ActionMapping;
|
||||||
|
import com.foxinmy.weixin4j.mp.model.HttpWeixinMessage;
|
||||||
|
import com.foxinmy.weixin4j.mp.response.BaseResponse;
|
||||||
|
import com.foxinmy.weixin4j.mp.type.EncryptType;
|
||||||
|
import com.foxinmy.weixin4j.mp.util.HttpUtil;
|
||||||
|
import com.foxinmy.weixin4j.util.MessageUtil;
|
||||||
|
|
||||||
public class WeixinServerHandler extends ChannelInboundHandlerAdapter {
|
public class WeixinServerHandler extends
|
||||||
|
SimpleChannelInboundHandler<HttpWeixinMessage> {
|
||||||
|
|
||||||
private final Logger log = LoggerFactory.getLogger(getClass());
|
private final Logger log = LoggerFactory.getLogger(getClass());
|
||||||
|
|
||||||
@ -37,49 +30,61 @@ public class WeixinServerHandler extends ChannelInboundHandlerAdapter {
|
|||||||
this.actionMapping = actionMapping;
|
this.actionMapping = actionMapping;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void channelReadComplete(ChannelHandlerContext ctx) {
|
public void channelReadComplete(ChannelHandlerContext ctx) {
|
||||||
ctx.flush();
|
ctx.flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void channelRead(ChannelHandlerContext ctx, Object msg)
|
|
||||||
throws DocumentException {
|
|
||||||
if (msg instanceof FullHttpRequest) {
|
|
||||||
FullHttpRequest req = (FullHttpRequest) msg;
|
|
||||||
if (HttpHeaders.is100ContinueExpected(req)) {
|
|
||||||
ctx.write(new DefaultFullHttpResponse(HTTP_1_1, CONTINUE));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
String xmlMsg = req.content().toString(StandardCharsets.UTF_8);
|
|
||||||
log.info("\n=================message in=================\n{}",
|
|
||||||
xmlMsg);
|
|
||||||
WeixinAction action = actionMapping.getAction(xmlMsg);
|
|
||||||
if (action == null) {
|
|
||||||
ctx.write(new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,
|
|
||||||
HttpResponseStatus.NOT_FOUND));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
String content = action.execute(xmlMsg);
|
|
||||||
log.info("\n=================message out=================\n{}",
|
|
||||||
content);
|
|
||||||
FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1,
|
|
||||||
OK, Unpooled.copiedBuffer(content, StandardCharsets.UTF_8));
|
|
||||||
response.headers().set(CONTENT_TYPE, "text/plain;charset=utf-8");
|
|
||||||
response.headers().set(CONTENT_LENGTH,
|
|
||||||
response.content().readableBytes());
|
|
||||||
if (!HttpHeaders.isKeepAlive(req)) {
|
|
||||||
ctx.write(response).addListener(ChannelFutureListener.CLOSE);
|
|
||||||
} else {
|
|
||||||
response.headers().set(CONNECTION, Values.KEEP_ALIVE);
|
|
||||||
ctx.write(response);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
|
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
|
||||||
cause.printStackTrace();
|
cause.printStackTrace();
|
||||||
ctx.close();
|
ctx.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void channelRead0(ChannelHandlerContext ctx,
|
||||||
|
HttpWeixinMessage httpMessage) throws Exception {
|
||||||
|
log.info("\n=================message in=================\n{}",
|
||||||
|
httpMessage);
|
||||||
|
boolean validate = false;
|
||||||
|
if (httpMessage.getMethod() == HttpMethod.GET
|
||||||
|
|| httpMessage.getEncryptType() == EncryptType.RAW) {
|
||||||
|
validate = MessageUtil.signature(httpMessage.getToken(),
|
||||||
|
httpMessage.getTimeStamp(), httpMessage.getNonce()).equals(
|
||||||
|
httpMessage.getSignature());
|
||||||
|
if (httpMessage.getMethod() == HttpMethod.GET && validate) {
|
||||||
|
HttpResponse httpResponse = HttpUtil
|
||||||
|
.createWeixinMessageResponse(httpMessage.getEchoStr());
|
||||||
|
ctx.write(httpResponse);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
validate = MessageUtil.signature(httpMessage.getToken(),
|
||||||
|
httpMessage.getTimeStamp(), httpMessage.getNonce(),
|
||||||
|
httpMessage.getEncryptContent()).equals(
|
||||||
|
httpMessage.getMsgSignature());
|
||||||
|
}
|
||||||
|
if (!validate) {
|
||||||
|
ctx.write(new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,
|
||||||
|
HttpResponseStatus.FORBIDDEN));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String xmlContent = httpMessage.getXmlContent();
|
||||||
|
WeixinAction action = actionMapping.getAction(xmlContent);
|
||||||
|
if (action == null) {
|
||||||
|
ctx.write(new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,
|
||||||
|
HttpResponseStatus.NOT_FOUND));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
BaseResponse response = action.execute(xmlContent);
|
||||||
|
log.info("\n=================message out=================\n{}",
|
||||||
|
response);
|
||||||
|
if (httpMessage.getEncryptType() == EncryptType.RAW) {
|
||||||
|
HttpResponse httpResponse = HttpUtil
|
||||||
|
.createWeixinMessageResponse(response.toXml());
|
||||||
|
ctx.write(httpResponse);
|
||||||
|
} else {
|
||||||
|
ctx.write(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,6 +22,8 @@ public class WeixinServerInitializer extends ChannelInitializer<SocketChannel> {
|
|||||||
ChannelPipeline pipeline = channel.pipeline();
|
ChannelPipeline pipeline = channel.pipeline();
|
||||||
pipeline.addLast(new HttpServerCodec());
|
pipeline.addLast(new HttpServerCodec());
|
||||||
pipeline.addLast(new HttpObjectAggregator(65536));
|
pipeline.addLast(new HttpObjectAggregator(65536));
|
||||||
|
pipeline.addLast(new WeixinMessageDecoder());
|
||||||
|
pipeline.addLast(new WeixinMessageEncoder());
|
||||||
pipeline.addLast(new WeixinServerHandler(actionMapping));
|
pipeline.addLast(new WeixinServerHandler(actionMapping));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
package com.foxinmy.weixin4j.mp.statrup;
|
package com.foxinmy.weixin4j.mp.startup;
|
||||||
|
|
||||||
import io.netty.bootstrap.ServerBootstrap;
|
import io.netty.bootstrap.ServerBootstrap;
|
||||||
import io.netty.channel.Channel;
|
import io.netty.channel.Channel;
|
||||||
@ -21,7 +21,7 @@ import com.foxinmy.weixin4j.mp.server.WeixinServerInitializer;
|
|||||||
* @since JDK 1.7
|
* @since JDK 1.7
|
||||||
* @see
|
* @see
|
||||||
*/
|
*/
|
||||||
public final class WeixinServiceBootstrap {
|
public final class WeixinServerBootstrap {
|
||||||
|
|
||||||
private final static int port;
|
private final static int port;
|
||||||
private final static int workerThreads;
|
private final static int workerThreads;
|
||||||
@ -0,0 +1,42 @@
|
|||||||
|
package com.foxinmy.weixin4j.mp.util;
|
||||||
|
|
||||||
|
import static io.netty.handler.codec.http.HttpHeaders.Names.CONNECTION;
|
||||||
|
import static io.netty.handler.codec.http.HttpHeaders.Names.CONTENT_LENGTH;
|
||||||
|
import static io.netty.handler.codec.http.HttpHeaders.Names.CONTENT_TYPE;
|
||||||
|
import static io.netty.handler.codec.http.HttpHeaders.Names.DATE;
|
||||||
|
import static io.netty.handler.codec.http.HttpHeaders.Names.SERVER;
|
||||||
|
import static io.netty.handler.codec.http.HttpResponseStatus.OK;
|
||||||
|
import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
|
||||||
|
import io.netty.buffer.Unpooled;
|
||||||
|
import io.netty.handler.codec.http.DefaultFullHttpResponse;
|
||||||
|
import io.netty.handler.codec.http.FullHttpResponse;
|
||||||
|
import io.netty.handler.codec.http.HttpHeaders.Values;
|
||||||
|
import io.netty.handler.codec.http.HttpResponse;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
import org.apache.http.Consts;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP工具类
|
||||||
|
*
|
||||||
|
* @className HttpUtil
|
||||||
|
* @author jy
|
||||||
|
* @date 2014年11月15日
|
||||||
|
* @since JDK 1.7
|
||||||
|
* @see
|
||||||
|
*/
|
||||||
|
public class HttpUtil {
|
||||||
|
|
||||||
|
public static HttpResponse createWeixinMessageResponse(String content) {
|
||||||
|
FullHttpResponse httpResponse = new DefaultFullHttpResponse(HTTP_1_1,
|
||||||
|
OK, Unpooled.copiedBuffer(content, Consts.UTF_8));
|
||||||
|
httpResponse.headers().set(CONTENT_TYPE,
|
||||||
|
"application/xml;encoding=utf-8");
|
||||||
|
httpResponse.headers().set(CONTENT_LENGTH, content.getBytes().length);
|
||||||
|
httpResponse.headers().set(CONNECTION, Values.KEEP_ALIVE);
|
||||||
|
httpResponse.headers().set(DATE, new Date());
|
||||||
|
httpResponse.headers().set(SERVER, "netty4");
|
||||||
|
return httpResponse;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,6 +1,7 @@
|
|||||||
# \u516c\u4f17\u53f7\u4fe1\u606f
|
# \u516c\u4f17\u53f7\u4fe1\u606f
|
||||||
account={"appId":"wx4ab8f8de58159a57","appSecret":"1d4eb0f4bf556aaed539f30ed05ca795",\
|
account={"appId":"wx4ab8f8de58159a57","appSecret":"1d4eb0f4bf556aaed539f30ed05ca795",\
|
||||||
"token":"\u5f00\u653e\u8005\u7684token \u975e\u5fc5\u987b","openId":"\u516c\u4f17\u53f7\u7684openid \u975e\u5fc5\u987b",\
|
"token":"\u5f00\u653e\u8005\u7684token \u975e\u5fc5\u987b","openId":"\u516c\u4f17\u53f7\u7684openid \u975e\u5fc5\u987b",\
|
||||||
|
"encodingAesKey":"\u516c\u4f17\u53f7\u8bbe\u7f6e\u4e86\u52a0\u5bc6\u65b9\u5f0f\u4e14\u4e3a\u300c\u5b89\u5168\u6a21\u5f0f\u300d\u9700\u8981\u586b\u5165",\
|
||||||
"mchId":"V3.x\u7248\u672c\u4e0b\u7684\u5fae\u4fe1\u5546\u6237\u53f7",\
|
"mchId":"V3.x\u7248\u672c\u4e0b\u7684\u5fae\u4fe1\u5546\u6237\u53f7",\
|
||||||
"version":3,\
|
"version":3,\
|
||||||
"partnerId":"\u8d22\u4ed8\u901a\u7684\u5546\u6237\u53f7","partnerKey":"\u8d22\u4ed8\u901a\u5546\u6237\u6743\u9650\u5bc6\u94a5Key",\
|
"partnerId":"\u8d22\u4ed8\u901a\u7684\u5546\u6237\u53f7","partnerKey":"\u8d22\u4ed8\u901a\u5546\u6237\u6743\u9650\u5bc6\u94a5Key",\
|
||||||
@ -15,4 +16,4 @@ media_path=/tmp/weixin/media
|
|||||||
# \u5bf9\u8d26\u5355\u4fdd\u5b58\u8def\u5f84
|
# \u5bf9\u8d26\u5355\u4fdd\u5b58\u8def\u5f84
|
||||||
bill_path=/tmp/weixin/bill
|
bill_path=/tmp/weixin/bill
|
||||||
# ca\u8bc1\u4e66\u5b58\u653e\u7684\u5b8c\u6574\u8def\u5f84
|
# ca\u8bc1\u4e66\u5b58\u653e\u7684\u5b8c\u6574\u8def\u5f84
|
||||||
ca_file=/tmp/weixin/xxxxx.p12
|
ca_file=/tmp/weixin/xxxxxx.p12
|
||||||
@ -6,10 +6,10 @@ JAVA_HOME="/usr/local/java/"
|
|||||||
RUNNING_USER=root
|
RUNNING_USER=root
|
||||||
|
|
||||||
#Run home
|
#Run home
|
||||||
APP_HOME="/usr/local/weixin/weixin-service"
|
APP_HOME="/usr/local/weixin/weixin-mp-server"
|
||||||
|
|
||||||
#main class
|
#main class
|
||||||
APP_MAINCLASS=com.foxinmy.weixin4j.mp.startup.WeixinServiceBootstrap
|
APP_MAINCLASS=com.foxinmy.weixin4j.mp.startup.WeixinServerBootstrap
|
||||||
|
|
||||||
#classpath
|
#classpath
|
||||||
CLASSPATH=$APP_HOME/classes
|
CLASSPATH=$APP_HOME/classes
|
||||||
@ -42,9 +42,9 @@ start() {
|
|||||||
checkpid
|
checkpid
|
||||||
|
|
||||||
if [ $psid -ne 0 ]; then
|
if [ $psid -ne 0 ]; then
|
||||||
echo "================================"
|
echo "====================================================="
|
||||||
echo "warn: $APP_MAINCLASS already started! (pid=$psid)"
|
echo "warn: $APP_MAINCLASS already started! (pid=$psid)"
|
||||||
echo "================================"
|
echo "====================================================="
|
||||||
else
|
else
|
||||||
echo -n "Starting $APP_MAINCLASS ..."
|
echo -n "Starting $APP_MAINCLASS ..."
|
||||||
# JAVA_CMD="nohup $JAVA_HOME/bin/java $JAVA_OPTS -classpath $CLASSPATH $APP_MAINCLASS >/dev/null 2>&1 &"
|
# JAVA_CMD="nohup $JAVA_HOME/bin/java $JAVA_OPTS -classpath $CLASSPATH $APP_MAINCLASS >/dev/null 2>&1 &"
|
||||||
@ -79,9 +79,9 @@ stop() {
|
|||||||
stop
|
stop
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
echo "================================"
|
echo "====================================================="
|
||||||
echo "warn: $APP_MAINCLASS is not running"
|
echo "warn: $APP_MAINCLASS is not running"
|
||||||
echo "================================"
|
echo "====================================================="
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user