Browse Source

添加docker配置

tuonina 5 years ago
parent
commit
c44ba4db16

+ 1 - 0
tuon-core/build.gradle

@@ -14,4 +14,5 @@ dependencies {
 //    compile 'org.springframework.boot:spring-boot-starter-data-mongodb-reactive'
     compile("com.baomidou:mybatis-plus-boot-starter:$mybatisPlusVersion")
     compile 'org.springframework.session:spring-session-data-redis'
+    compile ("com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.9.9")
 }

+ 14 - 0
tuon-core/src/main/kotlin/cn/tonyandmoney/tuon/core/config/CustomWebMvcConfiguration.kt

@@ -1,6 +1,7 @@
 package cn.tonyandmoney.tuon.core.config
 
 import cn.tonyandmoney.tuon.core.properties.CustomConfigProperties
+import com.fasterxml.jackson.databind.ObjectMapper
 import com.fasterxml.jackson.databind.ser.std.ToStringSerializer
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
 import org.springframework.boot.context.properties.EnableConfigurationProperties
@@ -49,6 +50,19 @@ class CustomWebMvcConfiguration : WebFluxConfigurationSupport() {
         }
     }
 
+    @Bean
+    fun xmlConverter():MappingJackson2HttpMessageConverter{
+        return MappingJackson2HttpMessageConverter().apply {
+            this.objectMapper = Jackson2ObjectMapperBuilder.xml().build()
+        }
+
+    }
+
+    @Bean("xmlObjectMapper")
+    fun xmlObjectMapper():ObjectMapper{
+        return Jackson2ObjectMapperBuilder.xml().build()
+    }
+
     @Bean
     @Primary
     fun builder(): Jackson2ObjectMapperBuilder {

+ 24 - 0
tuon-qywx/src/main/java/cn/tonyandmoney/tuon/qywx/QywxProperties.java

@@ -3,6 +3,9 @@ package cn.tonyandmoney.tuon.qywx;
 
 import org.springframework.boot.context.properties.ConfigurationProperties;
 
+import java.util.HashMap;
+import java.util.Map;
+
 /**
  * 企业微信登录视频
  */
@@ -14,7 +17,26 @@ public class QywxProperties {
     private String accessTokenPath="/cgi-bin/gettoken?corpid={corpid}&corpsecret={corpsecret}";
     private String userInfoPath="/cgi-bin/user/getuserinfo?access_token={access_token}&code={code}";
     private String userGetPath="/cgi-bin/user/get?access_token={access_token}&userid={userid}";
+    //消息接收的Token-EncodingAESKey
+    private String token="xwKMrU6J";
+    private String encodingAESKey="uiSDpQAl9yJHtv5iOS9Nr25oKyrIeRcD30DQHECR3zV";
+
+
+    public void setEncodingAESKey(String encodingAESKey) {
+        this.encodingAESKey = encodingAESKey;
+    }
 
+    public void setToken(String token) {
+        this.token = token;
+    }
+
+    public String getEncodingAESKey() {
+        return encodingAESKey;
+    }
+
+    public String getToken() {
+        return token;
+    }
 
     public String getHost() {
         return host;
@@ -63,4 +85,6 @@ public class QywxProperties {
     public void setCorpSecret(String corpSecret) {
         this.corpSecret = corpSecret;
     }
+
+
 }

+ 59 - 0
tuon-qywx/src/main/java/cn/tonyandmoney/tuon/qywx/aes/AesException.java

@@ -0,0 +1,59 @@
+package cn.tonyandmoney.tuon.qywx.aes;
+
+@SuppressWarnings("serial")
+public class AesException extends Exception {
+
+	public final static int OK = 0;
+	public final static int ValidateSignatureError = -40001;
+	public final static int ParseXmlError = -40002;
+	public final static int ComputeSignatureError = -40003;
+	public final static int IllegalAesKey = -40004;
+	public final static int ValidateCorpidError = -40005;
+	public final static int EncryptAESError = -40006;
+	public final static int DecryptAESError = -40007;
+	public final static int IllegalBuffer = -40008;
+	//public final static int EncodeBase64Error = -40009;
+	//public final static int DecodeBase64Error = -40010;
+	//public final static int GenReturnXmlError = -40011;
+
+	private int code;
+
+	private static String getMessage(int code) {
+		switch (code) {
+		case ValidateSignatureError:
+			return "签名验证错误";
+		case ParseXmlError:
+			return "xml解析失败";
+		case ComputeSignatureError:
+			return "sha加密生成签名失败";
+		case IllegalAesKey:
+			return "SymmetricKey非法";
+		case ValidateCorpidError:
+			return "corpid校验失败";
+		case EncryptAESError:
+			return "aes加密失败";
+		case DecryptAESError:
+			return "aes解密失败";
+		case IllegalBuffer:
+			return "解密后得到的buffer非法";
+//		case EncodeBase64Error:
+//			return "base64加密错误";
+//		case DecodeBase64Error:
+//			return "base64解密错误";
+//		case GenReturnXmlError:
+//			return "xml生成失败";
+		default:
+			return null; // cannot be
+		}
+	}
+
+	public int getCode() {
+		return code;
+	}
+
+	AesException(int code) {
+		super(getMessage(code));
+		this.code = code;
+	}
+
+}

+ 26 - 0
tuon-qywx/src/main/java/cn/tonyandmoney/tuon/qywx/aes/ByteGroup.java

@@ -0,0 +1,26 @@
+package cn.tonyandmoney.tuon.qywx.aes;
+
+import java.util.ArrayList;
+
+class ByteGroup {
+	ArrayList<Byte> byteContainer = new ArrayList<Byte>();
+
+	public byte[] toBytes() {
+		byte[] bytes = new byte[byteContainer.size()];
+		for (int i = 0; i < byteContainer.size(); i++) {
+			bytes[i] = byteContainer.get(i);
+		}
+		return bytes;
+	}
+
+	public ByteGroup addBytes(byte[] bytes) {
+		for (byte b : bytes) {
+			byteContainer.add(b);
+		}
+		return this;
+	}
+
+	public int size() {
+		return byteContainer.size();
+	}
+}

+ 67 - 0
tuon-qywx/src/main/java/cn/tonyandmoney/tuon/qywx/aes/PKCS7Encoder.java

@@ -0,0 +1,67 @@
+/**
+ * 对企业微信发送给企业后台的消息加解密示例代码.
+ * 
+ * @copyright Copyright (c) 1998-2014 Tencent Inc.
+ */
+
+// ------------------------------------------------------------------------
+
+package cn.tonyandmoney.tuon.qywx.aes;
+
+import java.nio.charset.Charset;
+import java.util.Arrays;
+
+/**
+ * 提供基于PKCS7算法的加解密接口.
+ */
+class PKCS7Encoder {
+	static Charset CHARSET = Charset.forName("utf-8");
+	static 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;
+	}
+
+}

+ 61 - 0
tuon-qywx/src/main/java/cn/tonyandmoney/tuon/qywx/aes/SHA1.java

@@ -0,0 +1,61 @@
+/**
+ * 对企业微信发送给企业后台的消息加解密示例代码.
+ * 
+ * @copyright Copyright (c) 1998-2014 Tencent Inc.
+ */
+
+// ------------------------------------------------------------------------
+
+package cn.tonyandmoney.tuon.qywx.aes;
+
+import java.security.MessageDigest;
+import java.util.Arrays;
+
+/**
+ * SHA1 class
+ *
+ * 计算消息签名接口.
+ */
+class SHA1 {
+
+	/**
+	 * 用SHA1算法生成安全签名
+	 * @param token 票据
+	 * @param timestamp 时间戳
+	 * @param nonce 随机字符串
+	 * @param encrypt 密文
+	 * @return 安全签名
+	 * @throws AesException 
+	 */
+	public static String getSHA1(String token, String timestamp, String nonce, String encrypt) throws AesException
+			  {
+		try {
+			String[] array = new String[] { token, timestamp, nonce, encrypt };
+			StringBuffer sb = new StringBuffer();
+			// 字符串排序
+			Arrays.sort(array);
+			for (int i = 0; i < 4; i++) {
+				sb.append(array[i]);
+			}
+			String str = sb.toString();
+			// SHA1签名生成
+			MessageDigest md = MessageDigest.getInstance("SHA-1");
+			md.update(str.getBytes());
+			byte[] digest = md.digest();
+
+			StringBuffer hexstr = new StringBuffer();
+			String shaHex = "";
+			for (int i = 0; i < digest.length; i++) {
+				shaHex = Integer.toHexString(digest[i] & 0xFF);
+				if (shaHex.length() < 2) {
+					hexstr.append(0);
+				}
+				hexstr.append(shaHex);
+			}
+			return hexstr.toString();
+		} catch (Exception e) {
+			e.printStackTrace();
+			throw new AesException(AesException.ComputeSignatureError);
+		}
+	}
+}

+ 135 - 0
tuon-qywx/src/main/java/cn/tonyandmoney/tuon/qywx/aes/Sample.java

@@ -0,0 +1,135 @@
+package cn.tonyandmoney.tuon.qywx.aes;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NodeList;
+import org.xml.sax.InputSource;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import java.io.StringReader;
+
+
+public class Sample {
+
+    public static void main(String[] args) throws Exception {
+        String sToken = "QDG6eK";
+        String sCorpID = "wx5823bf96d3bd56c7";
+        String sEncodingAESKey = "jWmYm7qr5nMoAUwZRjGtBxmz3KA1tkAj3ykkR6q2B2C";
+
+        WXBizMsgCrypt wxcpt = new WXBizMsgCrypt(sToken, sEncodingAESKey, sCorpID);
+		/*
+		------------使用示例一:验证回调URL---------------
+		*企业开启回调模式时,企业微信会向验证url发送一个get请求 
+		假设点击验证时,企业收到类似请求:
+		* GET /cgi-bin/wxpush?msg_signature=5c45ff5e21c57e6ad56bac8758b79b1d9ac89fd3&timestamp=1409659589&nonce=263014780&echostr=P9nAzCzyDtyTWESHep1vC5X9xho%2FqYX3Zpb4yKa9SKld1DsH3Iyt3tP3zNdtp%2B4RPcs8TgAE7OaBO%2BFZXvnaqQ%3D%3D 
+		* HTTP/1.1 Host: qy.weixin.qq.com
+
+		接收到该请求时,企业应		1.解析出Get请求的参数,包括消息体签名(msg_signature),时间戳(timestamp),随机数字串(nonce)以及企业微信推送过来的随机加密字符串(echostr),
+		这一步注意作URL解码。
+		2.验证消息体签名的正确性 
+		3. 解密出echostr原文,将原文当作Get请求的response,返回给企业微信
+		第23步可以用企业微信提供的库函数VerifyURL来实现。
+
+		*/
+        // 解析出url上的参数值如下:
+        // String sVerifyMsgSig = HttpUtils.ParseUrl("msg_signature");
+        String sVerifyMsgSig = "5c45ff5e21c57e6ad56bac8758b79b1d9ac89fd3";
+        // String sVerifyTimeStamp = HttpUtils.ParseUrl("timestamp");
+        String sVerifyTimeStamp = "1409659589";
+        // String sVerifyNonce = HttpUtils.ParseUrl("nonce");
+        String sVerifyNonce = "263014780";
+        // String sVerifyEchoStr = HttpUtils.ParseUrl("echostr");
+        String sVerifyEchoStr = "P9nAzCzyDtyTWESHep1vC5X9xho/qYX3Zpb4yKa9SKld1DsH3Iyt3tP3zNdtp+4RPcs8TgAE7OaBO+FZXvnaqQ==";
+        String sEchoStr; //需要返回的明文
+        try {
+            sEchoStr = wxcpt.VerifyURL(sVerifyMsgSig, sVerifyTimeStamp,
+                    sVerifyNonce, sVerifyEchoStr);
+            System.out.println("verifyurl echostr: " + sEchoStr);
+            // 验证URL成功,将sEchoStr返回
+            // HttpUtils.SetResponse(sEchoStr);
+        } catch (Exception e) {
+            //验证URL失败,错误原因请查看异常
+            e.printStackTrace();
+        }
+
+		/*
+		------------使用示例二:对用户回复的消息解密---------------
+		用户回复消息或者点击事件响应时,企业会收到回调消息,此消息是经过企业微信加密之后的密文以post形式发送给企业,密文格式请参考官方文档
+		假设企业收到企业微信的回调消息如下:
+		POST /cgi-bin/wxpush? msg_signature=477715d11cdb4164915debcba66cb864d751f3e6&timestamp=1409659813&nonce=1372623149 HTTP/1.1
+		Host: qy.weixin.qq.com
+		Content-Length: 613
+		<xml>		<ToUserName><![CDATA[wx5823bf96d3bd56c7]]></ToUserName><Encrypt><![CDATA[RypEvHKD8QQKFhvQ6QleEB4J58tiPdvo+rtK1I9qca6aM/wvqnLSV5zEPeusUiX5L5X/0lWfrf0QADHHhGd3QczcdCUpj911L3vg3W/sYYvuJTs3TUUkSUXxaccAS0qhxchrRYt66wiSpGLYL42aM6A8dTT+6k4aSknmPj48kzJs8qLjvd4Xgpue06DOdnLxAUHzM6+kDZ+HMZfJYuR+LtwGc2hgf5gsijff0ekUNXZiqATP7PF5mZxZ3Izoun1s4zG4LUMnvw2r+KqCKIw+3IQH03v+BCA9nMELNqbSf6tiWSrXJB3LAVGUcallcrw8V2t9EL4EhzJWrQUax5wLVMNS0+rUPA3k22Ncx4XXZS9o0MBH27Bo6BpNelZpS+/uh9KsNlY6bHCmJU9p8g7m3fVKn28H3KDYA5Pl/T8Z1ptDAVe0lXdQ2YoyyH2uyPIGHBZZIs2pDBS8R07+qN+E7Q==]]></Encrypt>
+		<AgentID><![CDATA[218]]></AgentID>
+		</xml>
+
+		企业收到post请求之后应该		1.解析出url上的参数,包括消息体签名(msg_signature),时间戳(timestamp)以及随机数字串(nonce)
+		2.验证消息体签名的正确性。
+		3.将post请求的数据进行xml解析,并将<Encrypt>标签的内容进行解密,解密出来的明文即是用户回复消息的明文,明文格式请参考官方文档
+		第23步可以用企业微信提供的库函数DecryptMsg来实现。
+		*/
+        // String sReqMsgSig = HttpUtils.ParseUrl("msg_signature");
+        String sReqMsgSig = "477715d11cdb4164915debcba66cb864d751f3e6";
+        // String sReqTimeStamp = HttpUtils.ParseUrl("timestamp");
+        String sReqTimeStamp = "1409659813";
+        // String sReqNonce = HttpUtils.ParseUrl("nonce");
+        String sReqNonce = "1372623149";
+        // post请求的密文数据
+        // sReqData = HttpUtils.PostData();
+        String sReqData = "<xml><ToUserName><![CDATA[wx5823bf96d3bd56c7]]></ToUserName><Encrypt><![CDATA[RypEvHKD8QQKFhvQ6QleEB4J58tiPdvo+rtK1I9qca6aM/wvqnLSV5zEPeusUiX5L5X/0lWfrf0QADHHhGd3QczcdCUpj911L3vg3W/sYYvuJTs3TUUkSUXxaccAS0qhxchrRYt66wiSpGLYL42aM6A8dTT+6k4aSknmPj48kzJs8qLjvd4Xgpue06DOdnLxAUHzM6+kDZ+HMZfJYuR+LtwGc2hgf5gsijff0ekUNXZiqATP7PF5mZxZ3Izoun1s4zG4LUMnvw2r+KqCKIw+3IQH03v+BCA9nMELNqbSf6tiWSrXJB3LAVGUcallcrw8V2t9EL4EhzJWrQUax5wLVMNS0+rUPA3k22Ncx4XXZS9o0MBH27Bo6BpNelZpS+/uh9KsNlY6bHCmJU9p8g7m3fVKn28H3KDYA5Pl/T8Z1ptDAVe0lXdQ2YoyyH2uyPIGHBZZIs2pDBS8R07+qN+E7Q==]]></Encrypt><AgentID><![CDATA[218]]></AgentID></xml>";
+
+        try {
+            String sMsg = wxcpt.DecryptMsg(sReqMsgSig, sReqTimeStamp, sReqNonce, sReqData);
+            System.out.println("after decrypt msg: " + sMsg);
+            // TODO: 解析出明文xml标签的内容进行处理
+            // For example:
+            DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
+            DocumentBuilder db = dbf.newDocumentBuilder();
+            StringReader sr = new StringReader(sMsg);
+            InputSource is = new InputSource(sr);
+            Document document = db.parse(is);
+
+            Element root = document.getDocumentElement();
+            NodeList nodelist1 = root.getElementsByTagName("Content");
+            String Content = nodelist1.item(0).getTextContent();
+            System.out.println("Content:" + Content);
+
+        } catch (Exception e) {
+            // TODO
+            // 解密失败,失败原因请查看异常
+            e.printStackTrace();
+        }
+
+	/*
+		------------使用示例三:企业回复用户消息的加密---------------
+		企业被动回复用户的消息也需要进行加密,并且拼接成密文格式的xml串。
+		假设企业需要回复用户的明文如下:
+		<xml>
+		<ToUserName><![CDATA[mycreate]]></ToUserName>
+		<FromUserName><![CDATA[wx5823bf96d3bd56c7]]></FromUserName>
+		<CreateTime>1348831860</CreateTime>
+		<MsgType><![CDATA[text]]></MsgType>
+		<Content><![CDATA[this is a test]]></Content>
+		<MsgId>1234567890123456</MsgId>
+		<AgentID>128</AgentID>
+		</xml>
+
+		为了将此段明文回复给用户,企业应:			1.自己生成时间时间戳(timestamp),随机数字串(nonce)以便生成消息体签名,也可以直接用从企业微信的post url上解析出的对应值。
+		2.将明文加密得到密文。	3.用密文,步骤1生成的timestamp,nonce和企业在企业微信设定的token生成消息体签名。			4.将密文,消息体签名,时间戳,随机数字串拼接成xml格式的字符串,发送给企业。
+		以上234步可以用企业微信提供的库函数EncryptMsg来实现。
+		*/
+        String sRespData = "<xml><ToUserName><![CDATA[mycreate]]></ToUserName><FromUserName><![CDATA[wx5823bf96d3bd56c7]]></FromUserName><CreateTime>1348831860</CreateTime><MsgType><![CDATA[text]]></MsgType><Content><![CDATA[this is a test]]></Content><MsgId>1234567890123456</MsgId><AgentID>128</AgentID></xml>";
+        try {
+            String sEncryptMsg = wxcpt.EncryptMsg(sRespData, sReqTimeStamp, sReqNonce);
+            System.out.println("after encrypt sEncrytMsg: " + sEncryptMsg);
+            // 加密成功
+            // TODO:
+            // HttpUtils.SetResponse(sEncryptMsg);
+        } catch (Exception e) {
+            e.printStackTrace();
+            // 加密失败
+        }
+
+    }
+}

+ 302 - 0
tuon-qywx/src/main/java/cn/tonyandmoney/tuon/qywx/aes/WXBizMsgCrypt.java

@@ -0,0 +1,302 @@
+/**
+ * 对企业微信发送给企业后台的消息加解密示例代码.
+ *
+ * @copyright Copyright (c) 1998-2014 Tencent Inc.
+ * <p>
+ * 针对org.apache.commons.codec.binary.Base64,
+ * 需要导入架包commons-codec-1.9(或commons-codec-1.8等其他版本)
+ * 官方下载地址:http://commons.apache.org/proper/commons-codec/download_codec.cgi
+ */
+
+// ------------------------------------------------------------------------
+
+/**
+ * 针对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 cn.tonyandmoney.tuon.qywx.aes;
+
+import java.nio.charset.Charset;
+import java.util.Arrays;
+import java.util.Random;
+
+import javax.crypto.Cipher;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.apache.commons.codec.binary.Base64;
+
+/**
+ * 提供接收和推送给企业微信消息的加解密接口(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 WXBizMsgCrypt {
+    static Charset CHARSET = Charset.forName("utf-8");
+    Base64 base64 = new Base64();
+    byte[] aesKey;
+    String token;
+    String receiveid;
+
+    /**
+     * 构造函数
+     * @param token 企业微信后台,开发者设置的token
+     * @param encodingAesKey 企业微信后台,开发者设置的EncodingAESKey
+     * @param receiveid, 不同场景含义不同,详见文档
+     *
+     * @throws AesException 执行失败,请查看该异常的错误码和具体的错误信息
+     */
+    public WXBizMsgCrypt(String token, String encodingAesKey, String receiveid) throws AesException {
+        if (encodingAesKey.length() != 43) {
+            throw new AesException(AesException.IllegalAesKey);
+        }
+
+        this.token = token;
+        this.receiveid = receiveid;
+        aesKey = Base64.decodeBase64(encodingAesKey + "=");
+    }
+
+    // 生成4个字节的网络字节序
+    byte[] getNetworkBytesOrder(int sourceNumber) {
+        byte[] orderBytes = new byte[4];
+        orderBytes[3] = (byte) (sourceNumber & 0xFF);
+        orderBytes[2] = (byte) (sourceNumber >> 8 & 0xFF);
+        orderBytes[1] = (byte) (sourceNumber >> 16 & 0xFF);
+        orderBytes[0] = (byte) (sourceNumber >> 24 & 0xFF);
+        return orderBytes;
+    }
+
+    // 还原4个字节的网络字节序
+    int recoverNetworkBytesOrder(byte[] orderBytes) {
+        int sourceNumber = 0;
+        for (int i = 0; i < 4; i++) {
+            sourceNumber <<= 8;
+            sourceNumber |= orderBytes[i] & 0xff;
+        }
+        return sourceNumber;
+    }
+
+    // 随机生成16位字符串
+    String getRandomStr() {
+        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();
+    }
+
+    /**
+     * 对明文进行加密.
+     *
+     * @param text 需要加密的明文
+     * @return 加密后base64编码的字符串
+     * @throws AesException aes加密失败
+     */
+    String encrypt(String randomStr, String text) throws AesException {
+        ByteGroup byteCollector = new ByteGroup();
+        byte[] randomStrBytes = randomStr.getBytes(CHARSET);
+        byte[] textBytes = text.getBytes(CHARSET);
+        byte[] networkBytesOrder = getNetworkBytesOrder(textBytes.length);
+        byte[] receiveidBytes = receiveid.getBytes(CHARSET);
+
+        // randomStr + networkBytesOrder + text + receiveid
+        byteCollector.addBytes(randomStrBytes);
+        byteCollector.addBytes(networkBytesOrder);
+        byteCollector.addBytes(textBytes);
+        byteCollector.addBytes(receiveidBytes);
+
+        // ... + 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) {
+            e.printStackTrace();
+            throw new AesException(AesException.EncryptAESError);
+        }
+    }
+
+    /**
+     * 对密文进行解密.
+     *
+     * @param text 需要解密的密文
+     * @return 解密得到的明文
+     * @throws AesException aes解密失败
+     */
+    String decrypt(String text) throws AesException {
+        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(text);
+
+            // 解密
+            original = cipher.doFinal(encrypted);
+        } catch (Exception e) {
+            e.printStackTrace();
+            throw new AesException(AesException.DecryptAESError);
+        }
+
+        String xmlContent, from_receiveid;
+        try {
+            // 去除补位字符
+            byte[] bytes = PKCS7Encoder.decode(original);
+
+            // 分离16位随机字符串,网络字节序和receiveid
+            byte[] networkOrder = Arrays.copyOfRange(bytes, 16, 20);
+
+            int xmlLength = recoverNetworkBytesOrder(networkOrder);
+
+            xmlContent = new String(Arrays.copyOfRange(bytes, 20, 20 + xmlLength), CHARSET);
+            from_receiveid = new String(Arrays.copyOfRange(bytes, 20 + xmlLength, bytes.length),
+                    CHARSET);
+        } catch (Exception e) {
+            e.printStackTrace();
+            throw new AesException(AesException.IllegalBuffer);
+        }
+
+        // receiveid不相同的情况
+        if (!from_receiveid.equals(receiveid)) {
+            throw new AesException(AesException.ValidateCorpidError);
+        }
+        return xmlContent;
+
+    }
+
+    /**
+     * 将企业微信回复用户的消息加密打包.
+     * <ol>
+     * 	<li>对要发送的消息进行AES-CBC加密</li>
+     * 	<li>生成安全签名</li>
+     * 	<li>将消息密文和安全签名打包成xml格式</li>
+     * </ol>
+     *
+     * @param replyMsg 企业微信待回复用户的消息,xml格式的字符串
+     * @param timeStamp 时间戳,可以自己生成,也可以用URL参数的timestamp
+     * @param nonce 随机串,可以自己生成,也可以用URL参数的nonce
+     *
+     * @return 加密后的可以直接回复用户的密文,包括msg_signature, timestamp, nonce, encrypt的xml格式的字符串
+     * @throws AesException 执行失败,请查看该异常的错误码和具体的错误信息
+     */
+    public String EncryptMsg(String replyMsg, String timeStamp, String nonce) throws AesException {
+        // 加密
+        String encrypt = encrypt(getRandomStr(), replyMsg);
+
+        // 生成安全签名
+        if (timeStamp == "") {
+            timeStamp = Long.toString(System.currentTimeMillis());
+        }
+
+        String signature = SHA1.getSHA1(token, timeStamp, nonce, encrypt);
+
+        // System.out.println("发送给平台的签名是: " + signature[1].toString());
+        // 生成发送的xml
+        String result = XMLParse.generate(encrypt, signature, timeStamp, nonce);
+        return result;
+    }
+
+    /**
+     * 检验消息的真实性,并且获取解密后的明文.
+     * <ol>
+     * 	<li>利用收到的密文生成安全签名,进行签名验证</li>
+     * 	<li>若验证通过,则提取xml中的加密消息</li>
+     * 	<li>对消息进行解密</li>
+     * </ol>
+     *
+     * @param msgSignature 签名串,对应URL参数的msg_signature
+     * @param timeStamp 时间戳,对应URL参数的timestamp
+     * @param nonce 随机串,对应URL参数的nonce
+     * @param postData 密文,对应POST请求的数据
+     *
+     * @return 解密后的原文
+     * @throws AesException 执行失败,请查看该异常的错误码和具体的错误信息
+     */
+    public String DecryptMsg(String msgSignature, String timeStamp, String nonce, String postData)
+            throws AesException {
+
+        // 密钥,公众账号的app secret
+        // 提取密文
+        Object[] encrypt = XMLParse.extract(postData);
+
+        // 验证安全签名
+        String signature = SHA1.getSHA1(token, timeStamp, nonce, encrypt[1].toString());
+
+        // 和URL中的签名比较是否相等
+        // System.out.println("第三方收到URL中的签名:" + msg_sign);
+        // System.out.println("第三方校验签名:" + signature);
+        if (!signature.equals(msgSignature)) {
+            throw new AesException(AesException.ValidateSignatureError);
+        }
+
+        // 解密
+        return decrypt(encrypt[1].toString());
+    }
+
+    public String decryptMsg(String msgSignature, String timeStamp, String nonce, String encrypt) throws AesException {
+        // 验证安全签名
+        String signature = SHA1.getSHA1(token, timeStamp, nonce, encrypt);
+        if (!signature.equals(msgSignature)) {
+            throw new AesException(AesException.ValidateSignatureError);
+        }
+        // 解密
+        return decrypt(encrypt);
+
+    }
+
+    /**
+     * 验证URL
+     * @param msgSignature 签名串,对应URL参数的msg_signature
+     * @param timeStamp 时间戳,对应URL参数的timestamp
+     * @param nonce 随机串,对应URL参数的nonce
+     * @param echoStr 随机串,对应URL参数的echostr
+     *
+     * @return 解密之后的echostr
+     * @throws AesException 执行失败,请查看该异常的错误码和具体的错误信息
+     */
+    public String VerifyURL(String msgSignature, String timeStamp, String nonce, String echoStr)
+            throws AesException {
+        String signature = SHA1.getSHA1(token, timeStamp, nonce, echoStr);
+
+        if (!signature.equals(msgSignature)) {
+            throw new AesException(AesException.ValidateSignatureError);
+        }
+
+        return decrypt(echoStr);
+    }
+
+}

