Java接入微信公众号实现验证码的自动回复,以及验证码缓存到redis,natapp内网穿透

目录

一、概述

二、相关开发文档

三、回调消息接入 

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、微信公众号配置

 五、测试

点击关注公众号:

输入“验证码”:

都是没有问题的

  • 7
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值