zhijiezhao 1 рік тому
коміт
212e0692f4
24 змінених файлів з 1837 додано та 0 видалено
  1. 143 0
      pom.xml
  2. 16 0
      src/main/java/com/caimei365/wechat/WechatApplication.java
  3. 38 0
      src/main/java/com/caimei365/wechat/config/GlobalCorsConfig.java
  4. 134 0
      src/main/java/com/caimei365/wechat/controller/WechatCallbackApi.java
  5. 303 0
      src/main/java/com/caimei365/wechat/entity/WXBizJsonMsgCrypt.java
  6. 86 0
      src/main/java/com/caimei365/wechat/entity/WeChatConstant.java
  7. 26 0
      src/main/java/com/caimei365/wechat/entity/WeChatUser.java
  8. 38 0
      src/main/java/com/caimei365/wechat/entity/WechatArticleDetail.java
  9. 38 0
      src/main/java/com/caimei365/wechat/entity/WechatReply.java
  10. 59 0
      src/main/java/com/caimei365/wechat/exception/AesException.java
  11. 25 0
      src/main/java/com/caimei365/wechat/service/WeChatTokenService.java
  12. 221 0
      src/main/java/com/caimei365/wechat/service/impl/WeChatTokenServiceImpl.java
  13. 26 0
      src/main/java/com/caimei365/wechat/utils/ByteGroup.java
  14. 178 0
      src/main/java/com/caimei365/wechat/utils/HttpUtil.java
  15. 66 0
      src/main/java/com/caimei365/wechat/utils/JsonParse.java
  16. 128 0
      src/main/java/com/caimei365/wechat/utils/MessageUtil.java
  17. 67 0
      src/main/java/com/caimei365/wechat/utils/PKCS7Encoder.java
  18. 62 0
      src/main/java/com/caimei365/wechat/utils/SHA1.java
  19. 72 0
      src/main/java/com/caimei365/wechat/utils/SignUtil.java
  20. 25 0
      src/main/resources/application.yml
  21. 26 0
      src/main/resources/config/beta/application-beta.yml
  22. 25 0
      src/main/resources/config/dev/application-dev.yml
  23. 25 0
      src/main/resources/config/prod/application-prod.yml
  24. 10 0
      src/main/resources/templates/index.html

+ 143 - 0
pom.xml

@@ -0,0 +1,143 @@
+<?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.2</version>
+        <relativePath/> <!-- lookup parent from repository -->
+    </parent>
+    <groupId>com.caimei365</groupId>
+    <artifactId>caimei365-wechat-qy</artifactId>
+    <version>0.0.1-SNAPSHOT</version>
+    <name>caimei365-wechat-qy</name>
+    <description>caimei365-wechat-qy</description>
+    <properties>
+        <java.version>1.8</java.version>
+    </properties>
+    <dependencies>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-thymeleaf</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-webflux</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.google.code.gson</groupId>
+            <artifactId>gson</artifactId>
+            <version>2.8.9</version>
+        </dependency>
+        <dependency>
+            <groupId>org.json</groupId>
+            <artifactId>json</artifactId>
+            <version>20200518</version>
+        </dependency>
+        <dependency>
+            <groupId>commons-codec</groupId>
+            <artifactId>commons-codec</artifactId>
+            <version>1.9</version>
+        </dependency>
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>fastjson</artifactId>
+            <version>1.2.79</version>
+        </dependency>
+        <dependency>
+            <groupId>org.dom4j</groupId>
+            <artifactId>dom4j</artifactId>
+            <version>2.1.3</version>
+        </dependency>
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <optional>true</optional>
+        </dependency>
+        <dependency>
+            <groupId>org.mybatis.spring.boot</groupId>
+            <artifactId>mybatis-spring-boot-starter</artifactId>
+            <version>2.2.0</version>
+        </dependency>
+        <dependency>
+            <groupId>com.github.pagehelper</groupId>
+            <artifactId>pagehelper-spring-boot-starter</artifactId>
+            <version>1.4.1</version>
+            <exclusions>
+                <exclusion>
+                    <artifactId>mybatis-spring-boot-starter</artifactId>
+                    <groupId>org.mybatis.spring.boot</groupId>
+                </exclusion>
+            </exclusions>
+        </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.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-redis</artifactId>
+        </dependency>
+    </dependencies>
+
+    <profiles>
+        <profile>
+            <id>dev</id>
+            <properties>
+                <!-- 环境标识,需要与配置文件的名称相对应 -->
+                <activatedProperties>dev</activatedProperties>
+            </properties>
+            <activation>
+                <!-- 默认环境 -->
+                <activeByDefault>true</activeByDefault>
+            </activation>
+        </profile>
+        <profile>
+            <id>beta</id>
+            <properties>
+                <activatedProperties>beta</activatedProperties>
+            </properties>
+        </profile>
+        <profile>
+            <id>prod</id>
+            <properties>
+                <activatedProperties>prod</activatedProperties>
+            </properties>
+        </profile>
+    </profiles>
+    <build>
+        <finalName>${project.artifactId}</finalName>
+        <resources>
+            <resource>
+                <directory>src/main/resources</directory>
+                <!--开启filtering功能  -->
+                <filtering>true</filtering>
+            </resource>
+            <resource>
+                <directory>src/main/resources/config/${activatedProperties}</directory>
+                <filtering>true</filtering>
+            </resource>
+        </resources>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+            </plugin>
+        </plugins>
+    </build>
+</project>

+ 16 - 0
src/main/java/com/caimei365/wechat/WechatApplication.java

@@ -0,0 +1,16 @@
+package com.caimei365.wechat;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+/**
+ * @author Administrator
+ */
+@SpringBootApplication
+public class WechatApplication {
+
+    public static void main(String[] args) {
+        SpringApplication.run(WechatApplication.class, args);
+    }
+
+}

+ 38 - 0
src/main/java/com/caimei365/wechat/config/GlobalCorsConfig.java

@@ -0,0 +1,38 @@
+package com.caimei365.wechat.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.cors.CorsConfiguration;
+import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
+import org.springframework.web.filter.CorsFilter;
+
+/**
+ * 全局跨域配置
+ *
+ * @author : Charles
+ * @date : 2022/1/13
+ */
+@Configuration
+public class GlobalCorsConfig {
+
+    @Bean
+    public CorsFilter corsFilter() {
+        CorsConfiguration config = new CorsConfiguration();
+        // 1 设置访问源地址
+        config.addAllowedOrigin("*");
+        // 3 设置访问源请求方法
+        config.addAllowedMethod("*");
+        // 2 设置访问源请求头
+        config.addAllowedHeader("*");
+        config.addExposedHeader("Content-Type");
+        config.addExposedHeader("X-Requested-With");
+        config.addExposedHeader("accept");
+        config.addExposedHeader("Origin");
+        config.addExposedHeader("Access-Control-Request-Method");
+        config.addExposedHeader("Access-Control-Request-Headers");
+        UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource();
+        // 4 对接口配置跨域设置
+        configSource.registerCorsConfiguration("/**", config);
+        return new CorsFilter(configSource);
+    }
+}

+ 134 - 0
src/main/java/com/caimei365/wechat/controller/WechatCallbackApi.java