+ 106 - 0
tuon-qywx/src/main/java/cn/tonyandmoney/tuon/qywx/aes/XMLParse.java

@@ -0,0 +1,106 @@
+/**
+ * 对企业微信发送给企业后台的消息加解密示例代码.
+ * 
+ * @copyright Copyright (c) 1998-2014 Tencent Inc.
+ */
+
+// ------------------------------------------------------------------------
+
+package cn.tonyandmoney.tuon.qywx.aes;
+
+import java.io.StringReader;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NodeList;
+import org.xml.sax.InputSource;
+
+/**
+ * XMLParse class
+ *
+ * 提供提取消息格式中的密文及生成回复消息格式的接口.
+ */
+class XMLParse {
+
+	/**
+	 * 提取出xml数据包中的加密消息
+	 * @param xmltext 待提取的xml字符串
+	 * @return 提取出的加密消息字符串
+	 * @throws AesException 
+	 */
+	public static Object[] extract(String xmltext) throws AesException     {
+		Object[] result = new Object[3];
+		try {
+			DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
+			
+			String FEATURE = null;
+			// This is the PRIMARY defense. If DTDs (doctypes) are disallowed, almost all XML entity attacks are prevented
+			// Xerces 2 only - http://xerces.apache.org/xerces2-j/features.html#disallow-doctype-decl
+			FEATURE = "http://apache.org/xml/features/disallow-doctype-decl";
+			dbf.setFeature(FEATURE, true);
+			
+			// If you can't completely disable DTDs, then at least do the following:
+			// Xerces 1 - http://xerces.apache.org/xerces-j/features.html#external-general-entities
+			// Xerces 2 - http://xerces.apache.org/xerces2-j/features.html#external-general-entities
+			// JDK7+ - http://xml.org/sax/features/external-general-entities 
+			FEATURE = "http://xml.org/sax/features/external-general-entities";
+			dbf.setFeature(FEATURE, false);
+			
+			// Xerces 1 - http://xerces.apache.org/xerces-j/features.html#external-parameter-entities
+			// Xerces 2 - http://xerces.apache.org/xerces2-j/features.html#external-parameter-entities
+			// JDK7+ - http://xml.org/sax/features/external-parameter-entities 
+			FEATURE = "http://xml.org/sax/features/external-parameter-entities";
+			dbf.setFeature(FEATURE, false);
+			
+			// Disable external DTDs as well
+			FEATURE = "http://apache.org/xml/features/nonvalidating/load-external-dtd";
+			dbf.setFeature(FEATURE, false);
+			
+			// and these as well, per Timothy Morgan's 2014 paper: "XML Schema, DTD, and Entity Attacks"
+			dbf.setXIncludeAware(false);
+			dbf.setExpandEntityReferences(false);
+			
+			// And, per Timothy Morgan: "If for some reason support for inline DOCTYPEs are a requirement, then 
+			// ensure the entity settings are disabled (as shown above) and beware that SSRF attacks
+			// (http://cwe.mitre.org/data/definitions/918.html) and denial 
+			// of service attacks (such as billion laughs or decompression bombs via "jar:") are a risk."
+			
+			// remaining parser logic
+			DocumentBuilder db = dbf.newDocumentBuilder();
+			StringReader sr = new StringReader(xmltext);
+			InputSource is = new InputSource(sr);
+			Document document = db.parse(is);
+
+			Element root = document.getDocumentElement();
+			NodeList nodelist1 = root.getElementsByTagName("Encrypt");
+			NodeList nodelist2 = root.getElementsByTagName("ToUserName");
+			result[0] = 0;
+			result[1] = nodelist1.item(0).getTextContent();
+			result[2] = nodelist2.item(0).getTextContent();
+			return result;
+		} catch (Exception e) {
+			e.printStackTrace();
+			throw new AesException(AesException.ParseXmlError);
+		}
+	}
+
+	/**
+	 * 生成xml消息
+	 * @param encrypt 加密后的消息密文
+	 * @param signature 安全签名
+	 * @param timestamp 时间戳
+	 * @param nonce 随机字符串
+	 * @return 生成的xml字符串
+	 */
+	public static String generate(String encrypt, String signature, String timestamp, String nonce) {
+
+		String format = "<xml>\n" + "<Encrypt><![CDATA[%1$s]]></Encrypt>\n"
+				+ "<MsgSignature><![CDATA[%2$s]]></MsgSignature>\n"
+				+ "<TimeStamp>%3$s</TimeStamp>\n" + "<Nonce><![CDATA[%4$s]]></Nonce>\n" + "</xml>";
+		return String.format(format, encrypt, signature, timestamp, nonce);
+
+	}
+}

