一、构建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验证

企业微信管理后台信息架构设计分析 企业微信的架构_微信_02


企业微信管理后台信息架构设计分析 企业微信的架构_java_03

返回结果:返回 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分钟会调用回调接口获取一次

企业微信管理后台信息架构设计分析 企业微信的架构_spring_04


点击“刷新Ticket” 会弹出如下图,然后点击确定

企业微信管理后台信息架构设计分析 企业微信的架构_java_05


Ticket 有效期为30分钟;建议把Ticket放到数据库或者redis中

企业微信管理后台信息架构设计分析 企业微信的架构_spring_06


③:获取auth_code

安装第三方应用的时候,会获取auth_code

企业微信管理后台信息架构设计分析 企业微信的架构_spring_07

④:安装测试流程

企业微信管理后台信息架构设计分析 企业微信的架构_spring_08


企业微信管理后台信息架构设计分析 企业微信的架构_java_09


通过企业微信扫码进行安装

企业微信管理后台信息架构设计分析 企业微信的架构_企业微信管理后台信息架构设计分析_10


企业微信管理后台信息架构设计分析 企业微信的架构_企业微信管理后台信息架构设计分析_11


上面就是验证通过,及获取Ticket和auth_code

5、总结
在第三方应用开发中,主要围绕三种类型的access_token(见企业微信地方应用(二))

provider_access_token:服务商的token
suite_access_token:获取第三方应用凭证
access_token:授权方(企业)access_token

通过上面的代码及配置,我们获取到了suiteTicket和auth_code。
接下来我们要通过这些值获取到上面token,通过springboot开发实现“企业微信第三方应用(二)api使用测试”