java对接企业微信
一、注册企业微信
1.1 简介
企业微信与微信具有一样的体验,通过企业内部与外部客户的管理,构建出社群生态。企业微信提供丰富的api进行调用获取数据管理,也提供各种回调事件。
1.2 注册
登录官网,一键注册即可。链接: 企业微信
1.2 填写主要信息
企业微信中填写相关企业信息和负责人,然后创建。进入即可添加所需要的微信人员。
之后进行通讯录同步(此步骤为最重要一点),同步过后通讯录的人员根据调用接口接收消息。
1.3 创建应用
创建自己需要的应用,并根据提示创建应用(也可以不创建,用以前有的应用作为发送消息的主体也可以)
二、企业微信基础信息
创建完企业微信和自己所需要的应用后,需要记住几处重要的参数,方便后续调用api使用。
简单介绍几个参数:(需存入数据库)
1.企业id:当前企业微信的固定id;
2.agentid:当前自建应用id;
3.secret:应用秘钥,调用验证秘钥;
注:企业应用的可见范围需添加微信中的人员表。为后续调用查询、发送人员作限定选择。
三、消息接收url机制-回调
在java集成企业微信的同时,需要一个回调服务。现可以实现:
自定义丰富的服务行为、可以及时获取状态变化。
通过企业微信服务器向后端服务器发送各种需要消息。
这里只讲配置建立消息接收url机制。
附录上述两张图,其中包含设置可信ip(开发服务器的ip,为了服务器调用api)、配置接收服务器(通过回调机制+配置,验证url是否能接收到消息,并返回企业微信所需要的信息来校验)。加密解密见附录五。
回调配置
能通过发送来的密文(附录5中具体解密),返回给企业微信端需要的密文。即可通过校验,然后就可以填写认证ip进行开发调用接口。
简单介绍几个参数:(需存入数据库)
1.url:接收企业微信消息的接口
2.token:计算签名(可在配置中随机生成)
3.encoding_aes_key:加密key(加解密使用)
四、java对接企业微信(自建应用)
在上几个步骤中,我们已有需求的5个参数配置。根据这5个参数就可以进行与java对接。
4.1 对接前提(获取access_token)
token是调用接口的基础。根据之前的保存参数即可获取。以下展示为项目所需接口调用,其他接口使用token基本都能调用。
4.2 手机号获取userid
4.3 发送应用消息
本文只展示文本方面,其他发送信息方面可参照api接口。api接口
五、附加-校验加密解密方式
5.1 方案说明
5.2 代码实例
/*** @Author sprem至尊* @Date 2022/10/21 16:43* @Version 1.0*/
package com.example.demo.controller;import com.example.demo.aes.WXBizJsonMsgCrypt;
import com.example.demo.cache.TokenCache;
import com.example.demo.entity.TestEntity;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
public class TestController {@Autowiredprivate TokenCache tokenCache;@RequestMapping("/")private String home(TestEntity testEntity) {//postman的参数放在form-data中 不然特殊符号会变空格String sToken = "axBoJ9yw3NeLPffBNUTQlfXiP612Xdpw";//SSrYt12q2KL QDG6eKString sCorpID = "ww1ca8f5fe0f582941e9";//""wwca8f511fe0f58941e9"; wx5823bf96d3bd561c7String sEncodingAESKey = "Sb8eDJndNDPjIyzqFzMI2sKtOfgoCYMrIWjqu2m1ql3Vb";// W5UBw9XoqW61HPfL7TUsI4qC2myJrw3MQ9bDrbDsWWQ2//jWmYm7qr5nMoAUwZRjGtBxmz3K4A1tkAj3ykkR6q2B2CString sVerifyMsgSig = testEntity.getMsg_signature();String sVerifyTimeStamp = testEntity.getTimestamp();String sVerifyNonce = testEntity.getNonce();String sVerifyEchoStr = testEntity.getEchostr();String sEchoStr ="";//明文try {//获取其中 aesKeyWXBizJsonMsgCrypt wxcpt = new WXBizJsonMsgCrypt(sToken, sEncodingAESKey, sCorpID);//获取明文sEchoStr = wxcpt.VerifyURL(sVerifyMsgSig,sVerifyTimeStamp,sVerifyNonce,sVerifyEchoStr);System.out.println("verifyurl echostr: " + sEchoStr);}catch (Exception e){e.printStackTrace();}return sEchoStr;}@RequestMapping("/testCache")private String testCache() {String s = TokenCache.getValue("s1111");if(s==null){System.out.println("时间过期");TokenCache.setKey("s12111","1243123123");s =TokenCache.getValue("s12111");}System.out.println(s);return "sss";}
}
/*** @Author sprem至尊* @Date 2022/10/21 17:00* @Version 1.0*/
package com.example.demo.entity;
import lombok.Data;
@Data
public class TestEntity {private String msg_signature;private String timestamp;private String nonce;private String echostr;
}
package com.example.demo.aes;@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;}}
package com.example.demo.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();}
}
/*** 对企业微信发送给企业后台的消息加解密示例代码.* * @copyright Copyright (c) 1998-2020 Tencent Inc.*/// ------------------------------------------------------------------------package com.example.demo.aes;/*** 针对 org.json.JSONObject,* 要编译打包架包json* 官方源码下载地址 : https://github.com/stleary/JSON-java, jar包下载地址 : https://mvnrepository.com/artifact/org.json/json*/
import org.json.JSONObject;/*** JsonParse class** 提供提取消息格式中的密文及生成回复消息格式的接口.*/
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 = new JSONObject(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);}
}
/*** 对企业微信发送给企业后台的消息加解密示例代码.* * @copyright Copyright (c) 1998-2014 Tencent Inc.*/// ------------------------------------------------------------------------package com.example.demo.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;}}
/*** 对企业微信发送给企业后台的消息加解密示例代码.* * @copyright Copyright (c) 1998-2014 Tencent Inc.*/// ------------------------------------------------------------------------package com.example.demo.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);}}
}
/*** 对企业微信发送给企业后台的消息加解密示例代码.* * @copyright Copyright (c) 1998-2014 Tencent Inc.*/// ------------------------------------------------------------------------/*** 针对org.apache.commons.codec.binary.Base64,* 需要导入架包commons-codec-1.9(或commons-codec-1.8等其他版本)* 官方下载地址:http://commons.apache.org/proper/commons-codec/download_codec.cgi*/
package com.example.demo.aes;import org.apache.commons.codec.binary.Base64;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.Random;/*** 提供接收和推送给企业微信消息的加解密接口(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 WXBizJsonMsgCrypt {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 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.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 + receiveidbyteCollector.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 jsonContent, from_receiveid;try {// 去除补位字符byte[] bytes = PKCS7Encoder.decode(original);// 分离16位随机字符串,网络字节序和receiveidbyte[] 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);// System.out.println("发送给平台的签名是: " + signature[1].toString());// 生成发送的jsonString 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;}
}
/*** @Author sprem至尊* @Date 2022/10/24 17:55* @Version 1.0*/
package com.example.demo.cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
@Component
public class TokenCache {private static LoadingCache<String,String> loadingCache = CacheBuilder.newBuilder().initialCapacity(100).maximumSize(100).expireAfterWrite(5, TimeUnit.SECONDS).build(new CacheLoader<String, String>() {@Overridepublic String load(String key) throws Exception {System.out.println("loadkey::::"+key);System.out.println("loadValue::::"+getValue(key));return null;}});public static void setKey(String key,String value){loadingCache.put(key, value);}public static String getValue(String key){String value = null;try{value = loadingCache.get(key);if(value == null){return null;}return value;}catch (Exception e){return null;}}
}
六、附加-对接第三方应用
可在开发中心详细查看,这里不做过多介绍,如后续项目需使用,即可添加。