@@ -0,0 +1,134 @@
+package com.caimei365.wechat.controller;
+
+
+import com.caimei365.wechat.entity.WXBizJsonMsgCrypt;
+import com.caimei365.wechat.exception.AesException;
+import com.caimei365.wechat.utils.MessageUtil;
+import com.google.gson.Gson;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.Map;
+
+
+@Slf4j
+@RestController
+@RequestMapping("wechat")
+public class WechatCallbackApi {
+
+    /**
+     * 代开发模板的回调接口
+     * @param request
+     * @return
+     * @throws AesException
+     */
+    @GetMapping("/callback")
+    public String getCall(HttpServletRequest request) throws AesException {
+        log.info("get--------------------------------->");
+        //String msg_signature, String timestamp, String nonce, String echostr
+        String sToken = "QDG6eK";
+        String sCorpID = "wwa9819ec0b7c5fea4";
+        String sEncodingAESKey = "jWmYm7qr5nMoAUwZRjGtBxmz3KA1tkAj3ykkR6q2B2C";
+        String msg_signature = request.getParameter("msg_signature");
+        String timestamp = request.getParameter("timestamp");
+        String nonce = request.getParameter("nonce");
+        String echostr = request.getParameter("echostr");
+
+
+        WXBizJsonMsgCrypt wxcpt = new WXBizJsonMsgCrypt(sToken, sEncodingAESKey, sCorpID);
+        log.info("msg_signature----------------------------->" + msg_signature);
+        log.info("timestamp----------------------------->" + timestamp);
+        log.info("nonce----------------------------->" + nonce);
+        log.info("echostr----------------------------->" + echostr);
+        // 需要返回的明文
+        String sEchoStr = "";
+        try {
+            sEchoStr = wxcpt.VerifyURL(msg_signature, timestamp, nonce, echostr);
+            log.info("verifyurl echostr: " + sEchoStr);
+            // 验证URL成功,将sEchoStr返回
+            // HttpUtils.SetResponse(sEchoStr);
+        } catch (Exception e) {
+            // 验证URL失败,错误原因请查看异常
+            e.printStackTrace();
+        }
+        return sEchoStr;
+    }
+
+    /**
+     * 获取Authcode
+     * @param request
+     * @return
+     * @throws AesException
+     */
+    @GetMapping("/dkf/callback")
+    public String getCallForDkf(HttpServletRequest request) throws Exception {
+        log.info("代开发模板get回调--------------------------------->");
+        String sToken = "QDG6eK";
+        // SuiteId 模板id
+        String sCorpID = "wpt0TTUAAAnD7mlTPUTd8bay_z10m5Rw";
+        String sEncodingAESKey = "jWmYm7qr5nMoAUwZRjGtBxmz3KA1tkAj3ykkR6q2B2C";
+        String msg_signature = request.getParameter("msg_signature");
+        String timestamp = request.getParameter("timestamp");
+        String nonce = request.getParameter("nonce");
+        String echostr = request.getParameter("echostr");
+
+
+        WXBizJsonMsgCrypt wxcpt = new WXBizJsonMsgCrypt(sToken, sEncodingAESKey, sCorpID);
+        log.info("msg_signature----------------------------->" + msg_signature);
+        log.info("timestamp----------------------------->" + timestamp);
+        log.info("nonce----------------------------->" + nonce);
+        log.info("echostr----------------------------->" + echostr);
+        String sEchoStr = "";
+        try {
+            sEchoStr = wxcpt.VerifyURL(msg_signature, timestamp, nonce, echostr);
+            log.info("sMsg:" + sEchoStr);
+
+        } catch (Exception e) {
+            // 解密失败,失败原因请查看异常
+            e.printStackTrace();
+        }
+        return sEchoStr;
+    }
+    /**
+     * 代开发应用的回调祭口 获取SuiteTicket
+     * 1.代开发模板中可设置代开发模板回调URl,此接口会接收tx官方定时刷新的suite_ticket
+     * 2.获取suite_ticket后通过https://qyapi.weixin.qq.com/cgi-bin/service/get_suite_token获取suite_access_token
+     * 注:在校验解密的过程中cropId 为代开发应用的cropId
+     * 关于suite_ticket和suite_access_token参考https://developer.work.weixin.qq.com/document/path/90600#14939
+     * @param request
+     * @return
+     * @throws Exception
+     */
+    @PostMapping("/callback")
+    public String postCall(HttpServletRequest request) throws Exception {
+        log.info("代开发模板post回调--------------------------------->");
+        String sToken = "QDG6eK";
+        // SuiteId 模板id
+        String sCorpID = "dkf01ed803e47f9a48";
+        String sEncodingAESKey = "jWmYm7qr5nMoAUwZRjGtBxmz3KA1tkAj3ykkR6q2B2C";
+        WXBizJsonMsgCrypt wxcpt = new WXBizJsonMsgCrypt(sToken, sEncodingAESKey, sCorpID);
+
+        Map<String, String> stringStringMap = MessageUtil.parseXml(request);
+        Gson gson = new Gson();
+        String jsonString = gson.toJson(stringStringMap);
+        log.info("jsonString ------------------------->" + jsonString);
+        String sReqMsgSig = request.getParameter("msg_signature");
+        String sReqTimeStamp = request.getParameter("timestamp");
+        String sReqNonce = request.getParameter("nonce");
+        log.info("msg_signature----------------------------->" + sReqMsgSig);
+        log.info("timestamp----------------------------->" + sReqTimeStamp);
+        log.info("nonce----------------------------->" + sReqNonce);
+        log.info("requestBody----------------------------->" + jsonString);
+        try {
+            String sMsg = wxcpt.DecryptMsg(sReqMsgSig, sReqTimeStamp, sReqNonce, jsonString);
+            log.info("sMsg:" + sMsg);
+
+        } catch (Exception e) {
+            // 解密失败,失败原因请查看异常
+            e.printStackTrace();
+        }
+        return "success";
+    }
+
+}

+ 303 - 0
src/main/java/com/caimei365/wechat/entity/WXBizJsonMsgCrypt.java

