企业微信对应有三个开发文档,要注意三个开发文档虽然说部分接口是通用的,但是其接口获取的内容、调用的本质却大有不同,我建议先把企业微信开发者前言部分的细读,搞明白了三者的概念。此处我均已第三方应用开发为准(申请部分的内容网上教程一大堆,大家跟着步骤走等审批就行)
虽然说企业微信需要配置的url很多,但是最主要的还是回调接口,这也是接入企业微信的第一步。这个回调接口,每10分钟会接收到企业微信发出的请求,刷新当前suiteTicket,然后我们需要在GET请求中校验企业微信的参数,然后在post接口中对接收到的参数进行解密,并且需要将suiteTicket转换为suiteAccessToken,作为请求企业微信接口的第一个重要凭证,GET、POST回调的Java代码示范:
@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();
}
/**
* 企业微信客户联系回调.
*
* @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();
}
}
注意:我这里特意打印了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的方式获取企业永久凭证
此处是获取授权地址接口,因为大部分的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);
}
}