Browse Source

企业微信回调接口配置

zhouy 1 year ago
parent
commit
233a75dd41

+ 1 - 0
build.gradle

@@ -32,6 +32,7 @@ subprojects { Project subproject ->
         ojdbc = 'com.oracle:ojdbc6:11.2.0'
         fastjson = 'com.alibaba:fastjson:1.2.47'
         threadLocal = 'com.alibaba:transmittable-thread-local:2.2.0'
+        commonscodec = '#.org.apache.commons:commons-codec:1.9'
         
         // repoBaseUrl = "https://maven.ubtob.net/artifactory"
         repoBaseUrl = "http://10.1.81.211:1202/artifactory"

+ 1 - 0
qywx-sdk/build.gradle

@@ -2,5 +2,6 @@ dependencies {
     compile "org.springframework:spring-web"
     compile "org.springframework:spring-context"
     compile "$fastjson"
+    compile "$commonscodec"
     testCompile "junit:junit"
 }

+ 59 - 0
qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/weixin/AesException.java

@@ -0,0 +1,59 @@
+package com.usoftchina.qywx.sdk.weixin;
+
+@SuppressWarnings("serial")
+public class AesException extends Exception {
+
+	public final static int OK = 0;
+	public final static int ValidateSignatureError = -40001;
+	public final static int ParseJsonError = -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 GenReturnJsonError = -40011;
+
+	private int code;
+
+	private static String getMessage(int code) {
+		switch (code) {
+		case ValidateSignatureError:
+			return "签名验证错误";
+		case ParseJsonError:
+			return "json解析失败";
+		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 GenReturnJsonError:
+			return "josn生成失败";
+		default:
+			return null; // cannot be
+		}
+	}
+
+	public int getCode() {
+		return code;
+	}
+
+	AesException(int code) {
+		super(getMessage(code));
+		this.code = code;
+	}
+
+}

+ 26 - 0
qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/weixin/ByteGroup.java

@@ -0,0 +1,26 @@
+package com.usoftchina.qywx.sdk.weixin;
+
+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();
+	}
+}

+ 54 - 0
qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/weixin/JsonParse.java

@@ -0,0 +1,54 @@
+
+package com.usoftchina.qywx.sdk.weixin;
+
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+
+/**
+ * JsonParse class
+ *
+ * 提供提取消息格式中的密文及生成回复消息格式的接口.
+ */
+public class JsonParse {
+
+	/**
+	 * 提取出 JSON 包中的加密消息
+	 * @param jsontext 待提取的json字符串
+	 * @return 提取出的加密消息字符串
+	 * @throws AesException
+	 */
+	public static Object[] extract(String jsontext) throws AesException     {
+		Object[] result = new Object[3];
+		try {
+
+			JSONObject json = JSON.parseObject(jsontext);
+        	String encrypt_msg = json.getString("encrypt");
+			String tousername  = json.getString("tousername");
+			String agentid     = json.getString("agentid");
+
+			result[0] = tousername;
+			result[1] = encrypt_msg;
+			result[2] = agentid;
+			return result;
+		} catch (Exception e) {
+			e.printStackTrace();
+			throw new AesException(AesException.ParseJsonError);
+		}
+	}
+
+	/**
+	 * 生成json消息
+	 * @param encrypt 加密后的消息密文
+	 * @param signature 安全签名
+	 * @param timestamp 时间戳
+	 * @param nonce 随机字符串
+	 * @return 生成的json字符串
+	 */
+	public static String generate(String encrypt, String signature, String timestamp, String nonce) {
+
+		String format = "{\"encrypt\":\"%1$s\",\"msgsignature\":\"%2$s\",\"timestamp\":\"%3$s\",\"nonce\":\"%4$s\"}";
+		return String.format(format, encrypt, signature, timestamp, nonce);
+
+	}
+}

+ 67 - 0
qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/weixin/PKCS7Encoder.java

@@ -0,0 +1,67 @@
+/**
+ * 对企业微信发送给企业后台的消息加解密示例代码.
+ * 
+ * @copyright Copyright (c) 1998-2014 Tencent Inc.
+ */
+
+// ------------------------------------------------------------------------
+
+package com.usoftchina.qywx.sdk.weixin;
+
+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
qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/weixin/SHA1.java