@@ -0,0 +1,303 @@
+/**
+ * 对企业微信发送给企业后台的消息加解密示例代码.
+ *
+ * @copyright Copyright (c) 1998-2014 Tencent Inc.
+ * <p>
+ * 针对org.apache.commons.codec.binary.Base64,
+ * 需要导入架包commons-codec-1.9(或commons-codec-1.8等其他版本)
+ * 官方下载地址:http://commons.apache.org/proper/commons-codec/download_codec.cgi
+ * <p>
+ * 针对org.apache.commons.codec.binary.Base64,
+ * 需要导入架包commons-codec-1.9(或commons-codec-1.8等其他版本)
+ * 官方下载地址:http://commons.apache.org/proper/commons-codec/download_codec.cgi
+ */
+
+// ------------------------------------------------------------------------
+
+/**
+ * 针对org.apache.commons.codec.binary.Base64,
+ * 需要导入架包commons-codec-1.9(或commons-codec-1.8等其他版本)
+ * 官方下载地址:http://commons.apache.org/proper/commons-codec/download_codec.cgi
+ */
+package com.caimei365.wechat.entity;
+
+import com.caimei365.wechat.exception.AesException;
+import com.caimei365.wechat.utils.*;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.codec.binary.Base64;
+
+import javax.crypto.Cipher;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+import java.nio.charset.Charset;
+import java.util.Arrays;
+import java.util.Random;
+
+/**
+ * 提供接收和推送给企业微信消息的加解密接口(UTF8编码的字符串).
+ * <ol>
+ * 	<li>第三方回复加密消息给企业微信</li>
+ * 	<li>第三方收到企业微信发送的消息,验证消息的安全性,并对消息进行解密。</li>
+ * </ol>
+ * 说明:异常java.security.InvalidKeyException:illegal Key Size的解决方案
+ * <ol>
+ * 	<li>在官方网站下载JCE无限制权限策略文件(JDK7的下载地址:
+ *      http://www.oracle.com/technetwork/java/javase/downloads/jce-7-download-432124.html</li>
+ * 	<li>下载后解压,可以看到local_policy.jar和US_export_policy.jar以及readme.txt</li>
+ * 	<li>如果安装了JRE,将两个jar文件放到%JRE_HOME%\lib\security目录下覆盖原来的文件</li>
+ * 	<li>如果安装了JDK,将两个jar文件放到%JDK_HOME%\jre\lib\security目录下覆盖原来文件</li>
+ * </ol>
+ */
+@Slf4j
+public class WXBizJsonMsgCrypt {
+    static Charset CHARSET = Charset.forName("utf-8");
+    Base64 base64 = new Base64();
+    byte[] aesKey;
+    String token;
+    String receiveid;
+
+    /**
+     * 构造函数
+     * @param token 企业微信后台,开发者设置的token
+     * @param encodingAesKey 企业微信后台,开发者设置的EncodingAESKey
+     * @param receiveid, 不同场景含义不同,详见文档
+     *
+     * @throws AesException 执行失败,请查看该异常的错误码和具体的错误信息
+     */
+    public WXBizJsonMsgCrypt(String token, String encodingAesKey, String receiveid) throws AesException {
+        if (encodingAesKey.length() != 43) {
+            throw new AesException(AesException.IllegalAesKey);
+        }
+
+        this.token = token;
+        this.receiveid = receiveid;
+        aesKey = Base64.decodeBase64(encodingAesKey + "=");
+    }
+
+    // 生成4个字节的网络字节序
+    byte[] getNetworkBytesOrder(int sourceNumber) {
+        byte[] orderBytes = new byte[4];
+        orderBytes[3] = (byte) (sourceNumber & 0xFF);
+        orderBytes[2] = (byte) (sourceNumber >> 8 & 0xFF);
+        orderBytes[1] = (byte) (sourceNumber >> 16 & 0xFF);
+        orderBytes[0] = (byte) (sourceNumber >> 24 & 0xFF);
+        return orderBytes;
+    }
+
+    // 还原4个字节的网络字节序
+    int recoverNetworkBytesOrder(byte[] orderBytes) {
+        int sourceNumber = 0;
+        for (int i = 0; i < 4; i++) {
+            sourceNumber <<= 8;
+            sourceNumber |= orderBytes[i] & 0xff;
+        }
+        return sourceNumber;
+    }
+
+    // 随机生成16位字符串
+    String getRandomStr() {
+        String base = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+        Random random = new Random();
+        StringBuffer sb = new StringBuffer();
+        for (int i = 0; i < 16; i++) {
+            int number = random.nextInt(base.length());
+            sb.append(base.charAt(number));
+        }
+        return sb.toString();
+    }
+
+    /**
+     * 对明文进行加密.
+     *
+     * @param text 需要加密的明文
+     * @return 加密后base64编码的字符串
+     * @throws AesException aes加密失败
+     */
+    String encrypt(String randomStr, String text) throws AesException {
+        ByteGroup byteCollector = new ByteGroup();
+        byte[] randomStrBytes = randomStr.getBytes(CHARSET);
+        byte[] textBytes = text.getBytes(CHARSET);
+        byte[] networkBytesOrder = getNetworkBytesOrder(textBytes.length);
+        byte[] receiveidBytes = receiveid.getBytes(CHARSET);
+
+        // randomStr + networkBytesOrder + text + receiveid
+        byteCollector.addBytes(randomStrBytes);
+        byteCollector.addBytes(networkBytesOrder);
+        byteCollector.addBytes(textBytes);
+        byteCollector.addBytes(receiveidBytes);
+
+        // ... + pad: 使用自定义的填充方式对明文进行补位填充
+        byte[] padBytes = PKCS7Encoder.encode(byteCollector.size());
+        byteCollector.addBytes(padBytes);
+
+        // 获得最终的字节流, 未加密
+        byte[] unencrypted = byteCollector.toBytes();
+
+        try {
+            // 设置加密模式为AES的CBC模式
+            Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
+            SecretKeySpec keySpec = new SecretKeySpec(aesKey, "AES");
+            IvParameterSpec iv = new IvParameterSpec(aesKey, 0, 16);
+            cipher.init(Cipher.ENCRYPT_MODE, keySpec, iv);
+
+            // 加密
+            byte[] encrypted = cipher.doFinal(unencrypted);
+
+            // 使用BASE64对加密后的字符串进行编码
+            String base64Encrypted = base64.encodeToString(encrypted);
+
+            return base64Encrypted;
+        } catch (Exception e) {
+            e.printStackTrace();
+            throw new AesException(AesException.EncryptAESError);
+        }
+    }
+
+    /**
+     * 对密文进行解密.
+     *
+     * @param text 需要解密的密文
+     * @return 解密得到的明文
+     * @throws AesException aes解密失败
+     */
+    String decrypt(String text) throws AesException {
+        log.info("密文解密-------------------------------------------------------------------》");
+        byte[] original;
+        try {
+            // 设置解密模式为AES的CBC模式
+            Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
+            SecretKeySpec key_spec = new SecretKeySpec(aesKey, "AES");
+            IvParameterSpec iv = new IvParameterSpec(Arrays.copyOfRange(aesKey, 0, 16));
+            cipher.init(Cipher.DECRYPT_MODE, key_spec, iv);
+
+            // 使用BASE64对密文进行解码
+            byte[] encrypted = Base64.decodeBase64(text);
+
+            // 解密
+            original = cipher.doFinal(encrypted);
+        } catch (Exception e) {
+            e.printStackTrace();
+            throw new AesException(AesException.DecryptAESError);
+        }
+
+        String jsonContent, from_receiveid;
+        try {
+            // 去除补位字符
+            byte[] bytes = PKCS7Encoder.decode(original);
+
+            // 分离16位随机字符串,网络字节序和receiveid
+            byte[] networkOrder = Arrays.copyOfRange(bytes, 16, 20);
+
+            int jsonLength = recoverNetworkBytesOrder(networkOrder);
+
+            jsonContent = new String(Arrays.copyOfRange(bytes, 20, 20 + jsonLength), CHARSET);
+            from_receiveid = new String(Arrays.copyOfRange(bytes, 20 + jsonLength, bytes.length),
+                    CHARSET);
+        } catch (Exception e) {
+            e.printStackTrace();
+            throw new AesException(AesException.IllegalBuffer);
+        }
+        log.info("receiveid---------------------------------------------------->" + receiveid);
+        log.info("from_receiveid---------------------------------------------------->" + from_receiveid);
+        log.info("jsonContent---------------------------------------------------->" + jsonContent);
+        // receiveid不相同的情况
+        if (!from_receiveid.equals(receiveid)) {
+            throw new AesException(AesException.ValidateCorpidError);
+        }
+        return jsonContent;
+
+    }
+
+    /**
+     * 将企业微信回复用户的消息加密打包.
+     * <ol>
+     * 	<li>对要发送的消息进行AES-CBC加密</li>
+     * 	<li>生成安全签名</li>
+     * 	<li>将消息密文和安全签名打包成json格式</li>
+     * </ol>
+     *
+     * @param replyMsg 企业微信待回复用户的消息,json格式的字符串
+     * @param timeStamp 时间戳,可以自己生成,也可以用URL参数的timestamp
+     * @param nonce 随机串,可以自己生成,也可以用URL参数的nonce
+     *
+     * @return 加密后的可以直接回复用户的密文,包括msg_signature, timestamp, nonce, encrypt的json格式的字符串
+     * @throws AesException 执行失败,请查看该异常的错误码和具体的错误信息
+     */
+    public String EncryptMsg(String replyMsg, String timeStamp, String nonce) throws AesException {
+        // 加密
+        String encrypt = encrypt(getRandomStr(), replyMsg);
+
+        // 生成安全签名
+        if (timeStamp == "") {
+            timeStamp = Long.toString(System.currentTimeMillis());
+        }
+
+        String signature = SHA1.getSHA1(token, timeStamp, nonce, encrypt);
+
+        // System.out.println("发送给平台的签名是: " + signature[1].toString());
+        // 生成发送的json
+        String result = JsonParse.generate(encrypt, signature, timeStamp, nonce);
+        return result;
+    }
+
+    /**
+     * 检验消息的真实性,并且获取解密后的明文.
+     * <ol>
+     * 	<li>利用收到的密文生成安全签名,进行签名验证</li>
+     * 	<li>若验证通过,则提取json中的加密消息</li>
+     * 	<li>对消息进行解密</li>
+     * </ol>
+     *
+     * @param msgSignature 签名串,对应URL参数的msg_signature
+     * @param timeStamp 时间戳,对应URL参数的timestamp
+     * @param nonce 随机串,对应URL参数的nonce
+     * @param postData 密文,对应POST请求的数据
+     *
+     * @return 解密后的原文
+     * @throws AesException 执行失败,请查看该异常的错误码和具体的错误信息
+     */
+    public String DecryptMsg(String msgSignature, String timeStamp, String nonce, String postData)
+            throws AesException {
+
+        // 密钥,公众账号的app secret
+        // 提取密文
+        Object[] encrypt = JsonParse.extract(postData);
+
+        // 验证安全签名
+        String signature = SHA1.getSHA1(token, timeStamp, nonce, encrypt[1].toString());
+        log.info("token--------------------------->" + token);
+        // 和URL中的签名比较是否相等
+        log.info("第三方收到URL中的签名:" + msgSignature);
+        log.info("第三方校验签名:" + signature);
+        if (!signature.equals(msgSignature)) {
+            throw new AesException(AesException.ValidateSignatureError);
+        }
+
+        // 解密
+        String result = decrypt(encrypt[1].toString());
+        return result;
+    }
+
+    /**
+     * 验证URL
+     * @param msgSignature 签名串,对应URL参数的msg_signature
+     * @param timeStamp 时间戳,对应URL参数的timestamp
+     * @param nonce 随机串,对应URL参数的nonce
+     * @param echoStr 随机串,对应URL参数的echostr
+     *
+     * @return 解密之后的echostr
+     * @throws AesException 执行失败,请查看该异常的错误码和具体的错误信息
+     */
+    public String VerifyURL(String msgSignature, String timeStamp, String nonce, String echoStr)
+            throws AesException {
+        String signature = SHA1.getSHA1(token, timeStamp, nonce, echoStr);
+
+        if (!signature.equals(msgSignature)) {
+            throw new AesException(AesException.ValidateSignatureError);
+        }
+
+        String result = decrypt(echoStr);
+        return result;
+    }
+
+}

