企业微信开发之代开发应用
- 创建应用
- 安装应用
- 配置回调url
- 管理员授权,拿到临时授权码 auth_code,仅10分钟有效。需要在回调url中保存,并换取永久授权码 permanent_code
- 获取permanent_code流程如下:
- 获取 suite_ticket,配置回调URL后,每10分钟会传一次过来,保存备用
- 获取SuiteAccessToken
- 获取永久permanet_code
- 小程序中调用 wx.qy.login登陆 ,拿 到code, 获取 userid
- 获取userid流程如下
- 获取access_token,通过 corpid+permanet_code
- 通过 JSCODE2SESSION 获取 Userid (加密)
- 获取用户详情,通过 https://qyapi.weixin.qq.com/cgi-bin/user/get?access_token=%s&userid=%s"获取用户详情,只有deptment等信息。
login代码参考:
@ApiOperation(value = "企微登陆") @ApiImplicitParams({@ApiImplicitParam(name = "code", value = "code"),}) @GetMapping("login") public Result login(@RequestParam(value = "code", required = false) String code, HttpServletRequest request) { log.info("企微登陆, code: " + code); try { String qywx_access_token = redisUtil.getString("qywx_access_token"); if (StringUtil.isEmpty(qywx_access_token)) { String permanent_code = configService.getValueByKey("permanent_code"); if (StringUtil.isEmpty(permanent_code)) { String suite_ticket = configService.getValueByKey("suite_ticket"); if (suite_ticket == null) { return Result.error("suite_ticket不存在"); } String suiteAccessToken = QywxUtil.getInstance().getSuiteAccessToken(config.getSuiteId(), config.getSuiteSecret(), suite_ticket); log.info("suiteAccessToken: " + suiteAccessToken); String auth_code = configService.getValueByKey("auth_code"); if (auth_code == null) { return Result.error("auth_code不存在"); } permanent_code = QywxUtil.getInstance().getPermanentCode(suiteAccessToken, auth_code); log.info("permanent_code: " + permanent_code); if (StringUtil.isNotEmpty(permanent_code)) { configService.saveValueByKey("permanent_code", permanent_code); } } else { //log.info("permanent_code: " + permanent_code); } qywx_access_token = QywxUtil.getInstance().getAccessToken(config.getCorpIdAesopOpen(), permanent_code); log.info("qywx_access_token: " + qywx_access_token); } String userid = QywxUtil.getInstance().getUserid(qywx_access_token, code); //解密userid //QywxUtil.getInstance().decorypt(); log.info("userid: " + userid); if (StringUtil.isEmpty(userid)) { return Result.error("获取userid失败"); } WxCpUserDetail wxCpUser = QywxUtil.getInstance().getUserDetail(qywx_access_token, userid); if (wxCpUser == null) { return Result.error("用户不存在"); } String auth_department_id = redisUtil.getString("auth_department_id"); if (StringUtil.isEmpty(auth_department_id)) { auth_department_id = configService.getValueByKey("auth_department_id"); redisUtil.set("auth_department_id", auth_department_id, TimeUnit.DAYS.toSeconds(1)); } if (wxCpUser.getDepartment() != null && wxCpUser.getDepartment().contains(Integer.parseInt(auth_department_id))) { Map<String, Object> data = new HashMap<String, Object>(); JWTUser jwtUser = new JWTUser(wxCpUser.getName(), userid, PlatformType.RC); data.put("token", JwtTokenUtil.sign(jwtUser)); data.put("user", jwtUser); return Result.success(data); } else { return Result.error("用户没有部门权限"); } } catch (Exception e) { log.error(e.getMessage(), e); return Result.error("未知错误,请联系管理员"); } }
callback代码参考 :
@RequestMapping(value = "callback", produces = "text/plain;charset=utf-8", method = {RequestMethod.GET, RequestMethod.POST}) public void callback(@RequestParam(value = "msg_signature", required = false) String signature, @RequestParam(value = "timestamp", required = false) String timestamp, @RequestParam(value = "nonce", required = false) String nonce, @RequestParam(value = "echostr", required = false) String echostr, HttpServletRequest request, HttpServletResponse response) throws IOException { log.info("qywx callback"); log.info("msg_signature:{}", signature); log.info("timestamp:{}", timestamp); log.info("nonce:{}", nonce); log.info("echostr:{}", echostr); response.setContentType("text/plain;charset=utf-8"); PrintWriter res = response.getWriter(); if (StringUtil.isNotEmpty(echostr)) { try { WXBizJsonMsgCrypt hdWxcpt = new WXBizJsonMsgCrypt(config.getCallbackToken(), config.getEncodingAeskey(), config.getCorpIdAesopOpen()); String result = hdWxcpt.VerifyURL(signature, timestamp, nonce, echostr); log.info("企业微信模板URL回调:{}", result); res.write(result); return; } catch (Exception e) { log.error("企业微信-回调错误!", e); res.write(e.getMessage()); } } else { try { WXBizMsgCrypt hdWxcpt = new WXBizMsgCrypt(config.getCallbackToken(), config.getEncodingAeskey(), config.getSuiteId()); String sReqData = IOUtils.toString(request.getInputStream(), "UTF-8"); String sMsg = hdWxcpt.DecryptMsg(signature, timestamp, nonce, sReqData); log.info("=========================after decrypt msg: " + sMsg); // TODO: 解析出明文json标签的内容进行处理 // For example: if (sMsg.indexOf("create_auth") > 0) { String startStr = "<AuthCode><![CDATA["; int start = sMsg.indexOf(startStr) + startStr.length(); // 然后,找到SuiteTicket标签的结束位置 int end = sMsg.indexOf("]]></AuthCode>"); // 提取SuiteTicket的内容 String authCodeContent = sMsg.substring(start, end); log.info("AuthCode: " + authCodeContent); //更新AuthCode SysConfig sysConfig = configService.getByKey("auth_code"); if (sysConfig == null) { sysConfig = new SysConfig(); sysConfig.setKey("auth_code"); sysConfig.setValue(authCodeContent); sysConfig.setTitle("auth_code"); sysConfig.setCreateTime(LocalDateTime.now()); sysConfig.setStatus(1); configService.save(sysConfig); } else { sysConfig.setValue(authCodeContent); configService.updateData(sysConfig); } } else if (sMsg.indexOf("suite_ticket") > 0) { String startStr = "<SuiteTicket><![CDATA["; int start = sMsg.indexOf(startStr) + startStr.length(); // 然后,找到SuiteTicket标签的结束位置 int end = sMsg.indexOf("]]></SuiteTicket>"); // 提取SuiteTicket的内容 String suiteTicketContent = sMsg.substring(start, end); log.info("SuiteTicket: " + suiteTicketContent); //更新SuiteTicket SysConfig sysConfig = configService.getByKey("suite_ticket"); if (sysConfig == null) { sysConfig = new SysConfig(); sysConfig.setKey("suite_ticket"); sysConfig.setValue(suiteTicketContent); sysConfig.setTitle("suite_ticket"); sysConfig.setCreateTime(LocalDateTime.now()); sysConfig.setStatus(1); configService.save(sysConfig); } else { sysConfig.setValue(suiteTicketContent); configService.updateData(sysConfig); } } res.write("success"); } catch (Exception e) { // TODO // 解密失败,失败原因请查看异常 log.error("解密失败,失败原因请查看异常", e); res.write(e.getMessage()); } } }
QywxUtil:
package com.artefact.common.utils; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.artefact.common.config.QywxConfig; import com.artefact.common.core.SpringContextHolder; import com.artefact.common.model.WxCpUserDetail; import lombok.Data; import lombok.extern.log4j.Log4j2; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.stereotype.Component; import org.springframework.web.client.RestTemplate; import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; @Component @Log4j2 public class QywxUtil { public static QywxUtil getInstance() { return SpringContextHolder.getBean(QywxUtil.class); } @Autowired QywxConfig config; @Autowired RedisUtil redisUtil; private static final String SUITE_ACCESS_TOKEN_URL = "https://qyapi.weixin.qq.com/cgi-bin/service/get_suite_token?suite_id=%s&suite_secret=%s&suite_ticket=%s"; private static final String GET_AUTH_INFO_URL = "https://qyapi.weixin.qq.com/cgi-bin/service/get_auth_info?suite_access_token={suiteAccessToken}"; private static final String GET_PERMANENT_CODE = "https://qyapi.weixin.qq.com/cgi-bin/service/get_permanent_code?suite_access_token=%s"; private static final String JSCODE2SESSION_URL = "https://qyapi.weixin.qq.com/cgi-bin/miniprogram/jscode2session?access_token=%s&js_code=%s&grant_type=authorization_code"; private static final String GET_ACCESS_TOKEN_URL = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=%s&corpsecret=%s"; private static final String GET_USER_INFO_URL = "https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo?access_token=%s&code=%s"; public String getSuiteAccessToken(String suiteId, String suiteSecret, String suiteTicket) { try { RestTemplate restTemplate = new RestTemplate(); Map<String, String> data = new HashMap<>(); data.put("suite_id", suiteId); data.put("suite_secret", suiteSecret); data.put("suite_ticket", suiteTicket); String response = restTemplate.postForObject(SUITE_ACCESS_TOKEN_URL, data, String.class); log.info("GET_SUITE_ACCESS_TOKEN:"); log.info(response); // 解析响应字符串,提取suite_access_token JSONObject jsonObject = JSON.parseObject(response); String suiteAccessToken = jsonObject.getString("suite_access_token"); if (StringUtil.isNotEmpty(suiteAccessToken)) { redisUtil.set("suite_access_token", suiteAccessToken, TimeUnit.HOURS.toSeconds(1)); } return suiteAccessToken; } catch (Exception e) { log.error(e.getMessage(), e); return null; } } public String getPermanentCode(String SUITE_ACCESS_TOKEN, String auth_code) { try { RestTemplate restTemplate = new RestTemplate(); String url = String.format(GET_PERMANENT_CODE, SUITE_ACCESS_TOKEN); Map<String, String> data = new HashMap<>(); data.put("auth_code", auth_code); String response = restTemplate.postForObject(url, data, String.class); log.info("GET_PERMANENT_CODE:"); log.info(response); // 解析响应字符串,提取suite_access_token JSONObject jsonObject = JSON.parseObject(response); String permanent_code = jsonObject.getString("permanent_code"); if (StringUtil.isNotEmpty(permanent_code)) { redisUtil.set("permanent_code", permanent_code, TimeUnit.HOURS.toSeconds(1)); } return permanent_code; } catch (Exception e) { log.error(e.getMessage(), e); return null; } } public String getAccessToken(String corpId, String corpSecret) { String url = String.format(GET_ACCESS_TOKEN_URL, corpId, corpSecret); RestTemplate restTemplate = new RestTemplate(); String response = restTemplate.getForObject(url, String.class); log.info("GET_ACCESS_TOKEN:"); log.info(response); JSONObject jsonObject = JSON.parseObject(response); String qywx_access_token = jsonObject.getString("access_token"); if (StringUtil.isNotEmpty(qywx_access_token)) { redisUtil.set("qywx_access_token", qywx_access_token, TimeUnit.HOURS.toSeconds(1)); } return qywx_access_token; } public String getUserid(String accessToken, String code) { try { RestTemplate restTemplate = new RestTemplate(); String url = String.format(JSCODE2SESSION_URL, accessToken, code); String response = restTemplate.getForObject(url, String.class); log.info("GET USERID BY JSCODE2SESSION:"); log.info(response); // 解析响应字符串,提取suite_access_token JSONObject jsonObject = JSON.parseObject(response); return jsonObject.getString("userid"); } catch (Exception e) { log.error(e.getMessage(), e); return null; } } private static final String GET_USER_DETAIL_URL = "https://qyapi.weixin.qq.com/cgi-bin/user/get?access_token=%s&userid=%s"; public WxCpUserDetail getUserDetail(String accessToken, String userid) { try { RestTemplate restTemplate = new RestTemplate(); String url = String.format(GET_USER_DETAIL_URL, accessToken, userid); //WxCpUserDetail response = restTemplate.getForObject(url, WxCpUserDetail.class); String response = restTemplate.getForObject(url, String.class); log.info("GET_USER_DETAIL:"); log.info(response); WxCpUserDetail response2 = JSON.parseObject(response, WxCpUserDetail.class); return response2; } catch (Exception e) { log.error(e.getMessage(), e); return null; } } }
解密代码,官网可下载:
/** * 对企业微信发送给企业后台的消息加解密示例代码. * * @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 com.artefact.common.utils; 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 lombok.extern.log4j.Log4j2; 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> */ @Log4j2 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)) { log.error("============= from_receiveid:" + from_receiveid + " receiveid:" + 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); } // 解密 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; } }
============ 欢迎各位老板打赏~ ===========
与本文相关的文章
- · vue3 html2canvas导出透明png图片
- · 成都共享办公室推荐
- · 群晖web station设置wordpress 伪静态
- · 2024年,写字楼市场面临新挑战与机遇
- · 共享办公联合办公室的优缺点
- · 2024最新成都共享办公室排名
- · 微信封装wx.request
- · git本地分支关联远程分支
- · Stable Diffusion(AI绘画) 绘世 WebUI/ComfyUI整合包 – 附常用的大模型和ControlNet 模型
- · nginx自动切换到手机/PC 网站
- · 本地部署ollama+qwen2-7b+open-webui
- · 黑群晖docker无法pull镜像,x509错误解决方法