目录

一、概述

二、相关开发文档

三、回调消息接入 

3.1、pom文件配置 

 3.2、application配置

3.3、启动类

3.4、Controller层代码

3.5、Utils层

3.5.1、SHA1算法生成安全签名

3.5.2、解析微信发来的请求

 3.6、handle层

3.6.1、消息类型枚举

 3.6.2、微信聊天消息处理接口

3.6.3、文本消息处理 

 3.6.4、用户关注事件

 3.6.5、消息处理工厂

 3.7、redis层

3.7.1、Redis的config处理

3.7.2、 RedisUtil工具类

四、内网穿透

4.1、配置内网穿透 

4.2、启动 natapp

 4.3、启动项目

4.3、微信公众号配置

 五、测试


一、概述

前言:整体采取个人号的登录模式,选取微信号的 openId 作为用户的唯一标识

整体流程:用户扫公众号码。然后发一条消息:验证码。我们通过 api 回复一个随机的码。存入 redis

redis 的主要结构,就是 openId 加验证码

二、相关开发文档

测试号地址:微信公众平台

公众号开发文档:微信公众平台开发概述 | 微信开放文档

回调消息接入指南:接入概述 | 微信开放文档

接收公众号消息体文档:文本消息 | 微信开放文档

三、回调消息接入 

3.1、pom文件配置 

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.zzz</groupId>
    <artifactId>zzz-wx</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <spring-boot.version>2.4.2</spring-boot.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <exclusions>
                <exclusion>
                    <artifactId>spring-boot-starter-logging</artifactId>
                    <groupId>org.springframework.boot</groupId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.26</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-log4j2</artifactId>
        </dependency>
        <dependency>
            <groupId>com.thoughtworks.xstream</groupId>
            <artifactId>xstream</artifactId>
            <version>1.4.18</version>
        </dependency>
        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
            <version>2.8.5</version>
        </dependency>
        <dependency>
            <groupId>org.dom4j</groupId>
            <artifactId>dom4j</artifactId>
            <version>2.1.1</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
            <version>2.12.7</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.12.7</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
            <version>2.9.0</version>
        </dependency>

    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <repositories>
        <repository>
            <id>central</id>
            <name>aliyun maven</name>
            <url>http://maven.aliyun.com/nexus/content/groups/public/</url>
            <layout>default</layout>
            <releases>
                <enabled>true</enabled>
            </releases>
            <snapshots>
                <enabled>true</enabled>
            </snapshots>
        </repository>
    </repositories>
    <build>
        <finalName>${project.artifactId}</finalName>
        <!--打包成jar包时的名字-->
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>2.3.0.RELEASE</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>


</project>

 3.2、application配置

server:
  port: 8080
spring:
  redis:
    # Redis数据库索引(默认为0)
    database: 1
    # Redis服务器地址
    host: ip地址
    # Redis服务器连接端口
    port: 6379
    # Redis服务器连接密码(默认为空)
    password: 123456
    # 连接超时时间
    timeout: 2s
    lettuce:
      pool:
        # 连接池最大连接数
        max-active: 200
        # 连接池最大阻塞等待时间(使用负值表示没有限制)
        max-wait: -1ms
        # 连接池中的最大空闲连接
        max-idle: 10
        # 连接池中的最小空闲连接
        min-idle: 0

3.3、启动类

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;

/**
 * WxApplication 微信服务启动器
 */
@SpringBootApplication
@ComponentScan("com.zzz")
public class WxApplication {
    public static void main(String[] args) {
        SpringApplication.run(WxApplication.class, args);
    }
}

3.4、Controller层代码

/**
 * @description: 微信回调controller
 **/
@RestController
@Slf4j
public class CallBackController {

    @Resource
    private WxChatMsgFactory wxChatMsgFactory;
    private static final String token = "adwidhaidwoaid";

    /**
     * 回调消息校验
     */
    @GetMapping("callback")
    public String callback(@RequestParam("signature") String signature,
                           @RequestParam("timestamp") String timestamp,
                           @RequestParam("nonce") String nonce,
                           @RequestParam("echostr") String echostr) {
        log.info("get验签请求参数:signature:{},timestamp:{},nonce:{},echostr:{}",
                signature, timestamp, nonce, echostr);
        //SHA1加密
        String shaStr = SHA1.getSHA1(token, timestamp, nonce, "");
        //签名校验
        if (signature.equals(shaStr)) {
            return echostr;
        }
        return "unknown";
    }