+ 86 - 0
src/main/java/com/caimei365/wechat/entity/WeChatConstant.java

@@ -0,0 +1,86 @@
+package com.caimei365.wechat.entity;
+
+/**
+ * Description
+ *
+ * @author : Charles
+ * @date : 2022/1/13
+ */
+public class WeChatConstant {
+    /**
+     * 服务器配置 -令牌(Token)
+     */
+    public static final String TOKEN = "caimei";
+    /**
+     * 消息类型:文本
+     */
+    public static final String MESSAGE_TYPE_TEXT = "text";
+    /**
+     * 消息类型:图片
+     */
+    public static final String MESSAGE_TYPE_IMAGE = "image";
+    /**
+     * 消息类型:音频
+     */
+    public static final String MESSAGE_TYPE_VOICE = "voice";
+    /**
+     * 消息类型:视频
+     */
+    public static final String MESSAGE_TYPE_VIDEO = "video";
+    /**
+     * 消息类型:音乐
+     */
+    public static final String MESSAGE_TYPE_MUSIC = "music";
+    /**
+     * 消息类型:图文
+     */
+    public static final String MESSAGE_TYPE_NEWS = "news";
+    /**
+     * 消息类型:链接
+     */
+    public static final String MESSAGE_TYPE_LINK = "link";
+    /**
+     * 消息类型:地理位置
+     */
+    public static final String MESSAGE_TYPE_LOCATION = "location";
+    /**
+     * 消息类型:事件推送
+     */
+    public static final String MESSAGE_TYPE_EVENT = "event";
+    /**
+     * 消息类型:转发到客服
+     */
+    public static final String MESSAGE_TYPE_TRANSFER = "transfer_customer_service";
+    /**
+     * 事件类型:subscribe(订阅)
+     */
+    public static final String EVENT_TYPE_SUBSCRIBE = "subscribe";
+    /**
+     * 事件类型:unsubscribe(取消订阅)
+     */
+    public static final String EVENT_TYPE_UNSUBSCRIBE = "unsubscribe";
+    /**
+     * 事件类型:CLICK(自定义菜单点击事件)
+     */
+    public static final String EVENT_TYPE_CLICK = "CLICK";
+    /**
+     * 事件类型:LOCATION(上报地理位置事件)
+     */
+    public static final String EVENT_TYPE_LOCATION = "LOCATION";
+    /**
+     * 事件类型:SCAN(扫描事件)
+     */
+    public static final String EVENT_TYPE_SCAN = "SCAN";
+    /**
+     * xml节点
+     */
+    public static final String FROM_USER_NAME = "FromUserName";
+    public static final String TO_USER_NAME = "ToUserName";
+    public static final String MSG_TYPE = "MsgType";
+    public static final String CONTENT = "Content";
+    public static final String EVENT = "Event";
+    public static final String EVENT_KEY = "EventKey";
+    public static final String LONGITUDE = "Longitude";
+    public static final String LATITUDE = "Latitude";
+    public static final String PRECISION = "Precision";
+}

+ 26 - 0
src/main/java/com/caimei365/wechat/entity/WeChatUser.java

@@ -0,0 +1,26 @@
+package com.caimei365.wechat.entity;
+
+import lombok.Data;
+
+/**
+ * Description
+ *
+ * @author : Charles
+ * @date : 2022/3/8
+ */
+@Data
+public class WeChatUser {
+    private static final long serialVersionUID = 1L;
+    /**
+     * id
+     */
+    private Integer id;
+    /**
+     * openid
+     */
+    private String openid;
+    /**
+     * unionId
+     */
+    private String unionId;
+}

+ 38 - 0
src/main/java/com/caimei365/wechat/entity/WechatArticleDetail.java

@@ -0,0 +1,38 @@
+package com.caimei365.wechat.entity;
+
+import lombok.Data;
+
+/**
+ * Description
+ *
+ * @author : Charles
+ * @date : 2022/1/14
+ */
+@Data
+public class WechatArticleDetail {
+    private static final long serialVersionUID = 1L;
+    /**
+     * Id
+     */
+    private Integer id;
+    /**
+     * 图文Id
+     */
+    private Integer articleId;
+    /**
+     * 文章标题
+     */
+    private String title;
+    /**
+     * 跳转链接
+     */
+    private String url;
+    /**
+     * 图片链接
+     */
+    private String picUrl;
+    /**
+     * 文章内容
+     */
+    private String description;
+}

+ 38 - 0
src/main/java/com/caimei365/wechat/entity/WechatReply.java

@@ -0,0 +1,38 @@
+package com.caimei365.wechat.entity;
+
+import lombok.Data;
+
+/**
+ * Description
+ *
+ * @author : Charles
+ * @date : 2022/1/13
+ */
+@Data
+public class WechatReply {
+    private static final long serialVersionUID = 1L;
+    /**
+     * Id
+     */
+    private Integer id;
+    /**
+     * 关键字
+     */
+    private String keyword;
+    /**
+     * 回复类型
+     */
+    private String msgType;
+    /**
+     * 事件类型
+     */
+    private String responseType;
+    /**
+     * 素材id
+     */
+    private Integer relateId;
+    /**
+     * 回复素材标题
+     */
+    private String title;
+}

+ 59 - 0
src/main/java/com/caimei365/wechat/exception/AesException.java

@@ -0,0 +1,59 @@
+package com.caimei365.wechat.exception;
+
+@SuppressWarnings("serial")
+public class AesException extends Exception {
+
+	public final static int OK = 0;
+	public final static int ValidateSignatureError = -40001;
+	public final static int ParseJsonError = -40002;
+	public final static int ComputeSignatureError = -40003;
+	public final static int IllegalAesKey = -40004;
+	public final static int ValidateCorpidError = -40005;
+	public final static int EncryptAESError = -40006;
+	public final static int DecryptAESError = -40007;
+	public final static int IllegalBuffer = -40008;
+	public final static int EncodeBase64Error = -40009;
+	public final static int DecodeBase64Error = -40010;
+	public final static int GenReturnJsonError = -40011;
+
+	private int code;
+
+	private static String getMessage(int code) {
+		switch (code) {
+		case ValidateSignatureError:
+			return "签名验证错误";
+		case ParseJsonError:
+			return "json解析失败";
+		case ComputeSignatureError:
+			return "sha加密生成签名失败";
+		case IllegalAesKey:
+			return "SymmetricKey非法";
+		case ValidateCorpidError:
+			return "corpid校验失败";
+		case EncryptAESError:
+			return "aes加密失败";
+		case DecryptAESError:
+			return "aes解密失败";
+		case IllegalBuffer:
+			return "解密后得到的buffer非法";
+		case EncodeBase64Error:
+			return "base64加密错误";
+		case DecodeBase64Error:
+			return "base64解密错误";
+		case GenReturnJsonError:
+			return "josn生成失败";
+		default:
+			return null; // cannot be
+		}
+	}
+
+	public int getCode() {
+		return code;
+	}
+
+	public AesException(int code) {
+		super(getMessage(code));
+		this.code = code;
+	}
+
+}

+ 25 - 0
src/main/java/com/caimei365/wechat/service/WeChatTokenService.java

@@ -0,0 +1,25 @@
+package com.caimei365.wechat.service;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+public interface WeChatTokenService {
+
+    /**
+     * 获取代开发应用的accessToken
+     * @return
+     */
+    String getAccessToken(String suiteTicket);
+
+    /**
+     * 获取代开发应用的预授权码
+     * @return
+     */
+    String getPreAuthCode(String suiteAccessToken);
+
+    /**
+     * 获取企业永久授权码
+     * @return
+     */
+    String getPermanentCode(String suiteAccessToken, String preAuthCode);
+}

+ 221 - 0
src/main/java/com/caimei365/wechat/service/impl/WeChatTokenServiceImpl.java