+ 66 - 0
tuon-qywx/src/main/java/cn/tonyandmoney/tuon/qywx/bean/WxMsg.java

@@ -0,0 +1,66 @@
+package cn.tonyandmoney.tuon.qywx.bean;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * @Classname WxMsg
+ * @Description TODO
+ * @Date 2019/8/26 21:25
+ * @Created by Administrator
+ */
+public class WxMsg {
+    @JsonProperty("ToUserName")
+    private String ToUserName;
+    @JsonProperty("Encrypt")
+    private String Encrypt;
+    @JsonProperty("AgentID")
+    private String AgentID;
+    @JsonProperty("MsgId")
+    private String MsgId;
+    @JsonProperty("FromUserName")
+    private String FromUserName;
+
+    public void setFromUserName(String fromUserName) {
+        FromUserName = fromUserName;
+    }
+
+    public void setMsgId(String msgId) {
+        MsgId = msgId;
+    }
+    @JsonIgnore
+    public String getFromUserName() {
+        return FromUserName;
+    }
+    @JsonIgnore
+    public String getMsgId() {
+        return MsgId;
+    }
+
+    @JsonIgnore
+    public String getToUserName() {
+        return ToUserName;
+    }
+
+    public void setToUserName(String toUserName) {
+        ToUserName = toUserName;
+    }
+
+    @JsonIgnore
+    public String getEncrypt() {
+        return Encrypt;
+    }
+
+    public void setEncrypt(String encrypt) {
+        Encrypt = encrypt;
+    }
+
+    @JsonIgnore
+    public String getAgentID() {
+        return AgentID;
+    }
+
+    public void setAgentID(String agentID) {
+        AgentID = agentID;
+    }
+}