@@ -0,0 +1,61 @@
+/**
+ * 对企业微信发送给企业后台的消息加解密示例代码.
+ * 
+ * @copyright Copyright (c) 1998-2014 Tencent Inc.
+ */
+
+// ------------------------------------------------------------------------
+
+package com.usoftchina.qywx.sdk.weixin;
+
+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);
+		}
+	}
+}

+ 258 - 0
qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/weixin/WXBizJsonMsgCrypt.java

@@ -0,0 +1,258 @@
+package com.usoftchina.qywx.sdk.weixin;
+
+
+import javax.crypto.Cipher;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+import java.nio.charset.Charset;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.Random;
+
+
+public class WXBizJsonMsgCrypt {
+	private static Charset CHARSET = Charset.forName("utf-8");;
+	private byte[] aesKey;
+	private String token;
+	private String receiveid;
+
+	/**
+	 * 构造函数
+	 * @param token 企业微信后台,开发者设置的token
+	 * @param encodingAesKey 企业微信后台,开发者设置的EncodingAESKey
+	 * @param receiveid, 不同场景含义不同,详见文档
+	 * 
+	 * @throws AesException 执行失败,请查看该异常的错误码和具体的错误信息
+	 */
+	public WXBizJsonMsgCrypt(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.getDecoder().decode(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.getEncoder().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.getDecoder().decode(text);
+
+			// 解密
+			original = cipher.doFinal(encrypted);
+		} catch (Exception e) {
+			e.printStackTrace();
+			throw new AesException(AesException.DecryptAESError);
+		}
+
+		String jsonContent, from_receiveid;
+		try {
+			// 去除补位字符
+			byte[] bytes = PKCS7Encoder.decode(original);
+
+			// 分离16位随机字符串,网络字节序和receiveid
+			byte[] networkOrder = Arrays.copyOfRange(bytes, 16, 20);
+
+			int jsonLength = recoverNetworkBytesOrder(networkOrder);
+
+			jsonContent = new String(Arrays.copyOfRange(bytes, 20, 20 + jsonLength), CHARSET);
+			from_receiveid = new String(Arrays.copyOfRange(bytes, 20 + jsonLength, 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 jsonContent;
+
+	}
+
+	/**
+	 * 将企业微信回复用户的消息加密打包.
+	 * <ol>
+	 * 	<li>对要发送的消息进行AES-CBC加密</li>
+	 * 	<li>生成安全签名</li>
+	 * 	<li>将消息密文和安全签名打包成json格式</li>
+	 * </ol>
+	 * 
+	 * @param replyMsg 企业微信待回复用户的消息,json格式的字符串
+	 * @param timeStamp 时间戳,可以自己生成,也可以用URL参数的timestamp
+	 * @param nonce 随机串,可以自己生成,也可以用URL参数的nonce
+	 * 
+	 * @return 加密后的可以直接回复用户的密文,包括msg_signature, timestamp, nonce, encrypt的json格式的字符串
+	 * @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);
+
+		String result = JsonParse.generate(encrypt, signature, timeStamp, nonce);
+		return result;
+	}
+
+	/**
+	 * 检验消息的真实性,并且获取解密后的明文.
+	 * <ol>
+	 * 	<li>利用收到的密文生成安全签名,进行签名验证</li>
+	 * 	<li>若验证通过,则提取json中的加密消息</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 = JsonParse.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);
+		}
+
+		// 解密
+		String result = decrypt(encrypt[1].toString());
+		return result;
+	}
+
+	/**
+	 * 验证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);
+		}
+
+		String result = decrypt(echoStr);
+		return result;
+	}
+
+}

+ 1 - 1
uas-office-qywx-server/src/main/resources/config/application-dev.yaml

@@ -1,4 +1,4 @@
 spring:
   redis:
-    host: 10.1.81.2
+    host: 10.1.81.158
     port: 6379

+ 50 - 0
uas-office-qywx/src/main/java/com/usoftchina/uas/office/qywx/manage/controller/QywxCallbackController.java

@@ -0,0 +1,50 @@
+package com.usoftchina.uas.office.qywx.manage.controller;
+
+import com.usoftchina.qywx.sdk.util.HttpUtils;
+import com.usoftchina.uas.office.dto.Result;
+import com.usoftchina.uas.office.qywx.manage.service.QywxCallbackService;
+import com.usoftchina.uas.office.util.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.PrintWriter;
+
+/**
+ * 企业微信代开发回调接口
+ * */
+@RestController
+public class QywxCallbackController {
+
+    @Autowired
+    @Lazy
+    private QywxCallbackService qywxCallbackService;
+    /**
+     * 消息URL验证
+     * */
+    @GetMapping(value = "/qywx/Callback")
+    public void verifyURL(@RequestParam(name = "msg_signature")  String msgSignature,
+                            @RequestParam(name = "timestamp")  Integer timestamp,
+                            @RequestParam(name = "nonce")  String nonce,
+                            @RequestParam(name = "echostr")  String echostr , HttpServletResponse response) throws IOException {
+        String sEchoStr = qywxCallbackService.verifyURL(msgSignature, timestamp, nonce, echostr);
+        PrintWriter out = response.getWriter();
+        if(!StringUtils.isEmpty(sEchoStr)) {
+            out.write(sEchoStr);
+        }
+        out.flush();
+    }
+    /**
+     * 消息接收
+     * */
+    @PostMapping(path = "/qywx/Callback")
+    public String  receiveMessage(@PathVariable("msg_signature") String msg_signature, @PathVariable("timestamp") String timestamp,
+                                 @PathVariable("nonce") String nonce, @RequestBody String jsonMsg ){
+        return  qywxCallbackService.callBack(msg_signature, timestamp, nonce, jsonMsg);
+    }
+}
+
+
+

+ 52 - 0
uas-office-qywx/src/main/java/com/usoftchina/uas/office/qywx/manage/service/QywxCallbackService.java

@@ -0,0 +1,52 @@
+package com.usoftchina.uas.office.qywx.manage.service;
+
+import com.alibaba.fastjson.JSON;
+import com.usoftchina.qywx.sdk.weixin.AesException;
+import com.usoftchina.qywx.sdk.weixin.WXBizJsonMsgCrypt;
+import com.usoftchina.uas.office.service.AbstractService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+
+@Service
+public class QywxCallbackService extends AbstractService {
+
+    private final Logger logger = LoggerFactory.getLogger(QywxCallbackService.class);
+
+    public String verifyURL(String msgSignature, Integer timestamp, String nonce, String echostr){
+        //TODO 获取参数回调配置
+        String sToken = "fD8frpMuXpkPLUM";
+        String sEncodingAESKey = "73euRc0KW5IbOPNmD6IRFqGAYhcCBRUBrIloAPBLvsR";
+        String sCorpID = "wwbb7e27c4decb7872";
+        String callBackStr = null;
+        try {
+            WXBizJsonMsgCrypt wxcpt = new WXBizJsonMsgCrypt(sToken, sEncodingAESKey, sCorpID);
+            callBackStr = wxcpt.VerifyURL(msgSignature, String.valueOf(timestamp),
+                    nonce, echostr);
+        } catch (AesException e) {
+            e.printStackTrace();
+        }
+
+        return null;
+    }
+
+    public String callBack(String msgSignature, String timestamp, String nonce, String jsonMsg){
+        //TODO 获取参数回调配置
+        String sToken = "fD8frpMuXpkPLUM";
+        String sEncodingAESKey = "73euRc0KW5IbOPNmD6IRFqGAYhcCBRUBrIloAPBLvsR";
+        String sCorpID = "wwbb7e27c4decb7872";
+        String decryptMsg = null;
+        try {
+            WXBizJsonMsgCrypt wxcpt = new WXBizJsonMsgCrypt(sToken, sEncodingAESKey, sCorpID);
+            decryptMsg = wxcpt.DecryptMsg(msgSignature, timestamp, nonce, jsonMsg);
+
+            JSON.parseObject(decryptMsg);
+
+        } catch (AesException e) {
+            e.printStackTrace();
+        }
+
+        return null;
+    }
+}
+