    /**
     * 处理微信回调请求的方法。
     *
     * @param requestBody 请求体中的XML字符串,包含了微信发送的消息内容。
     * @param signature 微信加密签名,用于验证消息的真实性。
     * @param timestamp 时间戳,用于验证消息的时间有效性。
     * @param nonce 随机数,用于防止网络重放攻击。
     * @param msgSignature 可选参数,消息签名,用于验证消息的真实性。
     * @return 返回处理后的响应内容,通常为XML格式的消息。
     */
    @PostMapping(value = "callback", produces = "application/xml;charset=UTF-8")
    public String callback(
            @RequestBody String requestBody,
            @RequestParam("signature") String signature,
            @RequestParam("timestamp") String timestamp,
            @RequestParam("nonce") String nonce,
            @RequestParam(value = "msg_signature", required = false) String msgSignature) {
        log.info("接收到微信消息:requestBody:{}", requestBody);



        // 解析XML消息,将其转换为Map形式
        Map<String, String> messageMap = MessageUtil.parseXml(requestBody);
        // 获取消息类型
        String msgType = messageMap.get("MsgType");
        // 获取事件类型,如果不存在则默认为空字符串
        String event = messageMap.get("Event") == null ? "" : messageMap.get("Event");
        log.info("msgType:{},event:{}", msgType, event);



        // 构建消息类型键,用于从消息处理器工厂获取相应的消息处理器
        StringBuilder sb = new StringBuilder();
        sb.append(msgType);
        if (!StringUtils.isEmpty(event)) {
            sb.append(".");
            sb.append(event);
        }
        String msgTypeKey = sb.toString();
        // 根据消息类型获取相应的消息处理器
        WxChatMsgHandler wxChatMsgHandler = wxChatMsgFactory.getHandlerByMsgType(msgTypeKey);
        if (Objects.isNull(wxChatMsgHandler)) {
            return "unknown";
        }
        // 处理消息,并获取回复内容
        String replyContent = wxChatMsgHandler.dealMsg(messageMap);
        log.info("replyContent:{}", replyContent);
        return replyContent;
    }


}

3.5、Utils层

3.5.1、SHA1算法生成安全签名

import lombok.extern.slf4j.Slf4j;

import java.security.MessageDigest;
import java.util.Arrays;

/**
 * sha1生成签名工具
 */
@Slf4j
public class SHA1 {

    /**
     * 用SHA1算法生成安全签名
     *
     * @param token     票据
     * @param timestamp 时间戳
     * @param nonce     随机字符串
     * @param encrypt   密文
     * @return 安全签名
     */
    public static String getSHA1(String token, String timestamp, String nonce, String encrypt) {
        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) {
            log.error("sha加密生成签名失败:", e);
            return null;
        }
    }
}

3.5.2、解析微信发来的请求

/**
 * @Description: 解析微信发来的请求(XML)
 */
public class MessageUtil {

    /**
     * 解析微信发来的请求(XML).
     *
     * @param msg 消息
     * @return map
     */
    public static Map<String, String> parseXml(final String msg) {
        // 将解析结果存储在HashMap中
        Map<String, String> map = new HashMap<String, String>();

        // 从request中取得输入流
        try (InputStream inputStream = new ByteArrayInputStream(msg.getBytes(StandardCharsets.UTF_8.name()))) {
            // 读取输入流
            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());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        return map;
    }

}

 3.6、handle层

3.6.1、消息类型枚举

/**
 * 消息类型枚举
 */
public enum WxChatMsgTypeEnum {

    SUBSCRIBE("event.subscribe", "用户关注事件"),
    TEXT_MSG("text", "接收用户文本消息");

    private String msgType;

    private String desc;

    WxChatMsgTypeEnum(String msgType, String desc) {
        this.msgType = msgType;
        this.desc = desc;
    }

    public static WxChatMsgTypeEnum getByMsgType(String msgType) {
        for (WxChatMsgTypeEnum wxChatMsgTypeEnum : WxChatMsgTypeEnum.values()) {
            if (wxChatMsgTypeEnum.msgType.equals(msgType)) {
                return wxChatMsgTypeEnum;
            }
        }
        return null;
    }

}

 3.6.2、微信聊天消息处理接口

/**
 * 微信聊天消息处理接口。
 * 该接口定义了处理微信聊天消息的基本方法,包括识别消息类型和处理消息。
 */
public interface WxChatMsgHandler {
    /**
     * 获取消息类型。
     *
     * @return 返回消息类型的枚举值,用于识别接收到的是哪种类型的消息。
     */
    WxChatMsgTypeEnum getMsgType();
    /**
     * 处理消息。
     * 该方法具体处理接收到的微信聊天消息,并返回处理结果。
     *
     * @param messageMap 包含消息详细信息的Map,键值对形式,用于获取消息的具体内容。
     * @return 返回处理消息后的结果,通常为字符串形式的消息回复内容。
     */
    String dealMsg(Map<String, String> messageMap);

}

3.6.3、文本消息处理 

/**
 * 接收文本消息处理
 */