@@ -0,0 +1,221 @@
+package com.caimei365.wechat.service.impl;
+
+import com.caimei365.wechat.service.WeChatTokenService;
+import com.caimei365.wechat.utils.HttpUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+@Slf4j
+@Service
+public class WeChatTokenServiceImpl implements WeChatTokenService {
+    /**
+     * 1. 获取应用的accessToken,
+     * 获取接口:https://qyapi.weixin.qq.com/cgi-bin/service/get_suite_token
+     * 官方文档:https://developer.work.weixin.qq.com/document/path/90600
+     * 请求方式:POST
+     * 参数:{
+     * "suite_id":"wwddddccc7775555aaa" ,
+     * "suite_secret": "ldAE_H9anCRN21GKXVfdAAAAAAAAAAAAAAAAAA",
+     * "suite_ticket": "Cfp0_givEagXcYJIztF6sfbdmIZCmpaR8ZBsvJEFFNBrWmnD5-CGYJ3_NhYexMyw"
+     * }
+     * 返回结果:{
+     * "errcode":0,
+     * "errmsg":"ok",
+     * "suite_access_token":"61W3mEpU66027wgNZ_MhGHNQDHnFATkDa9-2llMBjUwxRSNPbVsMmyD-yq8wZETSoE5NQgecigDrSHkPtIYA",
+     * "expires_in":7200
+     * }
+     * <p>
+     * 2.获取预授权码 pre_auth_code
+     * 获取接口:https://qyapi.weixin.qq.com/cgi-bin/service/get_pre_auth_code?suite_access_token=SUITE_ACCESS_TOKEN
+     * 官方文档:https://developer.work.weixin.qq.com/document/path/90601
+     * 请求方式:GET
+     * 返回结果:{w'q
+     * "errcode":0 ,
+     * "errmsg":"ok" ,
+     * "pre_auth_code":"Cx_Dk6qiBE0Dmx4EmlT3oRfArPvwSQ-oa3NL_fwHM7VI08r52wazoZX2Rhpz1dEw",
+     * "expires_in":1200
+     * }
+     * <p>
+     * 3.获取企业永久授权码
+     * 获取接口:https://qyapi.weixin.qq.com/cgi-bin/service/get_permanent_code?suite_access_token=SUITE_ACCESS_TOKEN
+     * 官方文档:https://developer.work.weixin.qq.com/document/path/90603
+     * 请求方式:POST
+     * 返回结果:{
+     * 	"errcode":0,
+     * 	"errmsg":"ok",
+     * 	"access_token": "xxxxxx",
+     * 	"expires_in": 7200,
+     * 	"permanent_code": "xxxx",
+     * 	"dealer_corp_info":{
+     * 		"corpid": "xxxx",
+     * 		"corp_name": "name"
+     *    },
+     * 	"auth_corp_info":{
+     * 		"corpid": "xxxx",
+     * 		"corp_name": "name",
+     * 		"corp_type": "verified",
+     * 		"corp_square_logo_url": "yyyyy",
+     * 		"corp_user_max": 50,
+     * 		"corp_full_name":"full_name",
+     * 		"verified_end_time":1431775834,
+     * 		"subject_type": 1,
+     * 		"corp_wxqrcode": "zzzzz",
+     * 		"corp_scale": "1-50人",
+     * 		"corp_industry": "IT服务",
+     * 		"corp_sub_industry": "计算机软件/硬件/信息服务"
+     *    },
+     * 	"auth_info":
+     *    {
+     * 		"agent" :
+     * 		[
+     *            {
+     * 				"agentid":1,
+     * 				"name":"NAME",
+     * 				"round_logo_url":"xxxxxx",
+     * 				"square_logo_url":"yyyyyy",
+     * 				"appid":1,
+     * 				"auth_mode":1,
+     * 				"is_customized_app":false,
+     * 				"auth_from_thirdapp":false,
+     * 				"privilege":
+     *                {
+     * 					"level":1,
+     * 					"allow_party":[1,2,3],
+     * 					"allow_user":["zhansan","lisi"],
+     * 					"allow_tag":[1,2,3],
+     * 					"extra_party":[4,5,6],
+     * 					"extra_user":["wangwu"],
+     * 					"extra_tag":[4,5,6]
+     *                },
+     * 				"shared_from":
+     *                {
+     * 					"corpid":"wwyyyyy",
+     * 					"share_type": 1
+     *                }
+     *            },
+     *            {
+     * 				"agentid":2,
+     * 				"name":"NAME2",
+     * 				"round_logo_url":"xxxxxx",
+     * 				"square_logo_url":"yyyyyy",
+     * 				"appid":5,
+     * 				"shared_from":
+     *                {
+     * 					"corpid":"wwyyyyy",
+     * 					"share_type": 0
+     *                }
+     *            }
+     * 		]
+     *    },
+     * 	"auth_user_info":
+     *    {
+     * 		"userid":"aa",
+     * 		"open_userid":"xxxxxx",
+     * 		"name":"xxx",
+     * 		"avatar":"http://xxx"
+     *    },
+     * 	"register_code_info":
+     *    {
+     * 		"register_code":"1111",
+     * 		"template_id":"tpl111",
+     * 		"state":"state001"
+     *    },
+     * 	"state":"state001"
+     * }
+     *
+     */
+
+    @Override
+    public String getAccessToken(String suiteTicket) {
+        String response = "";
+        try {
+            Map<String, String> postMap = new HashMap<>();
+            postMap.put("suite_id", "dkf01ed803e47f9a48");
+            postMap.put("suite_secret", "tiEVBsRvpcm-8RC5AixCyY6N3BcCDrrVBwpfMMvvd8Q");
+            postMap.put("suite_ticket", suiteTicket);
+            response = HttpUtil.sendPost("https://qyapi.weixin.qq.com/cgi-bin/service/get_suite_token", postMap);
+        } catch (Exception e) {
+            log.error("获取accessToken失败------------------------------------->" + e);
+            throw new RuntimeException(e);
+        }
+        return response;
+    }
+
+    @Override
+    public String getPreAuthCode(String suiteAccessToken) {
+        String preAuthCode = "";
+        try {
+            preAuthCode = HttpUtil.sendGet("https://qyapi.weixin.qq.com/cgi-bin/service/get_pre_auth_code?suite_access_token=" + suiteAccessToken);
+        } catch (Exception e) {
+            log.error("获取预授权码失败------------------------------------->" + e);
+            throw new RuntimeException(e);
+        }
+        return preAuthCode;
+    }
+
+    @Override
+    public String getPermanentCode(String suiteAccessToken, String preAuthCode) {
+        String permanentCode = "";
+        try {
+            Map<String, String> postMap = new HashMap<>();
+            postMap.put("auth_code", preAuthCode);
+            permanentCode = HttpUtil.sendPost(" https://qyapi.weixin.qq.com/cgi-bin/service/get_permanent_code?suite_access_token=" + suiteAccessToken, postMap);
+        } catch (Exception e) {
+            log.error("获取企业永久授权码------------------------------------->" + e);
+            throw new RuntimeException(e);
+        }
+        return permanentCode;
+    }
+
+    public static void unzip(String zipFilePath, String destDirectory) throws IOException {
+        File destDir = new File(destDirectory);
+        if (!destDir.exists()) {
+            destDir.mkdir();
+        }
+        ZipInputStream zipIn = new ZipInputStream(new FileInputStream(zipFilePath));
+        ZipEntry entry = zipIn.getNextEntry();
+        while (entry != null) {
+            String filePath = destDirectory + File.separator + entry.getName();
+            if (!entry.isDirectory()) {
+                extractFile(zipIn, filePath);
+            } else {
+                File dir = new File(filePath);
+                dir.mkdir();
+            }
+            zipIn.closeEntry();
+            entry = zipIn.getNextEntry();
+        }
+        zipIn.close();
+    }
+
+    private static void extractFile(ZipInputStream zipIn, String filePath) throws IOException {
+        File file = new File(filePath);
+        file.getParentFile().mkdirs();
+        try (FileOutputStream fos = new FileOutputStream(file)) {
+            byte[] bytes = new byte[4096];
+            int length;
+            while ((length = zipIn.read(bytes)) != -1) {
+                fos.write(bytes, 0, length);
+            }
+        }
+    }
+
+    public static void main(String[] args) {
+        String zipFilePath = "C:\\Users\\Administrator\\Desktop\\wcc.zip";
+        String destDirectory = "C:\\Users\\Administrator\\Desktop";
+        try {
+            unzip(zipFilePath, destDirectory);
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+}

+ 26 - 0
src/main/java/com/caimei365/wechat/utils/ByteGroup.java

@@ -0,0 +1,26 @@
+package com.caimei365.wechat.utils;
+
+import java.util.ArrayList;
+
+public class ByteGroup {
+	ArrayList<Byte> byteContainer = new ArrayList<Byte>();
+
+	public byte[] toBytes() {
+		byte[] bytes = new byte[byteContainer.size()];
+		for (int i = 0; i < byteContainer.size(); i++) {
+			bytes[i] = byteContainer.get(i);
+		}
+		return bytes;
+	}
+
+	public ByteGroup addBytes(byte[] bytes) {
+		for (byte b : bytes) {
+			byteContainer.add(b);
+		}
+		return this;
+	}
+
+	public int size() {
+		return byteContainer.size();
+	}
+}

+ 178 - 0
src/main/java/com/caimei365/wechat/utils/HttpUtil.java

@@ -0,0 +1,178 @@
+package com.caimei365.wechat.utils;
+
+import org.springframework.util.StringUtils;
+
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.TrustManager;
+import java.io.*;
+import java.net.URL;
+import java.net.URLConnection;
+import java.nio.charset.StandardCharsets;
+import java.security.cert.X509Certificate;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * HTTP 请求工具类
+ *
+ * @author : Charles
+ * @date : 2022/1/5
+ */
+public class HttpUtil {
+    public static String httpRequest(String requestUrl, String requestMethod, String outputStr) {
+        StringBuffer buffer = new StringBuffer();
+        try {
+            // 创建SSLContext对象,并使用我们指定的信任管理器初始化
+            TrustManager[] tm = {
+                    new javax.net.ssl.X509TrustManager(){
+                        @Override
+                        public void checkClientTrusted(X509Certificate[] chain, String authType) {
+                        }
+                        @Override
+                        public void checkServerTrusted(X509Certificate[] chain, String authType) {
+                        }
+                        @Override
+                        public X509Certificate[] getAcceptedIssuers() {
+                            return null;
+                        }
+                    }
+            };
+            SSLContext sslContext = SSLContext.getInstance("SSL", "SunJSSE");
+            sslContext.init(null, tm, new java.security.SecureRandom());
+            // 从上述SSLContext对象中得到SSLSocketFactory对象
+            SSLSocketFactory ssf = sslContext.getSocketFactory();
+
+            URL url = new URL(requestUrl);
+            HttpsURLConnection httpUrlConn = (HttpsURLConnection) url.openConnection();
+            httpUrlConn.setSSLSocketFactory(ssf);
+            httpUrlConn.setDoOutput(true);
+            httpUrlConn.setDoInput(true);
+            httpUrlConn.setUseCaches(false);
+            // 设置请求方式(GET/POST)
+            httpUrlConn.setRequestMethod(requestMethod);
+            if ("GET".equalsIgnoreCase(requestMethod)) {
+                httpUrlConn.connect();
+            }
+            // 当有数据需要提交时
+            if (StringUtils.hasLength(outputStr)) {
+                OutputStream outputStream = httpUrlConn.getOutputStream();
+                // 注意编码格式,防止中文乱码
+                outputStream.write(outputStr.getBytes(StandardCharsets.UTF_8));
+                outputStream.close();
+            }
+
+            // 将返回的输入流转换成字符串
+            InputStream inputStream = httpUrlConn.getInputStream();
+            InputStreamReader inputStreamReader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
+            BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
+            String str = null;
+            while ((str = bufferedReader.readLine()) != null) {
+                buffer.append(str);
+            }
+            bufferedReader.close();
+            inputStreamReader.close();
+            // 释放资源
+            inputStream.close();
+            httpUrlConn.disconnect();
+            return buffer.toString();
+        } catch (Exception ignored) {}
+        return null;
+    }
+
+    /**
+     * 向指定URL发送GET方法的请求
+     *
+     * @param url   发送请求的URL,请求参数应该是 name1=value1&name2=value2 的形式。
+     * @return 远程资源的响应结果
+     */
+    public static String sendGet(String url) throws Exception {
+        StringBuilder result = new StringBuilder();
+        BufferedReader in = null;
+        try {
+
+            URL realUrl = new URL(url);
+            // 打开和URL之间的连接
+            URLConnection connection = realUrl.openConnection();
+            // 设置通用的请求属性
+            connection.setRequestProperty("accept", "*/*");
+            connection.setRequestProperty("connection", "Keep-Alive");
+            connection.setRequestProperty("Accept-Charset", "utf-8");
+            connection.setRequestProperty("contentType", "utf-8");
+            connection.setConnectTimeout(5000);
+            // 建立实际的连接
+            connection.connect();
+            // 获取所有响应头字段
+            Map<String, List<String>> map = connection.getHeaderFields();
+            // 定义 BufferedReader输入流来读取URL的响应
+            in = new BufferedReader(new InputStreamReader(
+                    connection.getInputStream()));
+            String line;
+            while ((line = in.readLine()) != null) {
+                result.append(line);
+            }
+        }
+        // 使用finally块来关闭输入流
+        finally {
+            if (in != null) {
+                in.close();
+            }
+        }
+        return result.toString();
+    }
+
+    /**
+     * 向指定 URL 发送POST方法的请求
+     *
+     * @param url 发送请求的 URL
+     * @param paramMap 请求参数
+     * @return 所代表远程资源的响应结果
+     */
+    public static String sendPost(String url, Map<String, ?> paramMap) throws Exception{
+        PrintWriter out = null;
+        BufferedReader in = null;
+        StringBuilder result = new StringBuilder();
+
+        StringBuilder param = new StringBuilder();
+
+        for (String key : paramMap.keySet()) {
+            param.append(key).append("=").append(paramMap.get(key)).append("&");
+        }
+
+        try {
+            URL realUrl = new URL(url);
+            // 打开和URL之间的连接
+            URLConnection conn = realUrl.openConnection();
+            // 设置通用的请求属性
+            conn.setRequestProperty("accept", "*/*");
+            conn.setRequestProperty("connection", "Keep-Alive");
+            conn.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)");
+            // 发送POST请求必须设置如下两行
+            conn.setDoOutput(true);
+            conn.setDoInput(true);
+            // 获取URLConnection对象对应的输出流
+            out = new PrintWriter(conn.getOutputStream());
+            // 发送请求参数
+            out.print(param);
+            // flush输出流的缓冲
+            out.flush();
+            // 定义BufferedReader输入流来读取URL的响应
+            in = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8));
+            String line;
+            while ((line = in.readLine()) != null) {
+                result.append(line);
+            }
+        }
+        //使用finally块来关闭输出流、输入流
+        finally{
+            if(out!=null){
+                out.close();
+            }
+            if(in!=null){
+                in.close();
+            }
+        }
+        return result.toString();
+    }
+}

