企业微信服务商集成解决方案
概述
详细
一、前言
先贴一张企业微信接入场景图,解释下这篇文章的使用场景。
如图所示,本方案是第三方应用开发的demo实现,因为企业内部开发比较简单,不在该篇文章中做过多介绍。
二、准备阶段
1、申请企业微信号
2、点击banner栏处的服务商平台,开通服务商资格
3、如果涉及到认证,请分别认证企业微信号及服务商开发资格
三、安装介绍
1、应用集成效果图如下,我们要做的就是在第三方这块,安装已上线的应用,当然,这里点击添加第三方应用,只能安装那些已经上架的应用(注意,上线成功不代表上架,企业微信会有一定的安装要求,只有达到了标准的应用才有资格上架,且被客户搜索到,具体可以查看下企业微信的文档),我们的demo中就包括使用连接的形式,让客户安装应用,别急,我们接着往下看
2、随便点开一个第三方应用,我这里打开的是“随心”,你会看到这样的图,注意下我标注出来的①②③,我们会在后面介绍这三个第三方分别是在哪里设置生成的
四、服务商应用介绍
1、进入服务商平台 > 网页应用 > 创建应用
这里如果不是特殊要求,这里按照图片创建即可,敏感信息处慎重选择,否则会影响后面的审核进度
2、下一步
下一步的信息填写,是本文的重点,涉及到应用必须正常运行,所以可以在此处按照格式随便填写,后面再修改(目的是先生成应用的SuiteID和Secret,开发时会使用到)
到此的介绍基本结束。接下来进入代码阶段
五、代码介绍
我大概画了下服务架构图,我们要实现的就是图中红色框部分
重点说明:该方案可实现单应用授权多家,多应用授权区分且经实践可行,并已正式投入生产环境,但因为授权服务器与应用服务器分离,而应用又是前后端分离的项目设计,必然会涉及到跨项目跳转及跨域问题,前期可能会因为配置导致项目启动失败或授权异常,具体问题不在此文档中过多描述,如果有此方面的疑问,可以给我留言
1、代码结构如下图
设计到2张表,数据结构:
-- ---------------------------- -- Table structure for s_qywx_application -- ---------------------------- DROP TABLE IF EXISTS `s_qywx_application`; CREATE TABLE `s_qywx_application` ( `id` varchar(32) NOT NULL, `suite_id` varchar(100) DEFAULT NULL COMMENT '第三方应用的suiteId', `suite_secret` varchar(100) DEFAULT NULL COMMENT '第三方应用对应的suiteSecret', `suite_ticket` varchar(600) DEFAULT NULL COMMENT 'ticket内容,最长为512字节', `ticket_time` timestamp NULL DEFAULT NULL COMMENT 'ticket推送时间', `suite_access_token` varchar(600) DEFAULT NULL COMMENT '第三方应用access_token,最长为512字节', `token_expires_time` timestamp NULL DEFAULT NULL COMMENT 'access_token过期时间,有效期2小时', `pre_auth_code` varchar(600) DEFAULT NULL COMMENT '预授权码,最长为512字节', `code_expires_time` timestamp NULL DEFAULT NULL COMMENT '预授权码过期时间', `create_date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='企业微信第三方应用信息'; -- ---------------------------- -- Table structure for s_qywx_application_authorizer -- ---------------------------- DROP TABLE IF EXISTS `s_qywx_application_authorizer`; CREATE TABLE `s_qywx_application_authorizer` ( `id` varchar(32) NOT NULL, `permanent_code` varchar(600) DEFAULT NULL COMMENT '企业微信永久授权码,最长为512字节', `access_token` varchar(600) DEFAULT NULL COMMENT '授权方(企业)access_token,最长为512字节', `access_token_expires_time` timestamp NULL DEFAULT NULL COMMENT 'access_token过期时间', `corp_jsapi_tick` varchar(600) DEFAULT NULL COMMENT 'jssdk生成签名所需的jsapi_ticket(企业)', `jsapi_ticket` varchar(600) DEFAULT NULL COMMENT 'jssdk生成签名所需的jsapi_ticket\r\n(应用) 刷新规则同access_token', `js_expires_time` timestamp NULL DEFAULT NULL COMMENT 'access_token过期时间', `suite_id` varchar(32) DEFAULT NULL COMMENT '第三方应用的suiteId', `auth_corpid` varchar(100) DEFAULT NULL COMMENT '授权方企业微信id', `auth_corp_name` varchar(300) DEFAULT NULL COMMENT '授权方企业名称', `auth_corp_type` varchar(300) DEFAULT NULL COMMENT '授权方企业类型,认证号:verified, 注册号:unverified', `auth_corp_square_logo_url` varchar(300) DEFAULT NULL COMMENT '授权方企业方形头像', `auth_corp_user_max` int(11) DEFAULT NULL COMMENT '授权方企业用户规模', `auth_corp_full_name` varchar(300) DEFAULT NULL COMMENT '授权方企业的主体名称(仅认证过的企业有)', `auth_subject_type` int(11) DEFAULT NULL COMMENT '企业类型,1. 企业; 2. 政府以及事业单位; 3. 其他组织, 4.团队号', `auth_verified_end_time` timestamp NULL DEFAULT NULL COMMENT '认证到期时间', `auth_corp_wxqrcode` varchar(300) DEFAULT NULL COMMENT '授权企业在微工作台(原企业号)的二维码,可用于关注微工作台', `auth_corp_scale` varchar(300) DEFAULT NULL COMMENT '企业规模。当企业未设置该属性时,值为空', `auth_corp_industry` varchar(300) DEFAULT NULL COMMENT '企业所属行业。当企业未设置该属性时,值为空', `auth_corp_sub_industry` varchar(300) DEFAULT NULL COMMENT '企业所属子行业。当企业未设置该属性时,值为空', `auth_location` varchar(300) DEFAULT NULL COMMENT '企业所在地信息, 为空时表示未知', `auth_info` varchar(1000) DEFAULT NULL COMMENT '授权信息', `auth_user_info` varchar(1000) DEFAULT NULL COMMENT '授权管理员的信息', `dealer_corp_info` varchar(1000) DEFAULT NULL COMMENT '代理服务商企业信息', `create_date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='企业微信第三方应用开发接入授权企业信息';
1.1 SuiteCallback:推送更新token,保证第三方应用的token(suite_access_token)不过期,同步更新预授权码(pre_auth_code),如果是测试安装,在该类中会实现测试权企业分配永久授权码的逻辑
1.2 ProviderAuthorize: 从服务商网站发起授权,安装授权应用,会根据上一步中的预授权码,为授权企业分配永久授权码
1.3 MessagePush:企业微信的消息推送实现
1.4 OAuth2Authorize:用户身份授权,包含我们上面在第二.2中提到的前往服务商后台实现
1.5 JSSDK 分享逻辑的实现
SuiteCallback:
package com.ffxl.hi.controller.qywx;import io.swagger.annotations.Api;import java.io.BufferedReader;import java.io.IOException;import java.util.Date;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import org.dom4j.Document;import org.dom4j.DocumentException;import org.dom4j.DocumentHelper;import org.dom4j.Element;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;import com.ffxl.cloud.model.SQywxApplication;import com.ffxl.cloud.service.SQywxApplicationAuthorizerService;import com.ffxl.cloud.service.SQywxApplicationService;import com.ffxl.hi.controller.BaseUtil;import com.ffxl.hi.controller.qywx.util.EnterpriseAPI;import com.ffxl.hi.controller.qywx.util.EnterpriseConst;import com.ffxl.hi.controller.qywx.util.EnumType;import com.ffxl.hi.controller.qywx.util.aes.AesException;import com.ffxl.hi.controller.qywx.util.aes.WXBizMsgCrypt;import com.ffxl.platform.qywx.model.ApiSuiteTokenRequest;import com.ffxl.platform.util.DateUtil;import com.ffxl.platform.util.StringUtil;/** * 推送更新token * * @author wison */@Controller@RequestMapping(value = "/qy/suite")@Api(value = "/qy/suite")public class SuiteCallback extends BaseUtil { private static final Logger logger = LoggerFactory.getLogger(SuiteCallback.class); @Autowired private SQywxApplicationService sqywxApplicationService; @Autowired private SQywxApplicationAuthorizerService sqywxApplicationAuthorizerService; /** * 指令接收 企业微信服务器会定时(每十分钟)推送ticket。ticket会实时变更,并用于后续接口的调用。 * * @param request * @param response * @throws IOException * @throws AesException * @throws DocumentException */ @RequestMapping(value = "/directive/receive") public void acceptAuthorizeEvent(String suiteid, HttpServletRequest request, HttpServletResponse response) throws IOException, AesException, DocumentException { logger.debug("企业微信服务器开始推送suite_ticket---------10分钟一次-----------"); logger.debug("获取到的第三方应用suiteid:" + suiteid); processAuthorizeEvent(suiteid, request, response); } /** * 服务商处理指令回调,解析suite_ticket数据 * * @param request * @throws IOException * @throws AesException * @throws DocumentException */ public void processAuthorizeEvent(String suitedid, HttpServletRequest request, HttpServletResponse response) throws IOException, DocumentException, AesException { // 第三方事件回调 WXBizMsgCrypt wxcpt = new WXBizMsgCrypt(EnterpriseConst.STOKEN, EnterpriseConst.SENCODINGAESKEY, suitedid); // 解析出url上的参数值如下: String nonce = request.getParameter("nonce"); String timestamp = request.getParameter("timestamp"); String msgSignature = request.getParameter("msg_signature"); String echostr = request.getParameter("echostr"); logger.debug("-----------------------suitedid:" + suitedid); logger.debug("-----------------------nonce:" + nonce); logger.debug("-----------------------timestamp:" + timestamp); logger.debug("-----------------------msg_signature:" + msgSignature); // 签名串 logger.debug("-----------------------echostr:" + echostr);// 随机串 String sEchoStr; // url验证时需要返回的明文 if (StringUtil.isEmpty(msgSignature)) return; // 回调 if (StringUtil.isEmpty(echostr)) { StringBuilder sb = new StringBuilder(); BufferedReader in = request.getReader(); String line; while ((line = in.readLine()) != null) { sb.append(line); } String xml = sb.toString(); logger.debug("-----------------------服务商接收到的原始 Xml=" + xml); String exml = wxcpt.DecryptMsg(msgSignature, timestamp, nonce, xml); logger.debug("-----------------------服务商接收到的xml解密后:" + exml); processAuthorizationEvent(request, exml); logger.debug("-----------------------解析成功,返回success"); output(response, "success"); // 输出响应的内容。 } else { // 校验,此处的receiveid为企业的corpid WXBizMsgCrypt wxcorp = new WXBizMsgCrypt(EnterpriseConst.STOKEN, EnterpriseConst.SENCODINGAESKEY, EnterpriseConst.SCORPID); sEchoStr = wxcorp.VerifyURL(msgSignature, timestamp, nonce, echostr); logger.debug("-----------------------URL验证成功,返回解析后的的echostr:" + sEchoStr); output(response, sEchoStr); // 输出响应的内容。 } } /** * 对解密后的xml信息进行处理 * * @param xml */ void processAuthorizationEvent(HttpServletRequest request, String echoXml) { Document doc; try { doc = DocumentHelper.parseText(echoXml); Element rootElt = doc.getRootElement(); // 消息类型 String infoType = rootElt.elementText("InfoType"); String suiteId = rootElt.elementText("SuiteId");// 第三方应用的SuiteId switch (EnumType.InfoType.valueOf(infoType)) { // 授权成功,从企业微信应用市场发起授权时,企业微信后台会推送授权成功通知。 // 从第三方服务商网站发起的应用授权流程,由于授权完成时会跳转第三方服务商管理后台,因此不会通过此接口向第三方服务商推送授权成功通知。 case create_auth: String authCode = rootElt.elementText("AuthCode");// 授权的auth_code,最长为512字节。用于获取企业的永久授权码。5分钟内有效 logger.debug("》》》》》》》》》》授权码AuthCode:" + authCode); // 换取企业永久授权码 EnterpriseAPI.getPermanentCodeAndAccessToken(suiteId, authCode); break; // 变更授权,服务商接收到变更通知之后,需自行调用获取企业授权信息进行授权内容变更比对。 case change_auth: String changeAuthCorpid = rootElt.elementText("AuthCorpId");// 授权方的corpid // 获取并更新本地授权的企业信息 EnterpriseAPI.getAuthInfo(suiteId, changeAuthCorpid); break; // 取消授权,当授权方(即授权企业)在企业微信管理端的授权管理中,取消了对应用的授权托管后,企业微信后台会推送取消授权通知。 case cancel_auth: // TODO 删除企业授权信息 String cancelAuthCorpid = rootElt.elementText("AuthCorpId");// 授权方的corpid sqywxApplicationAuthorizerService.deleteBySuiteAndCorpId(suiteId, cancelAuthCorpid); break; // 企业微信服务器会定时(每十分钟)推送ticket。ticket会实时变更,并用于后续接口的调用。 case suite_ticket: String suiteTicket = rootElt.elementText("SuiteTicket"); String timeStamp = rootElt.elementText("TimeStamp"); // 时间戳-秒 logger.debug("》》》》》》》》》》》》》》》》》》》》》》》》TimeStamp时间戳=========================" + timeStamp); // 存储ticket logger.debug("推送SuiteTicket协议-----------suiteTicket = " + suiteTicket); EnterpriseConst suiteConst = new EnterpriseConst("suite"); suiteConst.setKey(suiteId); String suiteSecret = suiteConst.getValue(); SQywxApplication entity = sqywxApplicationService.getQYWeixinApplication(suiteId, suiteSecret); entity.setSuiteTicket(suiteTicket); logger.debug("》》》》》》》》》》》》》》》》》》》》》》》》TimeStamp时间戳(毫秒)=========================" + Long.parseLong(timeStamp) * 1000); Date date = new Date(Long.parseLong(timeStamp) * 1000); logger.debug("》》》》》》》》》》》》》》》》》》》》》》》》TimeStamp时间戳=========================" + DateUtil.formatStandardDatetime(date)); Date ticketDate = DateUtil.parseDate(DateUtil.formatStandardDatetime(date)); entity.setTicketTime(ticketDate); int ret = sqywxApplicationService.updateByPrimaryKeySelective(entity); // 获取第三方应用凭证,有效期2小时 ApiSuiteTokenRequest apiSuiteToken = new ApiSuiteTokenRequest(); apiSuiteToken.setSuite_id(suiteId); apiSuiteToken.setSuite_secret(suiteSecret); // 授权事件接收会每隔10分钟检验一下ticket的有效性,从而保证了此处的ticket是长期有效的 apiSuiteToken.setSuite_ticket(suiteTicket); // 验证token有效性(2小时) String suiteAccessToken = EnterpriseAPI.getSuiteAccessToken(apiSuiteToken); // 验证预授权码有效性(10分钟) EnterpriseAPI.getPreAuthCode(suiteId, suiteAccessToken); break; // 变更通知,根据ChangeType区分消息类型 case change_contact: // TODO 更新令牌等 break; default: break; } } catch (DocumentException e) { e.printStackTrace(); } } }
ProviderAuthorize:
package com.ffxl.hi.controller.qywx;import io.swagger.annotations.Api;import java.io.IOException;import java.net.URLEncoder;import java.util.HashMap;import java.util.Map;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import org.dom4j.DocumentException;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.ResponseBody;import com.alibaba.fastjson.JSONObject;import com.ffxl.cloud.model.SQywxApplication;import com.ffxl.cloud.service.SQywxApplicationService;import com.ffxl.hi.controller.qywx.util.EnterpriseAPI;import com.ffxl.hi.controller.qywx.util.aes.AesException;import com.ffxl.platform.util.JsonResult;import com.ffxl.platform.util.Message;import com.ffxl.platform.util.StringUtil;/** * 企业授权应用 方式一:从服务商网站发起 * * @author wison */@Controller@RequestMapping(value = "/qy/auth")@Api(value = "/qy/auth")public class ProviderAuthorize { @Autowired private SQywxApplicationService sqywxApplicationService; // 授权页网址 public static final String INSTALL_URL = "https://open.work.weixin.qq.com/3rdapp/install?suite_id="; /** * 一键授权功能,主动引入用户进入授权页后,通过用户点击调用此方法 * * @param request * @param suiteId * 应用id * @throws IOException * @throws AesException * @throws DocumentException */ @RequestMapping(value = "/goAuthor") @ResponseBody public JsonResult goAuthor(HttpServletRequest request, String suiteId) throws IOException, AesException, DocumentException { if (StringUtil.isEmpty(suiteId)) { return new JsonResult(Message.M4003); } String baseUrl = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + request.getContextPath(); String redirectUri = baseUrl + "/qy/auth/authorCallback"; String redirectUriEncode = URLEncoder.encode(redirectUri, "UTF-8"); // Map<String,String> stateMap = new HashMap<String, String>(); // stateMap.put("suiteId", suiteId); // 查询第三方应用,获取预授权码 SQywxApplication application = sqywxApplicationService.queryBySuiteId(suiteId); if (application == null || StringUtil.isEmpty(application.getPreAuthCode())) { return new JsonResult(Message.M3001, "suiteId:" + suiteId + "对应的第三方应用尚未初始化,请等待10分钟或联系服务商", null); } // 获取预授权码,有效期10分钟 String preAuthCode = application.getPreAuthCode(); String url = INSTALL_URL + suiteId + "&pre_auth_code=" + preAuthCode + "&redirect_uri=" + redirectUriEncode + "&state=" + suiteId; return new JsonResult(Message.M2000, url); } /** * 引导授权回调 根据临时授权码(10分钟有效),换取永久授权码 * * @param request * @param response * @throws IOException * @throws AesException * @throws DocumentException */ @RequestMapping(value = "/authorCallback") public void authorCallback(HttpServletRequest request, HttpServletResponse response) throws IOException, AesException, DocumentException { String authCode = request.getParameter("auth_code"); String expires_in = request.getParameter("expires_in"); String state = request.getParameter("state"); // //解析state // JSONObject js = JSONObject.parseObject(state); // String suiteId = js.getString("suiteId"); String suiteId = state; // 换取永久授权码 EnterpriseAPI.getPermanentCodeAndAccessToken(suiteId, authCode); } }
MessagePush:
package com.ffxl.hi.controller.qywx;import io.swagger.annotations.Api;import java.io.BufferedReader;import java.io.IOException;import java.util.Calendar;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import org.dom4j.Document;import org.dom4j.DocumentException;import org.dom4j.DocumentHelper;import org.dom4j.Element;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.PathVariable;import org.springframework.web.bind.annotation.RequestMapping;import com.ffxl.hi.controller.BaseUtil;import com.ffxl.hi.controller.qywx.util.EnterpriseConst;import com.ffxl.hi.controller.qywx.util.aes.AesException;import com.ffxl.hi.controller.qywx.util.aes.WXBizMsgCrypt;import com.ffxl.platform.util.StringUtil;/** * 企业微信开放了消息发送接口,企业可以使用这些接口让自定义应用与企业微信后台或用户间进行双向通信。 * * @author wison */@Controller@RequestMapping(value = "/qy/message")@Api(value = "/qy/message")public class MessagePush extends BaseUtil { private static final Logger logger = LoggerFactory.getLogger(MessagePush.class); /** * 授权公众号的回调地址 处理消息等用户操作时,请务必使用appid进行匹配 * * @param appid * @param request * @param response * @throws IOException * @throws AesException * @throws DocumentException */ @RequestMapping(value = "{corpid}/callback") public void acceptMessageAndEvent(@PathVariable String corpid, HttpServletRequest request, HttpServletResponse response) throws IOException, DocumentException, AesException { String nonce = request.getParameter("nonce"); String timestamp = request.getParameter("timestamp"); String msgSignature = request.getParameter("msg_signature"); String echostr = request.getParameter("echostr"); logger.info("-----------------------corpid:" + corpid); logger.info("-----------------------nonce:" + nonce); logger.info("-----------------------timestamp:" + timestamp); logger.info("-----------------------msg_signature:" + msgSignature); // 签名串 logger.info("-----------------------echostr:" + echostr);// 随机串 String sEchoStr; // url验证时需要返回的明文 if (StringUtil.isEmpty(msgSignature)) return;// 微信推送给第三方开放平台的消息一定是加过密的,无消息加密无法解密消息 if (StringUtil.isEmpty(echostr)) { // 消息处理 StringBuilder sb = new StringBuilder(); BufferedReader in = request.getReader(); String line; while ((line = in.readLine()) != null) { sb.append(line); } in.close(); String xml = sb.toString(); logger.info("接收到的xml信息----------" + xml); checkWeixinAllNetworkCheck(request, response, corpid, xml); } else { // 校验,此处的receiveid为企业的corpid WXBizMsgCrypt wxcorp = new WXBizMsgCrypt(EnterpriseConst.STOKEN, EnterpriseConst.SENCODINGAESKEY, corpid); sEchoStr = wxcorp.VerifyURL(msgSignature, timestamp, nonce, echostr); logger.info("-----------------------URL验证成功,返回解析后的的echostr:" + sEchoStr); output(response, sEchoStr); // 输出响应的内容。 } } /** * 解密消息 * * @param appid * @param request * @param response * @param xml * @throws DocumentException * @throws IOException * @throws AesException */ public void checkWeixinAllNetworkCheck(HttpServletRequest request, HttpServletResponse response, String corpid, String xml) throws DocumentException, IOException { Document doc = DocumentHelper.parseText(xml); Element rootElt = doc.getRootElement(); String toUserName = rootElt.elementText("ToUserName"); // 企业微信的CorpID,当为第三方套件回调事件时,CorpID的内容为suiteid String agentID = rootElt.elementText("AgentID"); // 接收的应用id,可在应用的设置页面获取 String encrypt = rootElt.elementText("Encrypt"); // 消息结构体加密后的字符串 try { WXBizMsgCrypt wxcpt = new WXBizMsgCrypt(EnterpriseConst.STOKEN, EnterpriseConst.SENCODINGAESKEY, corpid); // 解析出url上的参数值如下: String sVerifyNonce = request.getParameter("nonce"); String sVerifyTimeStamp = request.getParameter("timestamp"); String sVerifyMsgSig = request.getParameter("msg_signature"); // 检验消息的真实性,并且获取解密后的明文. String sEncryptXml = wxcpt.DecryptMsg(sVerifyMsgSig, sVerifyTimeStamp, sVerifyNonce, xml); parsingMsg(request, response, sEncryptXml); } catch (Exception e) { // 验证URL失败,错误原因请查看异常 e.printStackTrace(); } } /** * 对解密后的xml信息进行处理 * * @param xml */ void parsingMsg(HttpServletRequest request, HttpServletResponse response, String sEncryptXml) throws DocumentException, IOException { Document doc = DocumentHelper.parseText(sEncryptXml); Element rootElt = doc.getRootElement(); String msgType = rootElt.elementText("MsgType"); String toUserName = rootElt.elementText("ToUserName"); String fromUserName = rootElt.elementText("FromUserName"); logger.info("---消息类型msgType:" + msgType + "-----------------企业微信CorpID:" + toUserName + "-----------------成员UserID:" + fromUserName); if ("event".equals(msgType)) { String event = rootElt.elementText("Event"); logger.info("开始解析事件消息--------,事件类型:" + event); // replyEventMessage(request, response, event, toUserName, fromUserName); } else if ("text".equals(msgType)) { logger.info("开始解析文本消息--------"); String content = rootElt.elementText("Content"); processTextMessage(request, response, content, toUserName, fromUserName); } } /** * 事件消息 * * @param request * @param response * @param event * @param toUserName * @param fromUserName * @param appid * @throws DocumentException * @throws IOException */ public void replyEventMessage(HttpServletRequest request, HttpServletResponse response, String event, String toUserName, String fromUserName) throws DocumentException, IOException { switch (event) { case "": break; default: break; } String content = event + "from_callback"; logger.info("---全网发布接入检测------step.4-------事件回复消息 content=" + content + " toUserName=" + toUserName + " fromUserName=" + fromUserName); replyTextMessage(request, response, content, toUserName, fromUserName); } /** * 文本消息 * * @param request * @param response * @param content * @param toUserName * @param fromUserName * @param appid * @throws IOException * @throws DocumentException */ public void processTextMessage(HttpServletRequest request, HttpServletResponse response, String content, String toUserName, String fromUserName) throws IOException, DocumentException { String reContent = content + "from_callback"; logger.info("---全网发布接入检测------step.4-------文本回复消息 content=" + content + " toUserName=" + toUserName + " fromUserName=" + fromUserName); replyTextMessage(request, response, content, toUserName, fromUserName); } /** * 回复微信服务器"文本消息" * * @param request * @param response * @param content * @param toUserName * @param fromUserName * @throws DocumentException * @throws IOException */ public void replyTextMessage(HttpServletRequest request, HttpServletResponse response, String content, String toUserName, String fromUserName) throws DocumentException, IOException { Long createTime = Calendar.getInstance().getTimeInMillis() / 1000; StringBuffer sb = new StringBuffer(); sb.append("<xml>"); sb.append("<ToUserName><![CDATA[" + fromUserName + "]]></ToUserName>"); sb.append("<FromUserName><![CDATA[" + toUserName + "]]></FromUserName>"); sb.append("<CreateTime>" + createTime + "</CreateTime>"); sb.append("<MsgType><![CDATA[text]]></MsgType>"); sb.append("<Content><![CDATA[" + content + "]]></Content>"); sb.append("</xml>"); String replyMsg = sb.toString(); String returnvaleue = ""; try { // 此处的receiveid 随便定义均可通过加密算法,联系企业微信未得到合理解释,暂时按照文档,此处参数使用企业对应的corpid logger.info("===================>>>企业coidId:" + toUserName); WXBizMsgCrypt wxcpt = new WXBizMsgCrypt(EnterpriseConst.STOKEN, EnterpriseConst.SENCODINGAESKEY, toUserName); returnvaleue = wxcpt.EncryptMsg(replyMsg, createTime.toString(), "easemob"); } catch (AesException e) { e.printStackTrace(); } output(response, returnvaleue); } }
OAuth2Authorize:
package com.ffxl.hi.controller.qywx;import io.swagger.annotations.Api;import io.swagger.annotations.ApiOperation;import java.io.UnsupportedEncodingException;import java.util.List;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.PathVariable;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.ResponseBody;import org.springframework.web.servlet.ModelAndView;import com.ffxl.cloud.model.SQywxApplication;import com.ffxl.cloud.service.SQywxApplicationAuthorizerService;import com.ffxl.cloud.service.SQywxApplicationService;import com.ffxl.hi.controller.qywx.util.EnterpriseAPI;import com.ffxl.hi.controller.qywx.util.EnterpriseConst;import com.ffxl.platform.exception.BusinessException;import com.ffxl.platform.qywx.model.ApiUserDetailResponse;import com.ffxl.platform.qywx.model.ApiUserInfoResponse;import com.ffxl.platform.util.JsonResult;import com.ffxl.platform.util.Message;import com.ffxl.platform.util.StringUtil;/** * 用户在不告知第三方自己的帐号密码情况下,通过授权方式,让第三方服务可以获取自己的资源信息 网页授权登陆 第三方应用oauth2链接 * * @author wison */@Controller@RequestMapping(value = "/qy/oauth2")@Api(value = "/qy/oauth2")public class OAuth2Authorize { private static final Logger logger = LoggerFactory.getLogger(OAuth2Authorize.class); @Autowired private SQywxApplicationService sqywxApplicationService; @Autowired private SQywxApplicationAuthorizerService sqywxApplicationAuthorizerService; // oauth2地址 public static final String OAUTH2_URL = "https://open.weixin.qq.com/connect/oauth2/authorize?appid="; /** * 企业微信网页授权API * * @param appId * 第三方应用id(即ww或wx开头的suite_id)。注意与企业的网页授权登录不同 * @param pageView * @param scope * 应用授权作用域。 * @param request * @param response * @return */ @RequestMapping(value = "/getRedirectUrl") @ResponseBody public JsonResult getRedirectUrl(String suiteId, String pageView, String scope, HttpServletRequest request, HttpServletResponse response) { if (StringUtil.isEmpty(suiteId, pageView, scope)) { throw new BusinessException(Message.M4003); } EnterpriseConst suiteConst = new EnterpriseConst("suite"); suiteConst.setKey(suiteId); String suiteSecret = suiteConst.getValue(); if (StringUtil.isEmpty(suiteSecret)) { throw new BusinessException(Message.M4004); } // 第一步:引导用户进入授权页面,根据应用授权作用域,获取code String basePath = request.getScheme() + "://" + request.getServerName(); String backUrl = basePath + "/third/qy/oauth2/redirect/" + suiteId; logger.info("------------------------回调地址:----" + backUrl); // 微信授权地址 String redirectUrl = oAuth2Url(suiteId, backUrl, scope, pageView); logger.info("------------------------授权地址:----" + redirectUrl); return new JsonResult(true, redirectUrl); } /** * 构造带员工身份信息的URL * * @param appid * 第三方应用id(即ww或wx开头的suite_id) * @param redirect_uri * 授权后重定向的回调链接地址,请使用urlencode对链接进行处理 * @param state * 重定向后会带上state參数,企业能够填写a-zA-Z0-9的參数值 * @return */ private String oAuth2Url(String suiteId, String redirect_uri, String scope, String state) { try { redirect_uri = java.net.URLEncoder.encode(redirect_uri, "utf-8"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } String oauth2Url = OAUTH2_URL + suiteId + "&redirect_uri=" + redirect_uri + "&response_type=code" + "&scope=" + scope + "&state=" + state + "#wechat_redirect"; System.out.println("oauth2Url=" + oauth2Url); return oauth2Url; } /** * 微信回调地址 * * @param request * @return */ @RequestMapping(value = "/redirect/{suiteId}") @ApiOperation(value = "微信回调地址", httpMethod = "GET", hidden = true, notes = "微信回调地址") public ModelAndView redirectDetail(@PathVariable String suiteId, HttpServletRequest request, HttpServletResponse response) { response.setHeader("Access-Control-Allow-Origin", "*"); response.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization"); response.setHeader("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS"); if (StringUtil.isEmpty(suiteId)) { throw new BusinessException(Message.M4003); } ModelAndView mv = new ModelAndView(); try { String code = request.getParameter("code"); // state为跳转路径,微信不识别&符号,顾前端如有参数,请使用@代替,在此处做转换 // 页面路径必须在jsp目录下,后缀名可自定义 String state = request.getParameter("state"); logger.debug("------------------------state:----" + state); state = state.replaceAll("@", "&"); logger.debug("------------------------state:----" + state); // 用户授权 SQywxApplication application = sqywxApplicationService.queryBySuiteId(suiteId); if (application == null) { throw new BusinessException("suiteId:" + suiteId + "对应的第三方应用尚未初始化,请等待10分钟或联系服务商管理员"); } String suiteAccessToken = application.getSuiteAccessToken(); logger.info("参数=================================="); logger.info("appid:" + suiteId); logger.info("code:" + code); logger.info("suiteAccessToken:" + suiteAccessToken); // 第一个第三方应用 if (EnterpriseConst.SUITE_ID_1.equals(suiteId)) { // 授权 ApiUserInfoResponse userInfo = EnterpriseAPI.getuserinfo3rd(suiteAccessToken, code); if (userInfo != null && StringUtil.isEmpty(userInfo.getUserId()) && StringUtil.isEmpty(userInfo.getUserTicket())) { mv.addObject("code", "5001"); mv.addObject("msg", "成员没有加入企业微信~"); mv.setViewName("qywx/error.jsp"); // 跳转等待页面,然后再跳回之前页面 return mv; } String corpId = userInfo.getCorpId(); // 授权方的企业id ApiUserDetailResponse userDetail3rd = EnterpriseAPI.getUserDetail3rd(suiteAccessToken, userInfo.getUserTicket()); // 验证用户是否咨询师身份 boolean isConsole = false; String accessToken = EnterpriseAPI.getCorpAccessToken(suiteAccessToken, suiteId, corpId); ApiUserDetailResponse userDetail = EnterpriseAPI.getUserDepartment(accessToken, userInfo.getUserId()); List<Integer> departmentList = userDetail.getDepartmentList(); List<String> departmentNameList = userDetail.getDepartmentNameList(); if (departmentList.contains(EnterpriseConst.SUITE_ID_1_DEPARTMENT_CONSOLE)) { isConsole = true; } mv.addObject("error", false); mv.addObject("isConsole", isConsole); mv.addObject("userId", userInfo.getUserId()); mv.addObject("corpId", corpId); mv.addObject("departmentName", departmentNameList); mv.addObject("name", userDetail3rd.getName()); mv.addObject("avatar", userDetail3rd.getAvatar()); mv.addObject("pageView", state); } else if (EnterpriseConst.SUITE_ID_2.equals(suiteId)) { // 授权 ApiUserInfoResponse userInfo = EnterpriseAPI.getuserinfo3rd(suiteAccessToken, code); if (userInfo != null && StringUtil.isEmpty(userInfo.getUserId()) && StringUtil.isEmpty(userInfo.getUserTicket())) { mv.addObject("code", "5001"); mv.addObject("msg", "成员没有加入企业微信~"); mv.setViewName("qywx/error.jsp"); // 跳转等待页面,然后再跳回之前页面 return mv; } String corpId = userInfo.getCorpId(); // 授权方的企业id ApiUserDetailResponse userDetail3rd = EnterpriseAPI.getUserDetail3rd(suiteAccessToken, userInfo.getUserTicket()); // 验证用户是否咨询师身份 boolean isConsole = false; String accessToken = EnterpriseAPI.getCorpAccessToken(suiteAccessToken, suiteId, corpId); ApiUserDetailResponse userDetail = EnterpriseAPI.getUserDepartment(accessToken, userInfo.getUserId()); List<Integer> departmentList = userDetail.getDepartmentList(); List<String> departmentNameList = userDetail.getDepartmentNameList(); if (departmentList.contains(EnterpriseConst.SUITE_ID_2_DEPARTMENT_CONSOLE)) { isConsole = true; } logger.info("返回用户======================" + userDetail); logger.info("返回部门======================" + departmentNameList); mv.addObject("error", false); mv.addObject("isConsole", isConsole); mv.addObject("userId", userInfo.getUserId()); mv.addObject("corpId", corpId); mv.addObject("departmentName", departmentNameList); mv.addObject("name", userDetail3rd.getName()); mv.addObject("avatar", userDetail3rd.getAvatar()); mv.addObject("pageView", state); } else if (EnterpriseConst.SUITE_ID_3.equals(suiteId)) { // 授权 ApiUserInfoResponse userInfo = EnterpriseAPI.getuserinfo3rd(suiteAccessToken, code); if (userInfo != null && StringUtil.isEmpty(userInfo.getUserId()) && StringUtil.isEmpty(userInfo.getUserTicket())) { mv.addObject("code", "5001"); mv.addObject("msg", "成员没有加入企业微信~"); mv.setViewName("qywx/error.jsp"); // 未加入企业微信 return mv; } String corpId = userInfo.getCorpId(); // 授权方的企业id ApiUserDetailResponse userDetail3rd = EnterpriseAPI.getUserDetail3rd(suiteAccessToken, userInfo.getUserTicket()); // 验证用户是否咨询师身份 boolean isConsole = false; String accessToken = EnterpriseAPI.getCorpAccessToken(suiteAccessToken, suiteId, corpId); ApiUserDetailResponse userDetail = EnterpriseAPI.getUserDepartment(accessToken, userInfo.getUserId()); List<Integer> departmentList = userDetail.getDepartmentList(); List<String> departmentNameList = userDetail.getDepartmentNameList(); if (departmentList.contains(EnterpriseConst.SUITE_ID_3_DEPARTMENT_CONSOLE)) { isConsole = true; } logger.info("返回用户======================" + userDetail); logger.info("返回部门======================" + departmentNameList); mv.addObject("error", false); mv.addObject("isConsole", isConsole); mv.addObject("userId", userInfo.getUserId()); mv.addObject("corpId", corpId); mv.addObject("departmentName", departmentNameList); mv.addObject("name", userDetail3rd.getName()); mv.addObject("avatar", userDetail3rd.getAvatar()); mv.addObject("pageView", state); } else { mv.addObject("error", true); } mv.setViewName("qywx/loading.jsp"); // 跳转等待页面,然后再跳回之前页面 return mv; } catch (BusinessException e) { mv.addObject("code", "5000"); mv.addObject("msg", "出错了,点击返回首页"); mv.setViewName("qywx/error.jsp"); // 跳转等待页面,然后再跳回之前页面 return mv; } } /** * 企业微信后台回调地址 * * @param request * @return */ @RequestMapping(value = "/admin/redirect") public ModelAndView adminRedirectDetail(String auth_code, HttpServletRequest request, HttpServletResponse response) { response.setHeader("Access-Control-Allow-Origin", "*"); response.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization"); response.setHeader("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS"); if (StringUtil.isEmpty(auth_code)) { throw new BusinessException(Message.M4003); } ModelAndView mv = new ModelAndView(); logger.info("参数=================================="); logger.info("code:" + auth_code); try { String providerAccessToken = EnterpriseAPI.getProviderToken(EnterpriseConst.SCORPID, EnterpriseConst.PROVIDERSECRET); ApiUserDetailResponse userDetail = EnterpriseAPI.getLoginInfo(providerAccessToken, auth_code); String url = "http://wxadmin.feifanxinli.com/admin/wechat_user/login?" + "wechatUserId=" + userDetail.getUserId() + "&userName=" + userDetail.getName() + "&headlImg=" + userDetail.getAvatar(); mv.addObject("error", false); mv.addObject("url", url); mv.setViewName("qywx/admin/wuxigongdian.jsp"); // 跳转等待页面,然后再跳回之前页面 return mv; } catch (Exception e) { mv.addObject("error", true); mv.setViewName("qywx/admin/wuxigongdian.jsp"); // 跳转等待页面,然后再跳回之前页面 return mv; } } }
JSSDK:
package com.ffxl.hi.controller.qywx;import io.swagger.annotations.Api;import io.swagger.annotations.ApiOperation;import java.io.IOException;import java.util.HashMap;import java.util.Map;import javax.servlet.http.HttpServletResponse;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.PathVariable;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.ResponseBody;import com.ffxl.cloud.model.SQywxApplication;import com.ffxl.cloud.service.SQywxApplicationService;import com.ffxl.hi.controller.qywx.util.EnterpriseAPI;import com.ffxl.platform.util.JsonResult;import com.ffxl.platform.util.Message;import com.ffxl.platform.util.StringUtil;@Controller@RequestMapping(value = "/qy/jssdk")@Api(value = "/qy/jssdk")public class JSSDK { private static final Logger logger = LoggerFactory.getLogger(JSSDK.class); @Autowired private SQywxApplicationService sqywxApplicationService; /** * 企业微信使用jssdk参数 * * @return * @throws IOException */ @RequestMapping(value = "/config/{suiteId}/{authCorpId}") @ResponseBody @ApiOperation(value = "公众号回调地址", httpMethod = "GET", hidden = true) public JsonResult jssdkconfig(@PathVariable String suiteId, @PathVariable String authCorpId, String requestUrl, HttpServletResponse response) { if (StringUtil.isEmpty(suiteId, authCorpId, requestUrl)) { return new JsonResult(Message.M4003); } // 根据suiteId查询第三方信息 SQywxApplication application = sqywxApplicationService.queryBySuiteId(suiteId); if (application == null || StringUtil.isEmpty(application.getSuiteAccessToken())) { return new JsonResult(Message.M3001, "suiteId:" + suiteId + "对应的第三方应用尚未初始化,请等待10分钟或联系服务商管理严", null); } String accessToken = EnterpriseAPI.getCorpAccessToken(application.getSuiteAccessToken(), suiteId, authCorpId); Map<String, String> tickMap = EnterpriseAPI.getJsTicket(accessToken, suiteId, authCorpId); String corpTicket = tickMap.get("corpTicket"); String ticket = tickMap.get("ticket"); String agentId = tickMap.get("agentId"); // 企业js-sdk签名 logger.info("requestUrl:" + requestUrl); logger.info("suiteId:" + suiteId); logger.info("authCorpId:" + authCorpId); logger.info("corpTicket:" + corpTicket); Map<String, Object> corpObjMap = EnterpriseAPI.getWxConfig(requestUrl, suiteId, authCorpId, corpTicket); logger.info("corpSignature:" + corpObjMap.get("signature")); // js-sdk签名 logger.info("requestUrl:" + requestUrl); logger.info("suiteId:" + suiteId); logger.info("authCorpId:" + authCorpId); logger.info("ticket:" + ticket); logger.info("agentId:" + agentId); Map<String, Object> objMap = EnterpriseAPI.getWxConfig(requestUrl, suiteId, authCorpId, ticket); objMap.put("agentId", agentId); // 查询企业的 logger.info("signature:" + objMap.get("signature")); Map<String, Object> restObj = new HashMap<String, Object>(); restObj.put("config", corpObjMap); // 企业 restObj.put("agentConfig", objMap); // 应用 return new JsonResult(true, restObj); } }
六、企业微信配置
项目启动成功后,打包发布到外网,我们继续修改第三步中随便填写的配置信息
应用主页:本项目不涉及,应该填写你的Web项目的index.html页面地址
可信域名:这里要填授权服务器以及前端项目对应的域名
安装完成回调域名:可填写授权服务器的域名
业务设置URL: http://xxx.com/third/qy/oauth2/admin/redirect
数据回调URL: http://xxx.com/third/qy/message/$CORPID$/callback
指令回调URL:http://xxx.com/third/qy/suite/directive/receive?suiteid=xxxxxx 注:该处填写应用的suitedId,方便动态获取
自定义菜单,这里要提前设置,审核后可在通知进入企业微信的消息列表,快速进入应用,如果是审核后设置,无效
成功运行后,系统会初始化s_qywx_application数据
七、效果图
浏览器输入管理员安装链接:域名/项目名/jsp/qywx/prodAuth.html
使用企业微信管理员扫码,同意授权后就能安装应用到工作台
安装成功后,系统会初始化s_qywx_application_authorizer数据
每隔10分钟,更更新相应数据,后续的接口调用,都要依靠以上表中的内容发起