@Component
@Slf4j
public class ReceiveTextMsgHandler implements WxChatMsgHandler {
    // 关键字,用于识别是否为需要处理的验证码信息
    private static final String KEY_WORD = "验证码";
    // 验证码在Redis中的key前缀
    private static final String LOGIN_PREFIX = "loginCode";

    @Resource
    private RedisUtil redisUtil;
    /**
     * 获取消息类型。
     * @return 返回消息类型枚举,本类处理的是文本消息。
     */
    @Override
    public WxChatMsgTypeEnum getMsgType() {
        return WxChatMsgTypeEnum.TEXT_MSG;
    }
    /**
     * 处理接收到的消息。
     * @param messageMap 包含消息内容等信息的map
     * @return 返回处理后的回复消息内容,如果不需要回复则返回空字符串。
     */
    @Override
    public String dealMsg(Map<String, String> messageMap) {
        log.info("接收到文本消息事件");
        // 获取消息内容
        String content = messageMap.get("Content");
        // 如果消息内容不是关键字,则不处理
        if (!KEY_WORD.equals(content)) {
            return "";
        }

        // 处理包含关键字的消息
        String fromUserName = messageMap.get("FromUserName"); // 发送方用户名
        String toUserName = messageMap.get("ToUserName");// 接收方用户名

        // 生成随机验证码
        Random random = new Random();
        int num = random.nextInt(1000);
        // 在Redis中存储验证码,并设置过期时间
        String numKey = redisUtil.buildKey(LOGIN_PREFIX, String.valueOf(num));
        redisUtil.setNx(numKey, fromUserName, 5L, TimeUnit.MINUTES);

        // 构造回复消息
        String numContent = "您当前的验证码是:" + num + ", 5分钟内有效";
        String replyContent = "<xml>\n" +
                "  <ToUserName><![CDATA[" + fromUserName + "]]></ToUserName>\n" +
                "  <FromUserName><![CDATA[" + toUserName + "]]></FromUserName>\n" +
                "  <CreateTime>12345678</CreateTime>\n" +
                "  <MsgType><![CDATA[text]]></MsgType>\n" +
                "  <Content><![CDATA[" + numContent + "]]></Content>\n" +
                "</xml>";

        return replyContent;
    }

}

 3.6.4、用户关注事件

/**
 * @description: 用户关注事件
 */
@Component
@Slf4j
public class SubscribeMsgHandler implements WxChatMsgHandler {
    /**
     * 获取消息类型。
     *
     * @return 返回消息类型枚举,本例中为订阅类型。
     */
    @Override
    public WxChatMsgTypeEnum getMsgType() {
        return WxChatMsgTypeEnum.SUBSCRIBE;
    }
    /**
     * 处理用户订阅消息。
     *
     * @param messageMap 包含微信消息关键参数的Map,如FromUserName、ToUserName等。
     * @return 返回处理后的消息内容,格式为XML字符串。
     */
    @Override
    public String dealMsg(Map<String, String> messageMap) {
        log.info("触发用户关注事件!");
        // 获取发送方用户名
        String fromUserName = messageMap.get("FromUserName");
        // 获取接收方用户名
        String toUserName = messageMap.get("ToUserName");
        // 定义回复用户的消息内容
        String subscribeContent = "感谢您的关注,我是叶思远!欢迎来学习有趣的知识";
        // 构造回复消息的XML格式字符串
        String content = "<xml>\n" +
                "  <ToUserName><![CDATA[" + fromUserName + "]]></ToUserName>\n" +
                "  <FromUserName><![CDATA[" + toUserName + "]]></FromUserName>\n" +
                "  <CreateTime>12345678</CreateTime>\n" +
                "  <MsgType><![CDATA[text]]></MsgType>\n" +
                "  <Content><![CDATA[" + subscribeContent + "]]></Content>\n" +
                "</xml>";
        return content;
    }

}

 3.6.5、消息处理工厂

/**
 * 消息处理工厂
 */
@Component
public class WxChatMsgFactory implements InitializingBean {
    // 注入所有WxChatMsgHandler实现列表
    @Resource
    private List<WxChatMsgHandler> wxChatMsgHandlerList;
    // 使用Map以消息类型为键,处理器为值,进行存储
    private Map<WxChatMsgTypeEnum, WxChatMsgHandler> handlerMap = new HashMap<>();
    /**
     * 根据消息类型获取对应的处理器
     *
     * @param msgType 消息类型字符串
     * @return 对应消息类型的处理器,如果找不到则返回null
     */
    public WxChatMsgHandler getHandlerByMsgType(String msgType) {
        WxChatMsgTypeEnum msgTypeEnum = WxChatMsgTypeEnum.getByMsgType(msgType);
        return handlerMap.get(msgTypeEnum);
    }
    /**
     * 初始化方法,配置完成后调用,用于将所有处理器按消息类型注册到处理器映射中
     */
    @Override
    public void afterPropertiesSet() throws Exception {
        // 遍历处理器列表,将每个处理器按其处理的消息类型注册到映射中
        for (WxChatMsgHandler wxChatMsgHandler : wxChatMsgHandlerList) {
            handlerMap.put(wxChatMsgHandler.getMsgType(), wxChatMsgHandler);
        }
    }

}

 3.7、redis层