+ 66 - 0
src/main/java/com/caimei365/wechat/utils/JsonParse.java

@@ -0,0 +1,66 @@
+/**
+ * 对企业微信发送给企业后台的消息加解密示例代码.
+ * 
+ * @copyright Copyright (c) 1998-2020 Tencent Inc.
+ */
+
+// ------------------------------------------------------------------------
+
+package com.caimei365.wechat.utils;
+
+/**
+ * 针对 org.json.JSONObject,
+ * 要编译打包架包json
+ * 官方源码下载地址 : https://github.com/stleary/JSON-java, jar包下载地址 : https://mvnrepository.com/artifact/org.json/json
+ */
+import com.caimei365.wechat.exception.AesException;
+import org.json.JSONObject;
+
+
+/**
+ * JsonParse class
+ *
+ * 提供提取消息格式中的密文及生成回复消息格式的接口.
+ */
+public class JsonParse {
+
+	/**
+	 * 提取出 JSON 包中的加密消息
+	 * @param jsontext 待提取的json字符串
+	 * @return 提取出的加密消息字符串
+	 * @throws AesException
+	 */
+	public static Object[] extract(String jsontext) throws AesException     {
+		Object[] result = new Object[3];
+		try {
+
+			JSONObject json = new JSONObject(jsontext);    
+        	String encrypt_msg = json.getString("Encrypt");
+			String ToUserName  = json.getString("ToUserName");
+			String AgentID     = json.getString("AgentID");
+
+			result[0] = ToUserName;
+			result[1] = encrypt_msg;
+			result[2] = AgentID;
+			return result;
+		} catch (Exception e) {
+			e.printStackTrace();
+			throw new AesException(AesException.ParseJsonError);
+		}
+	}
+
+	/**
+	 * 生成json消息
+	 * @param encrypt 加密后的消息密文
+	 * @param signature 安全签名
+	 * @param timestamp 时间戳
+	 * @param nonce 随机字符串
+	 * @return 生成的json字符串
+	 */
+	public static String generate(String encrypt, String signature, String timestamp, String nonce) {
+
+		String format = "{\"encrypt\":\"%1$s\",\"msgsignature\":\"%2$s\",\"timestamp\":\"%3$s\",\"nonce\":\"%4$s\"}";
+		return String.format(format, encrypt, signature, timestamp, nonce);
+
+	}
+}

