一 【企业微信急速救心丸】第三方应用开发 - Java整合企业微信回调

一、首先要了解开发场景,第三方应用开发、企业内部开发、智慧硬件开发的区别 。企业微信对应有三个开发文档,要注意三个开发文档虽然说部分接口是通用的,但是其接口获取的内容、调用的本质却大有不同,我建议先把企业微信开发者前言部分的细读,搞明白了三者的概念 。此处我均已第三方应用开发为准(申请部分的内容网上教程一大堆,大家跟着步骤走等审批就行)
二、服务商后台 - 应用管理 - 小程序 - 配置回调
虽然说企业微信需要配置的url很多,但是最主要的还是回调接口,这也是接入企业微信的第一步 。这个回调接口,每10分钟会接收到企业微信发出的请求,刷新当前suiteTicket,然后我们需要在GET请求中校验企业微信的参数,然后在post接口中对接收到的参数进行解密,并且需要将suiteTicket转换为suiteAccessToken,作为请求企业微信接口的第一个重要凭证,GET、POST回调的Java代码示范:
GET回调接口(企业微信工具类代码自行下载官方demo即可) @GetMapping("/callback")@ResponseBodypublic void callback(@RequestParam(name = "msg_signature") String signature, String timestamp, String nonce,String echostr, final HttpServletResponse response) { log.info("get验签请求参数 msg_signature = {}, timestamp = {}, nonce = {} , echostr = {}", signature, timestamp, nonce, echostr); wxMpservice.getCallback(signature, timestamp, nonce, echostr, response);}@Overridepublic void getCallback(String signature, String timestamp, String nonce, String echostr, HttpServletResponse response) { WXBizMsgCrypt wxcpt = null; String sEchoStr = null; try {wxcpt = new WXBizMsgCrypt(token, encodingAESKey, corpid);sEchoStr = wxcpt.VerifyURL(signature, timestamp, nonce, echostr); } catch (AesException e) {e.printStackTrace(); } log.info("weixin-callback:get请求回调签名校验通过, result = " + sEchoStr); PrintWriter out = null; try {out = response.getWriter();//必须要返回解密之后的明文if (GeneralUtil.isNotNullAndEmpty(sEchoStr)) {log.info("验证成功!");} else {log.error("URL验证失败");} } catch (Exception e) {e.printStackTrace(); } out.write(sEchoStr); out.flush();} POST接口(注意此处需要对suiteTicket做缓存处理,并且需要用suiteTicket转为suiteAccessToken 。此外!倘若有事件回调,必须要区分微信回调和第三方服务商的回调,否则POST获取到的解密内容是不同的!/** * 企业微信客户联系回调. * * @param requestrequest * @param sMsgSignature 签名 * @param sTimestamp时间戳 * @param sNonce随机值 * @return success */@ResponseBody@PostMapping(value = "https://tazarkount.com/callback")public String callback(final HttpServletRequest request,@RequestParam(name = "msg_signature") final String sMsgSignature,@RequestParam(name = "timestamp") final String sTimestamp,@RequestParam(name = "nonce") final String sNonce) { log.info("post验签请求参数 msg_signature = {}, timestamp = {}, nonce {}", sMsgSignature, sTimestamp, sNonce); wxMpservice.postCallback(request, sMsgSignature, sTimestamp, sNonce); return "success";}@Overridepublic void postCallback(HttpServletRequest request, String sMsgSignature, String sTimestamp, String sNonce) { WXBizMsgCrypt wxcpt = null; try {InputStream inputStream = request.getInputStream();String postData = https://tazarkount.com/read/CharStreams.toString(new InputStreamReader(inputStream, Charsets.UTF_8));wxcpt = new WXBizMsgCrypt(token, encodingAESKey, corpid);// 解密String sMsg = wxcpt.DecryptMsg(sMsgSignature, sTimestamp, sNonce, postData);// 将post数据转换为mapMap dataMap = MessageUtil.parseXml(sMsg);log.warn("回调map = {}", dataMap);// dataMap需要判断, 如果是事件回调, 则无需设置tokenif (Objects.nonNull(dataMap.get("SuiteTicket")) && Objects.nonNull(dataMap.get("SuiteId"))) {log.info("weixin-callback:post请求回调成功, dataMap = " + dataMap);// suite_ticket每10分钟刷新一次, 每个suite_ticket有效期为30分钟, redis会根据key直接覆盖旧值redisTemplate.opsForValue().set(RedisKeyConstants.WX_POST_CALLBACK_SUITE_TICKET, dataMap, 20L, TimeUnit.MINUTES);} else {//企业内成员idString openUserId = dataMap.get("FromUserName");// 事件需要 template_card_eventString event = dataMap.get("Event");// EventKey id需要对应上button_key_1 button_key_2 目前设置两个按钮一个为通过 一个为不通过String eventKey = dataMap.get("EventKey");String responseCode = dataMap.get("ResponseCode");String taskId = dataMap.get("TaskId");if (GeneralUtil.isNotNullAndEmpty(responseCode) && GeneralUtil.isNotNullAndEmpty(event)) {// 更新模板按钮log.info("操作人ID:{} , 发起事件:{} , 触发按钮:{}", openUserId, event, eventKey);updateTemplateBtnMsg(responseCode);// 接受事件回调, 处理业务逻辑switch (eventKey) {// 通过case "accept_user_key":govtGroupMemberService.approveGroupEntry(taskId, Boolean.TRUE);log.info("通过通过 event = {},taskId = {}", event, taskId);break;// 拒绝case "reject_user_key":govtGroupMemberService.approveGroupEntry(taskId, Boolean.TRUE);log.info("拒绝拒绝 event = {},taskId = {}", event, taskId);break;default:break;}}} } catch (Exception e) {e.printStackTrace(); }} 代码开发完成,需要将其请求路径配置到企业微信服务商的回调url中,配置完点击确定的时候,企业微信会立马发出两条请求消息校验请求是否成功(此外,记得配置企业微信的ip白名单)
再点击刷新ticket,再次校验是否配置成功,请求POST接口

注意:我这里特意打印了from_receiveid 和 receiveid,这是为什么?因为仔细的同学会发现,当我们获取企业微信回调的时候,我们post请求解密的时候用的是suiteid,但如果是事件回调的时候,此时post请求解密用的是corpid 。这里我踩了不少坑,官方给的DEMO十分之灵异,这里竟然没有特意注释和说明
// 解密是指POST-CALLBACK的这一步wxcpt = new WXBizMsgCrypt(token, encodingAESKey, corpid);/** * 备注:在企业内部的工具类中有!from_corpid.equals(corpId)的校验 * 但是在第三方应用的时候,由于postDate解密得到的是安装该应用的fromCorpID,所以不能进行比较 */// receiveid不相同的情况,为了解决corpid权限不足的报错,可以直接注释掉这段代码// if (!from_receiveid.equals(receiveid)) {//throw new AesException(AesException.ValidateCorpidError);// } 三、第三方应用授权流程开发 重新回到开始,不要觉得配置回调就OK了,然后立马去开发对接文档 。我们需要了解一下,第三方应用它的角色定义,是以第三方服务商的身份,将小程序应用关联到其它企业,所以企业微信早在这一步就在开发者API上的文档做了手脚,对授权操作交给了开发者进行处理 。因此,第三方服务商必须要提供一个授权的入口,并且授权成功后对回调内容进行处理 。建议开发者在授权成功后,拿到回调内容进行ORM处理,因为第三方应用调用接口API的时候需要带上accessToken,并且是要通过no corpsecret的方式获取的,所以要用get_corp_token的方式获取企业永久凭证
get_corp_token的企业微信官方文档
此处授权,其实主要是两个接口,一个是创建授权链接的接口,一个是请求授权连接成功授权后的回调接口(注意,回调接口需要到服务商后台配置回调域名 and 接口地址)
此处是获取授权地址接口,因为大部分的suiteAccessToken、preAuthCode、代码官网都有,大家大概看个流程就行,最终需要接口返回一个拼凑的url 。然后url提供给企业管理员扫码,确定后就能完成授权操作了 。
/** * 获取第三方应用授权链接接口 */@ResponseBody@GetMapping(value = "https://tazarkount.com/install")public String install() { SuiteAccessToken suiteAccessToken = tokenConfig.getSuiteAccessToken(); return tokenConfig.install(tokenConfig.getPreAuthCode(suiteAccessToken.getSuiteAccessToken()));}/** * 获取预授权码 * * @param suiteAccessToken * @return */public PreAuthCode getPreAuthCode(String suiteAccessToken) { // 判断缓存是否存在, 存在则无须重复请求 String cacheKey = (String) redisTemplate.opsForValue().get(RedisKeyConstants.WX_PRE_AUTH_CODE); if (Objects.nonNull(cacheKey)) {PreAuthCode preAuthCode = JSONObject.parseObject(cacheKey, PreAuthCode.class);// 设置授权setSessionInfo(preAuthCode.getPreAuthCode(), suiteAccessToken);return preAuthCode; } String result = null; try {result = HttpUtil.get(BaseUrlConstants.GET_PRE_AUTH_CODE.replace("SUITE_ACCESS_TOKEN", suiteAccessToken), null);log.info("getPreAuthCode result = " + result); } catch (Exception e) {e.printStackTrace(); } // 请求返回 if (Objects.nonNull(result)) {// 异常code抓取JSONObject jsonObject = JSONObject.parseObject(result);if (Objects.nonNull(jsonObject.getString("errcode")) && (!"0".equals(jsonObject.getString("errcode")))) {throw new BusinessException("getAppletAccessToken error,cause by:" + jsonObject.getString("errmsg:") + result);}// 解析PreAuthCode preAuthCode = JSONObject.parseObject(result, PreAuthCode.class);// 授权setSessionInfo(preAuthCode.getPreAuthCode(), suiteAccessToken);// 缓存applet_access_token, 7200s过期redisTemplate.opsForValue().set(RedisKeyConstants.WX_PRE_AUTH_CODE, JSONObject.toJSONString(preAuthCode),3600L, TimeUnit.SECONDS);return preAuthCode; } else {throw new BusinessException("preAuthCode获取失败!请重新授权!"); }}/** * 组装拼接URL */public String install(PreAuthCode preAuthCode) { // 拼凑url String result = BaseUrlConstants.INSTALL_URL.replace("SUITE_ID", suiteId).replace("PRE_AUTH_CODE", preAuthCode.getPreAuthCode()).replace("REDIRECT_URI", "https://~~~服务商后台配置的授权回调地址 ~~/wx/mp/installCallback").replace("STATE", GeneralUtil.generateShortUuid()); return result;}
授权回调
【一 【企业微信急速救心丸】第三方应用开发 - Java整合企业微信回调】/** * 第三方应用授权回调 * * @param authCode * @return */@ResponseBody@GetMapping(value = "https://tazarkount.com/installCallback")public String installCallback(@RequestParam(name = "auth_code") String authCode) { // 接收到请求参数进行回调, 初始化企业信息 SuiteAccessToken suiteAccessToken = tokenConfig.getSuiteAccessToken(); tokenConfig.installCallback(authCode, suiteAccessToken.getSuiteAccessToken()); return "授权成功!请关闭当前页!";}/** * 临时authCode置换永久code, 并且初始化企业信息 * * @param authCode * @param suiteAccessToken */public void installCallback(String authCode, String suiteAccessToken) { // 获取企业永久凭证初始化到ORM JSONObject requestObj = new JSONObject(); requestObj.put("auth_code", authCode); String result = null; try {result = HttpUtil.post(BaseUrlConstants.GET_PERMANENT_CODE_URL.replace("SUITE_ACCESS_TOKEN", suiteAccessToken), requestObj.toString());// 一次性请求, 授权成功初始化到数据库JSONObject jsonObject = JSONObject.parseObject(result);String permanentCode = jsonObject.getString("permanent_code");String authCorpInfo = jsonObject.getString("auth_corp_info");JSONObject authCorpObject = JSONObject.parseObject(authCorpInfo);String corpName = authCorpObject.getString("corp_name");String corpFullName = authCorpObject.getString("corp_full_name");// 获取agentId, 授权的应用idJSONObject childObject= (JSONObject) jsonObject.get("auth_info");JSONArray jsonArray = JSONObject.parseArray(JSONObject.toJSONString(childObject.get("agent")));JSONObject arrayObject = (JSONObject) jsonArray.get(0);Long agentId = arrayObject.getLong("agentid");GovtInstallDO govtInstallDO = new GovtInstallDO();govtInstallDO.setAgentId(agentId);govtInstallDO.setCorpName(corpName);govtInstallDO.setCorpFullName(corpFullName);govtInstallDO.setCreateTime(new Date());govtInstallDO.setPermanentCode(permanentCode);govtInstallMapper.insert(govtInstallDO);log.info("installCallback result = " + result); } catch (Exception e) {throw new BusinessException(StatusCodeEnum.INSTALL_CORP_ERROR); }}