一、构建spring boot项目
1、新建项目
新建一个模块(module):enterprise-wechat
新建一个子模块(module):wechat
目录结构如下:
结构描述:
common
-> WeChatConstants:存放企业微信一些常量,公用参数
-> WeChatUtils:存放企业微信第三方应用api
controller
-> SystemController:控制层,接收请求
entity
-> aes:目录下文件企业微信加解密包
service
-> IConfigService:调用企业微信服务层
pom.xml
-> 导入所需要的jar包
pom.xml中需要导入commons.codec包
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.9</version>
</dependency>
2、方法描述
1)doGetCallback:
① 接收验证请求,用于验证通用开发参数系统事件接收URL、数据回调URL、指令回调URL。
② 企业微信后台录入回调URL点击保存时,微信服务器会立即发送一条GET请求到对应URL,该函数就对URL的signature进行验证。
2)doPostCallback:
① 用于获取 suite_ticket,安装应用时企业微信传递过来的auth_code:指令回调URL。
② 当刷新ticket传递【SuitID】:指令回调URL。
③ 当打开应用时传递【CorpID】:数据回调URL。
3、代码编写
1)企业微信配置类:WeChatConstants
package com.wechat.common;
/**
* 企业微信
*/
public class WeChatConstants {
// 企业微信授权码获取时间
public static final Long EXPIRES_IN = 24 * 60 * 60 * 1000L;
//24 * 60 * 60 * 1000L 7200L * 1000
/**
* 服务商CorpID
*/
public static final String CORP_ID = "ww14438c6c07a317f2";
/**
* 服务商身份的调用凭证
*/
public static final String PROVIDER_SECRET = "RH7PehRJX3LIcw4axad_H2T9HSUG1finOBEpnLTVIioBrP-zgZrGsqJ9pHVw5vVj";
/**
* 应用的唯一身份标识
*/
public static final String SUITE_ID = "ww4f66fa544a32f920";
/**
* 应用的调用身份密钥
*/
public static final String SUITE_SECRET = "vVv8JzaBlEVCTQkHKqmr57EAMs65AILWiI_4ANc25T4";
/**
* 应用的ticket
*/
public static final String SUITE_TICKET = "SUITE_TICKET";
/**
* 应用的auth_code
*/
public static final String AUTH_CODE = "AUTH_CODE";
/**
* 第三方应用凭证token
*/
public static final String SUITE_TOKEN = "suiteToken";
/**
* 授权方(企业)token
*/
public static final String ACCESS_TOKEN = "ACCESS_TOKEN";
/**
* 提供商 授权方服务token
*/
public static final String PROVIDER_ACCESS_TOKEN = "PROVIDER_ACCESS_TOKEN";
/**
* 应用企业corpid
*/
public static final String AUTH_CORPID = "AUTH_CORPID";
/**
* 企业名称
*/
public static final String CORP_NAME = "CORPNAME";
/**
* 授权方的网页应用ID,在具体的网页应用中查看
*/
public static final String AGENT_ID = "AGENTID";
/**
* 用户id
*/
public static final String USER_ID = "userId";
// 回调相关
/**
* 回调/通用开发参数Token, 两者解密算法一样,所以为方便设为一样
*/
public static final String TOKENS = "E0sOXx4LqeE5BmDvMTAz3x";
/**
* 回调/通用开发参数EncodingAESKey, 两者解密算法一样,所以为方便设为一样
*/
public static final String ENCODING_AES_KEY = "IESLPSyW4vyBB90jkzfwfYRtcMky6LIOevr4SVefz7I";
public static final String REDIRECT_URI = "REDIRECT_URI";
/**
* 重定向地址,自己设置
*/
public static final String REDIRECT_URL = "www.baidu.com";
// 第三方应用id(即ww或wx开头的suite_id)
public static final String APP_ID= "APPID";
public static final String PERMANENT_CODE = "PERMANENT_CODE";
}
2)企业微信api:WeChatUtils
package com.wechat.common;
/**
* 企业微信工具类
*/
public class WeChatUtils {
/**
* 第三方应用api start
*/
// 获取第三方应用凭证
public final static String THIRD_BUS_WECHAT_SUITE_TOKEN = "https://qyapi.weixin.qq.com/cgi-bin/service/get_suite_token";
// 获取企业永久授权码
public final static String THIRD_BUS_WECHAT_ACCESS_TOKEN = "https://qyapi.weixin.qq.com/cgi-bin/service/get_permanent_code?suite_access_token=SUITE_ACCESS_TOKEN";
// 第三方 构造扫码登录链接
public final static String THIRD_BUS_WECHAT_LOGIN = "https://open.work.weixin.qq.com/wwopen/sso/3rd_qrConnect?appid=CORPID&redirect_uri=REDIRECT_URI&state=web_login&usertype=member";
// 第三方 获取登录用户信息 POST
public final static String THIRD_BUS_WECHAT_GET_LOGIN_INFO = "https://qyapi.weixin.qq.com/cgi-bin/service/get_login_info?access_token=PROVIDER_ACCESS_TOKEN";
// 第三方 构造网页授权链接
public final static String THIRD_BUS_WECHAT_AUTHORIZE_URL = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=snsapi_privateinfo&state=STATE#wechat_redirect";
// 第三方 获取访问用户身份 GET
public final static String THIRD_BUS_WECHAT_GET_USER_INFO = "https://qyapi.weixin.qq.com/cgi-bin/service/getuserinfo3rd?suite_access_token=SUITE_TOKEN&code=CODE";
// 第三方 获取访问用户敏感信息 post
public final static String THIRD_BUS_WECHAT_GET_USER_DETAIL3RD = "https://qyapi.weixin.qq.com/cgi-bin/service/getuserdetail3rd?suite_access_token=SUITE_ACCESS_TOKEN";
// 第三方 获取部门列表
public final static String THIRD_BUS_WECHAT_DEPART_LIST = "https://qyapi.weixin.qq.com/cgi-bin/department/list?access_token=ACCESS_TOKEN&id=ID";
// 第三方 获取部门成员
public final static String THIRD_BUS_WECHAT_DEPART_USER = "https://qyapi.weixin.qq.com/cgi-bin/user/simplelist?access_token=ACCESS_TOKEN&department_id=DEPARTMENT_ID&fetch_child=FETCH_CHILD";
// 第三方 获取部门成员详情
public final static String THIRD_BUS_WECHAT_DEPART_USER_DETAIL = "https://qyapi.weixin.qq.com/cgi-bin/user/list?access_token=ACCESS_TOKEN&department_id=DEPARTMENT_ID&fetch_child=FETCH_CHILD";
// 第三方 读取成员 GET
public final static String THIRD_BUS_WECHAT_GET_USER = "https://qyapi.weixin.qq.com/cgi-bin/user/get?access_token=ACCESS_TOKEN&userid=USERID";
// 服务商的token
public final static String THIRD_BUS_WECHAT_GET_PROVIDER_TOKEN = "https://qyapi.weixin.qq.com/cgi-bin/service/get_provider_token";
// 获取企业凭证
public final static String THIRD_BUS_WECHAT_GET_CORP_TOKEN = "https://qyapi.weixin.qq.com/cgi-bin/service/get_corp_token?suite_access_token=SUITE_ACCESS_TOKEN";
// 发送应用消息
public final static String THIRD_BUS_WECHAT_SEND = "https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=ACCESS_TOKEN";
// 获取应用的jsapi_ticket
public final static String THIRD_BUS_GET_JSAPI_TICKET = "https://qyapi.weixin.qq.com/cgi-bin/ticket/get?access_token=ACCESS_TOKEN&type=agent_config";
// 获取企业的jsapi_ticket
public final static String THIRD_BUS_GET_JSAPI_TICKET_BUS = "https://qyapi.weixin.qq.com/cgi-bin/get_jsapi_ticket?access_token=ACCESS_TOKEN";
/**
* 第三方应用api end
*/
}
3)controller层:SystemController
package com.wechat.controller;
import com.wechat.service.IConfigService;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
/**
* 控制层
*/
@Slf4j
@RestController
@RequestMapping(value = "system")
public class SystemController {
@Autowired
private IConfigService configService;
/**
* 验证通用开发参数及应用回调
* @param: request
* @param: response
* @returns: void
*/
@ApiOperation(value = "验证通用开发参数及应用回调")
@GetMapping(value = "getEchostr")
public void doGetCallback(HttpServletRequest request, HttpServletResponse response) throws Exception {
// 微信加密签名
String msgSignature = request.getParameter("msg_signature");
// 时间戳
String timestamp = request.getParameter("timestamp");
// 随机数
String nonce = request.getParameter("nonce");
// 随机字符串
// 如果是刷新,需返回原echostr
String echoStr = request.getParameter("echostr");
String sEchoStr= "";
PrintWriter out;
log.debug("msgSignature: " + msgSignature+"timestamp="+timestamp+"nonce="+nonce+"echoStr="+echoStr);
try {
sEchoStr = configService.doGetCallback(msgSignature,timestamp,nonce,echoStr); //需要返回的明文;
log.debug("doGetCallback-> echostr: " + sEchoStr);
// 验证URL成功,将sEchoStr返回
out = response.getWriter();
out.print(sEchoStr);
} catch (Exception e) {
//验证URL失败,错误原因请查看异常
e.printStackTrace();
}
}
/**
* 刷新ticket,AuthCode
*/
@ApiOperation(value = "刷新ticket,AuthCode")
@PostMapping(value = "getEchostr")
public String doPostCallback(HttpServletRequest request) throws Exception {
// 微信加密签名
String msgSignature = request.getParameter("msg_signature");
// 时间戳
String timestamp = request.getParameter("timestamp");
// 随机数
String nonce = request.getParameter("nonce");
// 类型
String type = request.getParameter("type");
// 企业id
String corpId = request.getParameter("corpid");
ServletInputStream in = request.getInputStream();
// 刷新ticket,AuthCode
String success = configService.doPostCallback(msgSignature, timestamp, nonce, type, corpId, in);
return success;
}
}
4)Service层:IConfigService
package com.wechat.service;
import javax.servlet.ServletInputStream;
/**
* 企业微信第三方服务service
*/
public interface IConfigService {
/**
* 验证通用开发参数及应用回调
* @returns: java.lang.String
*/
String doGetCallback(String msgSignature, String timestamp, String nonce, String echoStr);
/**
* 获取SuiteTicket,AuthCode
*/
String doPostCallback(String msgSignature, String timestamp, String nonce, String type, String corpId, ServletInputStream in);
}
5)service实现类:ConfigServiceImpl
package com.wechat.service.impl;
import com.alibaba.druid.support.json.JSONUtils;
import com.wechat.common.StringUtils;
import com.wechat.common.WeChatConstants;
import com.wechat.common.WxUtil;
import com.wechat.common.cache.CacheData;
import com.wechat.entity.aes.AesException;
import com.wechat.entity.aes.WXBizMsgCrypt;
import com.wechat.service.IConfigService;
import com.wechat.service.IWeChatService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.servlet.ServletInputStream;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.Map;
/**
* 回调service
*/
@Slf4j
@Service
public class ConfigServiceImpl implements IConfigService {
@Autowired
private IWeChatService weChatService;
/**
* 验证通用开发参数及应用回调
* @returns: java.lang.String
*/
@Override
public String doGetCallback(String msgSignature, String timestamp, String nonce, String echoStr) {
//需要返回的明文
String sEchoStr="";
try {
log.debug(WeChatConstants.TOKENS, WeChatConstants.ENCODING_AES_KEY, WeChatConstants.CORP_ID);
WXBizMsgCrypt wxcpt = new WXBizMsgCrypt(WeChatConstants.TOKENS, WeChatConstants.ENCODING_AES_KEY, WeChatConstants.CORP_ID);
sEchoStr = wxcpt.VerifyURL(msgSignature, timestamp, nonce, echoStr);
} catch (AesException e) {
e.printStackTrace();
}
return sEchoStr;
}
/**
* 获取SuiteTicket,AuthCode
* @param: msgSignature 微信加密签名
* @param: timestamp 时间戳
* @param: nonce 随机数
* @param: type 类型
* @param: corpId 企业id
* @param: in
* @returns: java.lang.String
*/
@Override
public String doPostCallback(String msgSignature, String timestamp, String nonce, String type, String corpId, ServletInputStream in) {
String id = "";
// 访问应用和企业回调传不同的ID
if(!StringUtils.isNull(type) && type.equals("data")){
id = corpId;
log.debug("======corpId==="+id);
} else {
id = WeChatConstants.SUITE_ID;
log.debug("======SuiteId===" + id);
}
try {
WXBizMsgCrypt wxcpt = new WXBizMsgCrypt(WeChatConstants.TOKENS, WeChatConstants.ENCODING_AES_KEY, id);
String postData=""; // 密文,对应POST请求的数据
//1.获取加密的请求消息:使用输入流获得加密请求消息postData
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
String tempStr = ""; //作为输出字符串的临时串,用于判断是否读取完毕
while(null != (tempStr=reader.readLine())){
postData+=tempStr;
}
log.debug("====msg_signature===="+msgSignature+"====timestamp==="+timestamp+"====nonce==="+nonce+"====postData==="+postData);
String suiteXml = wxcpt.DecryptMsg(msgSignature, timestamp, nonce, postData);
log.debug("suiteXml: " + suiteXml);
Map suiteMap = WxUtil.parseXml(suiteXml);
log.debug("==suiteMap=="+ JSONUtils.toJSONString(suiteMap));
if(suiteMap.get("SuiteTicket") != null) {
String suiteTicket = (String) suiteMap.get("SuiteTicket");
CacheData.put(WeChatConstants.SUITE_TICKET, suiteTicket);
log.debug("====SuiteTicket=====" + suiteTicket);
} else if(suiteMap.get("AuthCode") != null){
String authCode = (String) suiteMap.get("AuthCode");
log.debug("doPostValid->AuthCode:" + authCode);
//根据authcode获取企业永久授权码
weChatService.getPermanentCode(authCode);
CacheData.put(WeChatConstants.AUTH_CODE, authCode);
}
} catch (Exception e) {
e.printStackTrace();
}
return "success";
}
}
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!--<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.5</version>
<relativePath/>
</parent>-->
<parent>
<groupId>org.example</groupId>
<artifactId>third-wechat</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<groupId>com.wechat</groupId>
<artifactId>wechat</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>wechat</name>
<description>wechat</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>2.0.0</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.9</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.10</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.16</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.4</version>
</dependency>
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-annotations</artifactId>
<version>1.5.24</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.5</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.9.6</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
logback.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">
<!-- 日志存放路径 -->
<property name="log.path" value="logs/wechat" />
<!-- 日志输出格式 -->
<property name="log.pattern" value="%d{HH:mm:ss.SSS} [%thread] %-5level %logger{20} - [%method,%line] - %msg%n" />
<!-- 控制台输出 -->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${log.pattern}</pattern>
</encoder>
</appender>
<!-- 系统日志输出 -->
<appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/info.log</file>
<!-- 循环政策:基于时间创建日志文件 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 日志文件名格式 -->
<fileNamePattern>${log.path}/info.%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- 日志最大的历史 60天 -->
<maxHistory>60</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${log.pattern}</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<!-- 过滤的级别 -->
<level>INFO</level>
<!-- 匹配时的操作:接收(记录) -->
<onMatch>ACCEPT</onMatch>
<!-- 不匹配时的操作:拒绝(不记录) -->
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/error.log</file>
<!-- 循环政策:基于时间创建日志文件 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 日志文件名格式 -->
<fileNamePattern>${log.path}/error.%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- 日志最大的历史 60天 -->
<maxHistory>60</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${log.pattern}</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<!-- 过滤的级别 -->
<level>ERROR</level>
<!-- 匹配时的操作:接收(记录) -->
<onMatch>ACCEPT</onMatch>
<!-- 不匹配时的操作:拒绝(不记录) -->
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- 系统模块日志级别控制 -->
<logger name="com.wechat" level="debug" />
<!-- Spring日志级别控制 -->
<logger name="org.springframework" level="warn" />
<!--<root level="info">
<appender-ref ref="console" />
</root>-->
<root level="debug">
<appender-ref ref="console" />
</root>
<!--系统操作日志-->
<root level="info">
<appender-ref ref="file_info" />
<appender-ref ref="file_error" />
</root>
</configuration>
4、验证
以上代码编写完成后,就可以打包到环境上面进行测试验证:
①:echostr验证
返回结果:返回 echostr,并显示已验证
16:11:46.940 [http-nio-9205-exec-7] INFO c.q.w.s.c.SystemController - [doGetValid,94] - doGetCallback->echostr: 577115934236344259
16:11:46.969 [http-nio-9205-exec-3] INFO c.q.w.s.c.SystemController - [doGetValid,94] - doGetCallback->echostr: 5267604771365158379
②:刷新Ticket:获取Ticket有两种方式,一是点击按钮获取,二是企业微信每15分钟会调用回调接口获取一次
点击“刷新Ticket” 会弹出如下图,然后点击确定
Ticket 有效期为30分钟;建议把Ticket放到数据库或者redis中
③:获取auth_code
安装第三方应用的时候,会获取auth_code
④:安装测试流程
通过企业微信扫码进行安装
上面就是验证通过,及获取Ticket和auth_code
5、总结
在第三方应用开发中,主要围绕三种类型的access_token(见企业微信地方应用(二))
provider_access_token:服务商的token
suite_access_token:获取第三方应用凭证
access_token:授权方(企业)access_token
通过上面的代码及配置,我们获取到了suiteTicket和auth_code。
接下来我们要通过这些值获取到上面token,通过springboot开发实现“企业微信第三方应用(二)api使用测试”