+ 128 - 0
src/main/java/com/caimei365/wechat/utils/MessageUtil.java

@@ -0,0 +1,128 @@
+package com.caimei365.wechat.utils;
+
+import com.caimei365.wechat.entity.WeChatConstant;
+import com.caimei365.wechat.entity.WechatArticleDetail;
+import lombok.extern.slf4j.Slf4j;
+import org.dom4j.Document;
+import org.dom4j.Element;
+import org.dom4j.io.SAXReader;
+import org.springframework.util.StringUtils;
+
+import javax.servlet.http.HttpServletRequest;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+
+@Slf4j
+public class MessageUtil {
+
+    /**
+     * 解析XML
+     */
+    public static Map<String, String> parseXml(HttpServletRequest request) throws Exception {
+        // 将解析结果存储在HashMap中
+        Map<String, String> map = new HashMap<>();
+        // 从request中取得输入流
+        InputStream inputStream = request.getInputStream();
+        // 读取输入流
+        SAXReader reader = new SAXReader();
+        Document document = reader.read(inputStream);
+        // 得到xml根元素
+        Element root = document.getRootElement();
+        // 得到根元素的所有子节点
+        List<Element> elementList = root.elements();
+        // 遍历所有子节点
+        for (Element e : elementList) { map.put(e.getName(), e.getText()); }
+        // 释放资源
+        inputStream.close();
+        return map;
+    }
+
+    /**
+     * 包装xml根返回消息
+     *  <xml>
+     *   <ToUserName><![CDATA[toUser]]></ToUserName>
+     *   <FromUserName><![CDATA[fromUser]]></FromUserName>
+     *   <CreateTime>1399197672</CreateTime>
+     *   <消息内容></消息内容>
+     * </xml>
+     * @return xml字符串
+     */
+    public static String setRootXml(String toUser, String fromUser, String xmlContent) {
+        return "<xml>" +
+                "<ToUserName><![CDATA[" + toUser + "]]></ToUserName>" +
+                "<FromUserName><![CDATA[" + fromUser + "]]></FromUserName>" +
+                "<CreateTime>" + System.currentTimeMillis() + "</CreateTime>" +
+                xmlContent +
+                "</xml>";
+    }
+
+    /**
+     * 将消息转发到到客服系统
+     *  <xml>
+     *   <ToUserName><![CDATA[toUser]]></ToUserName>
+     *   <FromUserName><![CDATA[fromUser]]></FromUserName>
+     *   <CreateTime>1399197672</CreateTime>
+     *   <MsgType><![CDATA[transfer_customer_service]]></MsgType>
+     * </xml>
+     */
+    public static String setCustomerXml(String toUser, String fromUser) {
+        String xmlContent = "<MsgType><![CDATA[transfer_customer_service]]></MsgType>";
+        return setRootXml(toUser, fromUser, xmlContent);
+    }
+
+    /**
+     * 包装xml文本内容
+     * <xml>
+     *   <ToUserName><![CDATA[toUser]]></ToUserName>
+     *   <FromUserName><![CDATA[fromUser]]></FromUserName>
+     *   <CreateTime>12345678</CreateTime>
+     *   <MsgType><![CDATA[text]]></MsgType>
+     *   <Content><![CDATA[你好]]></Content>
+     * </xml>
+     */
+    public static String setTextXml(String toUser, String fromUser, String content) {
+        String xmlContent = "<MsgType><![CDATA[" + WeChatConstant.MESSAGE_TYPE_TEXT + "]]></MsgType>"+
+                "<Content><![CDATA[" + content + "]]></Content>";
+        return setRootXml(toUser, fromUser, xmlContent);
+    }
+
+    /**
+     * 包装xml图文内容
+     * <xml>
+     *   <ToUserName><![CDATA[toUser]]></ToUserName>
+     *   <FromUserName><![CDATA[fromUser]]></FromUserName>
+     *   <CreateTime>12345678</CreateTime>
+     *   <MsgType><![CDATA[news]]></MsgType>
+     *   <ArticleCount>1</ArticleCount>
+     *   <Articles>
+     *     <item>
+     *       <Title><![CDATA[title1]]></Title>
+     *       <Description><![CDATA[description1]]></Description>
+     *       <PicUrl><![CDATA[picurl]]></PicUrl>
+     *       <Url><![CDATA[url]]></Url>
+     *     </item>
+     *   </Articles>
+     * </xml>
+     */
+    public static String setArticleXml(String toUser, String fromUser, List<WechatArticleDetail> articleList) {
+        StringBuilder content = new StringBuilder();
+        content.append("<MsgType><![CDATA[").append(WeChatConstant.MESSAGE_TYPE_NEWS).append("]]></MsgType>");
+        content.append("<ArticleCount><![CDATA[").append(articleList.size()).append("]]></ArticleCount>");
+        content.append("<Articles>");
+        for (WechatArticleDetail article : articleList) {
+            if (null != article) {
+                content.append("<item>");
+                content.append("<Title><![CDATA[").append(article.getTitle()).append("]]></Title>");
+                content.append("<Description><![CDATA[").append(StringUtils.hasLength(article.getDescription()) ? article.getDescription() : "").append("]]></Description>");
+                content.append("<PicUrl><![CDATA[").append(article.getPicUrl()).append("]]></PicUrl>");
+                content.append("<Url><![CDATA[").append(article.getUrl()).append("]]></Url>");
+                content.append("</item>");
+            }
+        }
+        content.append("</Articles>");
+        return setRootXml(toUser, fromUser, content.toString());
+    }
+}

+ 67 - 0
src/main/java/com/caimei365/wechat/utils/PKCS7Encoder.java

@@ -0,0 +1,67 @@
+/**
+ * 对企业微信发送给企业后台的消息加解密示例代码.
+ * 
+ * @copyright Copyright (c) 1998-2014 Tencent Inc.
+ */
+
+// ------------------------------------------------------------------------
+
+package com.caimei365.wechat.utils;
+
+import java.nio.charset.Charset;
+import java.util.Arrays;
+
+/**
+ * 提供基于PKCS7算法的加解密接口.
+ */
+public class PKCS7Encoder {
+	static Charset CHARSET = Charset.forName("utf-8");
+	static int BLOCK_SIZE = 32;
+
+	/**
+	 * 获得对明文进行补位填充的字节.
+	 * 
+	 * @param count 需要进行填充补位操作的明文字节个数
+	 * @return 补齐用的字节数组
+	 */
+	public static byte[] encode(int count) {
+		// 计算需要填充的位数
+		int amountToPad = BLOCK_SIZE - (count % BLOCK_SIZE);
+		if (amountToPad == 0) {
+			amountToPad = BLOCK_SIZE;
+		}
+		// 获得补位所用的字符
+		char padChr = chr(amountToPad);
+		String tmp = new String();
+		for (int index = 0; index < amountToPad; index++) {
+			tmp += padChr;
+		}
+		return tmp.getBytes(CHARSET);
+	}
+
+	/**
+	 * 删除解密后明文的补位字符
+	 * 
+	 * @param decrypted 解密后的明文
+	 * @return 删除补位字符后的明文
+	 */
+	public static byte[] decode(byte[] decrypted) {
+		int pad = (int) decrypted[decrypted.length - 1];
+		if (pad < 1 || pad > 32) {
+			pad = 0;
+		}
+		return Arrays.copyOfRange(decrypted, 0, decrypted.length - pad);
+	}
+
+	/**
+	 * 将数字转化成ASCII码对应的字符,用于对明文进行补码
+	 * 
+	 * @param a 需要转化的数字
+	 * @return 转化得到的字符
+	 */
+	public static char chr(int a) {
+		byte target = (byte) (a & 0xFF);
+		return (char) target;
+	}
+
+}

+ 62 - 0
src/main/java/com/caimei365/wechat/utils/SHA1.java