+ 6 - 0
tuon-qywx/src/main/kotlin/cn/tonyandmoney/tuon/qywx/QywxConfiguration.kt

@@ -1,5 +1,6 @@
 package cn.tonyandmoney.tuon.qywx
 
+import cn.tonyandmoney.tuon.qywx.aes.WXBizMsgCrypt
 import cn.tonyandmoney.tuon.qywx.service.IQywxService
 import cn.tonyandmoney.tuon.qywx.service.impl.QywxServiceImpl
 import org.springframework.beans.factory.annotation.Autowired
@@ -22,4 +23,9 @@ class QywxConfiguration {
     }
 
 
+    @Bean
+    fun bizMsgCrypt():WXBizMsgCrypt{
+        return WXBizMsgCrypt(mProperties.token,mProperties.encodingAESKey,mProperties.corpid)
+    }
+
 }

+ 65 - 6
tuon-qywx/src/main/kotlin/cn/tonyandmoney/tuon/qywx/controller/QywxController.kt

@@ -6,25 +6,41 @@ import cn.tonyandmoney.tuon.core.error.OpException
 import cn.tonyandmoney.tuon.core.error.SessionNoUserException
 import cn.tonyandmoney.tuon.core.session.SessionUtils
 import cn.tonyandmoney.tuon.core.user.IUser
