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

一、首先要了解开发场景,第三方应用开发、企业内部开发、智慧硬件开发的区别。

企业微信对应有三个开发文档,要注意三个开发文档虽然说部分接口是通用的,但是其接口获取的内容、调用的本质却大有不同,我建议先把企业微信开发者前言部分的细读,搞明白了三者的概念。此处我均已第三方应用开发为准(申请部分的内容网上教程一大堆,大家跟着步骤走等审批就行)

二、服务商后台 - 应用管理 - 小程序 - 配置回调

在这里插入图片描述
虽然说企业微信需要配置的url很多,但是最主要的还是回调接口,这也是接入企业微信的第一步。这个回调接口,每10分钟会接收到企业微信发出的请求,刷新当前suiteTicket,然后我们需要在GET请求中校验企业微信的参数,然后在post接口中对接收到的参数进行解密,并且需要将suiteTicket转换为suiteAccessToken,作为请求企业微信接口的第一个重要凭证,GET、POST回调的Java代码示范:

GET回调接口(企业微信工具类代码自行下载官方demo即可)
@GetMapping("/callback")
@ResponseBody
public 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);
}

@Override
public 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 request       request
 * @param sMsgSignature 签名
 * @param sTimestamp    时间戳
 * @param sNonce        随机值
 * @return success
 */
@ResponseBody
@PostMapping(value = "/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";
}

@Override
public void postCallback(HttpServletRequest request, String sMsgSignature, String sTimestamp, String sNonce) {
	WXBizMsgCrypt wxcpt = null;
	try {
		InputStream inputStream = request.getInputStream();
		String postData = CharStreams.toString(new InputStreamReader(inputStream, Charsets.UTF_8));
		wxcpt = new WXBizMsgCrypt(token, encodingAESKey, corpid);
		// 解密
		String sMsg = wxcpt.DecryptMsg(sMsgSignature, sTimestamp, sNonce, postData);
		// 将post数据转换为map
		Map<String, String> dataMap = MessageUtil.parseXml(sMsg);
		log.warn("回调map = {}", dataMap);
		// dataMap需要判断, 如果是事件回调, 则无需设置token
		if (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 {
			//企业内成员id
			String openUserId = dataMap.get("FromUserName");
			// 事件需要 template_card_event
			String 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 = "/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;
}

在这里插入图片描述

授权回调

/**
 * 第三方应用授权回调
 *
 * @param authCode
 * @return
 */
@ResponseBody
@GetMapping(value = "/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, 授权的应用id
		JSONObject 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);
	}

}
  • 8
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值