@@ -0,0 +1,62 @@
+/**
+ * 对企业微信发送给企业后台的消息加解密示例代码.
+ * 
+ * @copyright Copyright (c) 1998-2014 Tencent Inc.
+ */
+
+// ------------------------------------------------------------------------
+
+package com.caimei365.wechat.utils;
+
+import com.caimei365.wechat.exception.AesException;
+
+import java.security.MessageDigest;
+import java.util.Arrays;
+
+/**
+ * SHA1 class
+ *
+ * 计算消息签名接口.
+ */
+public class SHA1 {
+
+	/**
+	 * 用SHA1算法生成安全签名
+	 * @param token 票据
+	 * @param timestamp 时间戳
+	 * @param nonce 随机字符串
+	 * @param encrypt 密文
+	 * @return 安全签名
+	 * @throws AesException
+	 */
+	public static String getSHA1(String token, String timestamp, String nonce, String encrypt) throws AesException {
+		try {
+			String[] array = new String[] { token, timestamp, nonce, encrypt };
+			StringBuffer sb = new StringBuffer();
+			// 字符串排序
+			Arrays.sort(array);
+			for (int i = 0; i < 4; i++) {
+				sb.append(array[i]);
+			}
+			String str = sb.toString();
+			// SHA1签名生成
+			MessageDigest md = MessageDigest.getInstance("SHA-1");
+			md.update(str.getBytes());
+			byte[] digest = md.digest();
+
+			StringBuffer hexstr = new StringBuffer();
+			String shaHex = "";
+			for (int i = 0; i < digest.length; i++) {
+				shaHex = Integer.toHexString(digest[i] & 0xFF);
+				if (shaHex.length() < 2) {
+					hexstr.append(0);
+				}
+				hexstr.append(shaHex);
+			}
+			return hexstr.toString();
+		} catch (Exception e) {
+			e.printStackTrace();
+			throw new AesException(AesException.ComputeSignatureError);
+		}
+	}
+}

+ 72 - 0
src/main/java/com/caimei365/wechat/utils/SignUtil.java

@@ -0,0 +1,72 @@
+package com.caimei365.wechat.utils;
+
+import com.caimei365.wechat.entity.WeChatConstant;
+import lombok.extern.slf4j.Slf4j;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+
+/**
+ * Description
+ *
+ * @author : Charles
+ * @date : 2022/1/13
+ */
+@Slf4j
+public class SignUtil {
+
+    /**
+     * 加密/校验流程如下:
+     *    1)将token、timestamp、nonce三个参数进行字典序排序
+     *    2)将三个参数字符串拼接成一个字符串进行sha1加密
+     *    3)开发者获得加密后的字符串可与signature对比,标识该请求来源于微信
+     * @param signature 表示微信加密签名,signature结合了开发者填写的 token 参数和请求中的timestamp参数、nonce参数
+     * @param timestamp 表示时间戳
+     * @param nonce 表示随机数
+     */
+    public static boolean checkSignature(String signature, String timestamp, String nonce) {
+        // 微信 - 服务器配置 -令牌(Token)
+        String token = WeChatConstant.TOKEN;
+        String[] arr = new String[] {token, timestamp, nonce};
+        String tmpStr = null;
+        try {
+            // 将token、timestamp、nonce三个参数进行字典序排序
+            Arrays.sort(arr);
+            StringBuilder content = new StringBuilder();
+            for (String s : arr) {
+                content.append(s);
+            }
+            MessageDigest md = MessageDigest.getInstance("SHA-1");
+            // 将三个参数字符串拼接成一个字符串进行sha1加密
+            byte[] digest = md.digest(content.toString().getBytes());
+            tmpStr = byteToStr(digest);
+        } catch (NoSuchAlgorithmException e) {
+            log.error(e.toString());
+        }
+        // 将sha1加密后的字符串可与signature对比,标识该请求来源于微信
+        return tmpStr != null && tmpStr.equals(signature.toUpperCase());
+    }
+
+    /**
+     * 将字节数组转换为十六进制字符串
+     */
+    private static String byteToStr(byte[] byteArray) {
+        StringBuilder strDigest = new StringBuilder();
+        for (byte b : byteArray) {
+            strDigest.append(byteToHexStr(b));
+        }
+        return strDigest.toString();
+    }
+
+    /**
+     * 将字节转换为十六进制字符串
+     */
+    private static String byteToHexStr(byte mByte) {
+        char[] digit = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };
+        char[] tempArr = new char[2];
+        tempArr[0] = digit[(mByte >>> 4) & 0X0F];
+        tempArr[1] = digit[mByte & 0X0F];
+        return new String(tempArr);
+    }
+}

+ 25 - 0
src/main/resources/application.yml

@@ -0,0 +1,25 @@
+server:
+  port: 38998
+  compression:
+    enabled: true
+    mime-types: application/json,application/xml,text/xml,text/plain,text/html,text/javascript,text/css
+  servlet:
+    encoding:
+      charset: UTF-8
+
+spring:
+  application:
+    name: @artifactId@
+  profiles:
+    active: @activatedProperties@
+  thymeleaf:
+    enabled: true  #开启thymeleaf视图解析
+    encoding: utf-8  #编码
+    prefix: classpath:/templates/  #前缀
+    mode: HTML5  #严格的HTML语法模式
+    suffix: .html  #后缀名
+    reactive:
+      max-chunk-size: 8192
+
+mybatis:
+  mapper-locations: classpath:mapper/*.xml

+ 26 - 0
src/main/resources/config/beta/application-beta.yml

@@ -0,0 +1,26 @@
+spring:
+  #数据源连接--start
+  datasource:
+    url: jdbc:mysql://172.31.165.28:3306/caimei?characterEncoding=UTF8&serverTimezone=Asia/Shanghai
+    username: developer
+    password: J5p3tgOVazNl4ydf
+    # Hikari will use the above plus the following to setup connection pooling
+    type: com.zaxxer.hikari.HikariDataSource
+    hikari:
+      minimum-idle: 5
+      maximum-pool-size: 15
+      auto-commit: true
+      idle-timeout: 30000
+      pool-name: DatebookHikariCP
+      max-lifetime: 1800000
+      connection-timeout: 30000
+      connection-test-query: SELECT 1
+  #数据源连接--end
+
+#日志配置
+logging:
+  file:
+    path: /mnt/newdatadrive/data/runtime/jar-instance/wechat/logs
+  level:
+    root: info
+

+ 25 - 0
src/main/resources/config/dev/application-dev.yml

@@ -0,0 +1,25 @@
+spring:
+  #数据源连接--start
+  datasource:
+    #本地连接数据库
+    url: jdbc:mysql://192.168.2.100:3306/caimei?characterEncoding=UTF8&serverTimezone=Asia/Shanghai
+    username: developer
+    password: 05bZ/OxTB:X+yd%1
+    type: com.zaxxer.hikari.HikariDataSource
+    hikari:
+      minimum-idle: 5
+      maximum-pool-size: 15
+      auto-commit: true
+      idle-timeout: 30000
+      pool-name: DatebookHikariCP
+      max-lifetime: 1800000
+      connection-timeout: 30000
+      connection-test-query: SELECT 1
+  #数据源连接--end
+
+
+#日志配置
+logging:
+  level:
+    root: info
+    com.caimei365.wechat.dao: debug

+ 25 - 0
src/main/resources/config/prod/application-prod.yml

@@ -0,0 +1,25 @@
+spring:
+  #数据源连接--start
+  datasource:
+    url: jdbc:mysql://rm-wz928s8btl7kxil44.mysql.rds.aliyuncs.com:3306/caimei?characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true
+    username: coder
+    password: diKtPYZ%wToI&9#L
+    # Hikari will use the above plus the following to setup connection pooling
+    type: com.zaxxer.hikari.HikariDataSource
+    hikari:
+      minimum-idle: 5
+      maximum-pool-size: 15
+      auto-commit: true
+      idle-timeout: 30000
+      pool-name: DatebookHikariCP
+      max-lifetime: 1800000
+      connection-timeout: 30000
+      connection-test-query: SELECT 1
+  #数据源连接--end
+
+#日志配置
+logging:
+  file:
+    path: /mnt/newdatadrive/data/runtime/jar-instance/wechat/logs
+  level:
+    root: info

+ 10 - 0
src/main/resources/templates/index.html

@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <title>测试</title>
+</head>
+<body>
+    <h1>测试页面</h1>
+</body>
+</html>