+import cn.tonyandmoney.tuon.qywx.QywxProperties
 import cn.tonyandmoney.tuon.qywx.WxErrorCode
+import cn.tonyandmoney.tuon.qywx.aes.WXBizMsgCrypt
+import cn.tonyandmoney.tuon.qywx.bean.WxMsg
 import cn.tonyandmoney.tuon.qywx.bean.WxUser
 import cn.tonyandmoney.tuon.qywx.service.IQywxService
+import com.fasterxml.jackson.databind.ObjectMapper
+import org.slf4j.LoggerFactory
+import org.slf4j.MarkerFactory
 import org.springframework.beans.factory.annotation.Autowired
-import org.springframework.web.bind.annotation.GetMapping
-import org.springframework.web.bind.annotation.RequestMapping
-import org.springframework.web.bind.annotation.RequestParam
-import org.springframework.web.bind.annotation.RestController
+import org.springframework.beans.factory.annotation.Qualifier
+import org.springframework.core.io.buffer.DataBufferUtils
+import org.springframework.http.server.reactive.ServerHttpRequest
+import org.springframework.web.bind.annotation.*
 import org.springframework.web.server.ServerWebExchange
 import reactor.core.publisher.Mono
-import reactor.core.publisher.onErrorResume
-import java.util.function.Function
 
 @RestController
 @RequestMapping("/qywx")
 class QywxController {
 
+    companion object {
+        private val logger = LoggerFactory.getLogger(QywxController::class.java.simpleName)
+        private val msgMarker = MarkerFactory.getMarker("WxMsg")
+    }
+
     @Autowired
     private lateinit var qywxService: IQywxService
+    @Autowired
+    private lateinit var properties: QywxProperties
+    @Autowired
+    private lateinit var wxBizMsgCrypt: WXBizMsgCrypt
+    @Autowired
+    @Qualifier("xmlObjectMapper")
+    private lateinit var objectMapper: ObjectMapper
 
     @GetMapping("/user")
     fun getUserInfo(@RequestParam("code", required = false) code: String?,
@@ -53,4 +69,47 @@ class QywxController {
 
     }
 
+    /**
+     * 接收消息
+     */
+    @PostMapping("/config/message", produces = ["application/xml;charset=UTF-8"], consumes = ["application/xml;charset=UTF-8"])
+    fun configUserMessage(@RequestParam("msg_signature") signature: String,
+                          @RequestParam("timestamp") timestamp: String,
+                          @RequestParam("nonce") nonce: String,
+                          request: ServerHttpRequest): Mono<String> {
+
+        return DataBufferUtils.join(request.body)
+                .flatMap { dataBuffer ->
+                    Mono.create<String> {
+                        val bytes = ByteArray(dataBuffer.readableByteCount())
+                        dataBuffer.read(bytes)
+                        logger.info(msgMarker, "content:{}", String(bytes))
+                        val wxMsg = objectMapper.readValue<WxMsg>(bytes, WxMsg::class.java)
+                        val content = wxBizMsgCrypt.decryptMsg(signature, timestamp, nonce, wxMsg.encrypt)
+                        val resp = ""
+                        if ("值班" == content) {
+
+                        } else {
+
+                        }
+                        Mono.just(resp)
+                    }
+                }
+
+    }
+
+    /**
+     * 验证URL
+     */
+    @GetMapping("/config/message")
+    fun verifySignature(@RequestParam("msg_signature") signature: String,
+                        @RequestParam("timestamp") timestamp: String,
+                        @RequestParam("nonce") nonce: String,
+                        @RequestParam("echostr") echostr: String): Mono<String> {
+        return Mono.create {
+            val utils = WXBizMsgCrypt(properties.token, properties.encodingAESKey, properties.corpid)
+            it.success(utils.VerifyURL(signature, timestamp, nonce, echostr))
+        }
+    }
+
 }

+ 35 - 0
tuon-web/build.gradle

@@ -8,3 +8,38 @@ dependencies {
    compile project(':tuon-core')
    compile project(':tuon-qywx')
 }
+
+task dockerBuilder(type: Docker) {
+   registry='docker.tonyandmoney.cn'
+   applicationName = 'tuonq-config'
+   tagVersion = jar.getArchiveVersion()
+   addFile("./${jar.getArchiveBaseName()}-${jar.getArchiveVersion()}.jar","app.jar")
+   entryPoint(["java","-XX:+UnlockExperimentalVMOptions","-XX:+UseCGroupMemoryLimitForHeap","-Djava.security.egd=file:/dev/./urandom","-Duser.timezone=GMT+08","-jar",'app.jar'])
+   exposePort(11000)
+   doFirst {
+      copy {
+         from jar
+         into stageDir
+      }
+   }
+   push = true
+}
+
+task innerDockerBuilder(type: Docker){
+   registry='registry.tuonq.cn:5000'
+   applicationName = 'tuonq-config'
+   tagVersion = jar.getArchiveVersion()
+   addFile("./${jar.getArchiveBaseName()}-${jar.getArchiveVersion()}.jar","app.jar")
+   entryPoint(["java","-XX:+UnlockExperimentalVMOptions","-XX:+UseCGroupMemoryLimitForHeap","-Djava.security.egd=file:/dev/./urandom","-Duser.timezone=GMT+08","-jar",'app.jar'])
+   exposePort(11000)
+   doFirst {
+      copy {
+         from jar
+         into stageDir
+      }
+   }
+   push = true
+}
+
+dockerBuilder.dependsOn(bootJar)
+innerDockerBuilder.dependsOn(bootJar)

+ 5 - 0
tuon-web/src/main/resources/application.yml

@@ -5,6 +5,11 @@ spring:
     type: com.zaxxer.hikari.HikariDataSource
     driver-class-name: com.mysql.jdbc.Driver
     url: jdbc:mysql://192.168.42.1:5201/tuonq_fw?useUnicode=true&characterEncoding=utf-8&useSSL=false&zeroDateTimeBehavior=convertToNull
+  http:
+    converters:
+      preferred-json-mapper: jackson
+
+
 
 # 企业微信配置
 qywx: