目录
一、概述
前言:整体采取个人号的登录模式,选取微信号的 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的国内高速内网映射工具
内网穿透使用指南:
4.1、配置内网穿透
4.2、启动 natapp
去natapp.exe目录下执行以下命令
start natapp -authtoken=xxxx
4.3、启动项目
是可以访问到的
4.3、微信公众号配置
五、测试
点击关注公众号:
输入“验证码”:
都是没有问题的