3.7.1、Redis的config处理

/**
 * Redis的config处理
 */
@Configuration
public class RedisConfig {
    /**
     * 创建并配置RedisTemplate,用于操作Redis数据库。
     *
     * @param redisConnectionFactory Redis连接工厂,用于创建Redis连接。
     * @return 配置好的RedisTemplate实例,可以用于执行Redis操作。
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        // 配置Redis连接工厂
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        // 设置键的序列化方式为StringRedisSerializer
        redisTemplate.setKeySerializer(redisSerializer);
        // 设置哈希键的序列化方式为StringRedisSerializer
        redisTemplate.setHashKeySerializer(redisSerializer);
        // 设置值的序列化方式为Jackson2JsonRedisSerializer
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer());
        // 设置哈希值的序列化方式为Jackson2JsonRedisSerializer
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer());
        return redisTemplate;
    }
    /**
     * 创建并配置Jackson2JsonRedisSerializer,用于将Java对象序列化为JSON格式存储到Redis中。
     *
     * @return 配置好的Jackson2JsonRedisSerializer实例。
     */
    private Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer() {
        Jackson2JsonRedisSerializer<Object> jsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper objectMapper = new ObjectMapper();

        // 配置ObjectMapper,以便于序列化和反序列化时使用
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
        // 将配置好的ObjectMapper设置到Jackson2JsonRedisSerializer中
        jsonRedisSerializer.setObjectMapper(objectMapper);
        return jsonRedisSerializer;
    }

}

3.7.2、 RedisUtil工具类

/**
 * RedisUtil工具类
 */
@Component
@Slf4j
public class RedisUtil {

    @Resource
    private RedisTemplate redisTemplate;

    private static final String CACHE_KEY_SEPARATOR = ".";

    /**
     * 构建缓存key
     */
    public String buildKey(String... strObjs) {
        return Stream.of(strObjs).collect(Collectors.joining(CACHE_KEY_SEPARATOR));
    }

    /**
     * 是否存在key
     */
    public boolean exist(String key) {
        return redisTemplate.hasKey(key);
    }

    /**
     * 删除key
     */
    public boolean del(String key) {
        return redisTemplate.delete(key);
    }

    /**
     * set(不带过期)
     */
    public void set(String key, String value) {
        redisTemplate.opsForValue().set(key, value);
    }

    /**
     * set(带过期)
     */
    public boolean setNx(String key, String value, Long time, TimeUnit timeUnit) {
        return redisTemplate.opsForValue().setIfAbsent(key, value, time, timeUnit);
    }

    /**
     * 获取string类型缓存
     */
    public String get(String key) {
        return (String) redisTemplate.opsForValue().get(key);
    }

    public Boolean zAdd(String key, String value, Long score) {
        return redisTemplate.opsForZSet().add(key, value, Double.valueOf(String.valueOf(score)));
    }

    public Long countZset(String key) {
        return redisTemplate.opsForZSet().size(key);
    }

    public Set<String> rangeZset(String key, long start, long end) {
        return redisTemplate.opsForZSet().range(key, start, end);
    }

    public Long removeZset(String key, Object value) {
        return redisTemplate.opsForZSet().remove(key, value);
    }

    public void removeZsetList(String key, Set<String> value) {
        value.stream().forEach((val) -> redisTemplate.opsForZSet().remove(key, val));
    }

    public Double score(String key, Object value) {
        return redisTemplate.opsForZSet().score(key, value);
    }

    public Set<String> rangeByScore(String key, long start, long end) {
        return redisTemplate.opsForZSet().rangeByScore(key, Double.valueOf(String.valueOf(start)), Double.valueOf(String.valueOf(end)));
    }

    public Object addScore(String key, Object obj, double score) {
        return redisTemplate.opsForZSet().incrementScore(key, obj, score);
    }

    public Object rank(String key, Object obj) {
        return redisTemplate.opsForZSet().rank(key, obj);
    }


}

四、内网穿透

下载地址:NATAPP-内网穿透 基于ngrok的国内高速内网映射工具
 

内网穿透使用指南:

NATAPP1分钟快速新手图文教程 - NATAPP-内网穿透 基于ngrok的国内高速内网映射工具

4.1、配置内网穿透 

4.2、启动 natapp

去natapp.exe目录下执行以下命令

start natapp -authtoken=xxxx

 4.3、启动项目

是可以访问到的

4.3、微信公众号配置

 五、测试

点击关注公众号:

输入“验证码”:

都是没有问题的

更多推荐