一、公众号开发初探
这里会使用到自己的域名进行交互,没有域名的小伙伴可以使用 内网穿透(NATAPP), 如果没有使用过的的同学请移步 20 秒轻松上手 NATAAPP (内网穿透)
公众号整体流程:用户扫公众号二维码。然后发一条消息:验证码。我们通过 api 回复一个随机的验证码,并且存入 redis。用户在验证码框输入之后,点击登录,进入我们的注册模块,同时关联角色和权限。就实现了网关的统一鉴权。
二、微信公众平台使用(测试号的申请)
首先我们进入微信公众平台首页,这里我们选用订阅号,点击开发文档
然后按照图中位置,进入测试号申请
申请完成后,我们会进入这样的页面,这样就成功啦,大家可以扫自己的测试二维码试一下
三、服务端环境搭建
首先,导入本次项目所需的pom依赖
<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.16</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>
然后新建applicationyml,配置端口和 redis
server:
port: 3012
spring:
redis:
# Redis数据库索引(默认为0)
database: 1
# Redis服务器地址
host: 123.60.123.123
# Redis服务器连接端口
port: 6379
# Redis服务器连接密码(默认为空)
password: 123456
# 连接超时时间
timeout: 2s
lettuce:
pool:
# 连接池最大连接数
max-active: 200
# 连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1ms
# 连接池中的最大空闲连接
max-idle: 10
# 连接池中的最小空闲连接
min-idle: 0
这里给大家补充一点知识:
1、因为微信公众平台他定义的参数是xml形式的数据,无论是请求还是回调,所以为了方便,这里我写了一个工具类将xml文件进行了一次转换
/**
* 解析微信发来的请求(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;
}
2、开发者可选择消息加解密方式:明文模式、兼容模式和安全模式。模式的选择与服务器配置在提交后都会立即生效,大家谨慎填写及选择。加解密方式的默认状态为明文模式,选择兼容模式和安全模式需要提前配置好相关加解密代码,详情请参考消息体签名及加解密部分的文档。这里为了方便我也写了一个抽象类进行加密操作进行签名的加密。
/**
* 用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;
}
}
然后我们新建一个接口,分别用来回调消息校验和被动消息回复,来先给大家测试一下我们的申请是否成功访问到微信公众平台。
/**
* 回调消息校验
*/
@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);
String shaStr = SHA1.getSHA1(token, timestamp, nonce, "");
if (signature.equals(shaStr)) {
return echostr;
}
return "unknown";
}
/**
* 被动消息回复
*/
@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:{},signature:{},timestamp:{},nonce:{}"
, requestBody, signature, timestamp, nonce);
Map<String, String> messageMap = MessageUtil.parseXml(requestBody);
String fromUserName = messageMap.get("FromUserName");
String toUserName = messageMap.get("ToUserName");
String msg = "<xml>\n" +
" <ToUserName><![CDATA[" + toUserName + "]]></ToUserName>\n" +
" <FromUserName><![CDATA[" + fromUserName + "]]></FromUserName>\n" +
" <CreateTime>12345678</CreateTime>\n" +
" <MsgType><![CDATA[text]]></MsgType>\n" +
" <Content><![CDATA[测试,被动回复消息]]></Content>\n" +
"</xml>";
return msg;
}
如果大家配置正确,应该能得到下面的结果
四、整合工厂+策略模式 +redis进行二次改造
如果不熟悉策略+工厂模式的小伙伴,请移步 > 策略 + 工厂 + springboot实战结合
首先新建一个策略的枚举类,用来做登陆状态识别
/**
* 公众号登录状态识别
*/
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;
}
}
然后新建一个公众号策略接口
/**
* 策略接口
*/
public interface WxChatMsgHandler {
WxChatMsgTypeEnum getMsgType();
String dealMsg(Map<String, String> messageMap);
}
新建一个公众号工厂类
/**
* 公众号工厂
*/
@Component
public class WxChatMsgFactory implements InitializingBean {
@Resource
private List<WxChatMsgHandler>wxChatMsgHandlerList;
private Map<WxChatMsgTypeEnum,WxChatMsgHandler> handlerMap = new HashMap<>();
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);
}
}
}
然后,我们需要新建两个策略去实现我们的策略接口,这两个策略分别对应的处理:
1、关注事件:当用户关注时,我们应该返回这里是XXX公众号
2、验证码回复:当用户发送验证码时,我们应该捕获用户发送的信息,并进行比对,如果是我们定义的关键字,那么我们返回一个随机验证码并存入redis中返回
关注事件
/**
* 关注事件文本
*/
@Component
@Slf4j
public class SubscribeMsgHandler implements WxChatMsgHandler{
@Override
public WxChatMsgTypeEnum getMsgType() {
return WxChatMsgTypeEnum.SUBSCRIBE;
}
@Override
public String dealMsg(Map<String, String> messageMap) {
log.info("接收到关注事件");
String fromUserName = messageMap.get("FromUserName");
String toUserName = messageMap.get("ToUserName");
String content = "你好,这里是布川ku子的博客,欢迎大家留言评论!";
String msg = "<xml>\n" +
" <ToUserName><![CDATA[" + fromUserName + "]]></ToUserName>\n" +
" <FromUserName><![CDATA[" + toUserName + "]]></FromUserName>\n" +
" <CreateTime>12345678</CreateTime>\n" +
" <MsgType><![CDATA[text]]></MsgType>\n" +
" <Content><![CDATA["+content+"]]></Content>\n" +
"</xml>";
return msg;
}
}
验证码回复
/**
* 接收文本策略
*/
@Component
@Slf4j
public class ReceiveTextMsgHandler implements WxChatMsgHandler{
private static final String KEY_WORD = "验证码";
private static final String LOGIN_PREFIX = "loginCode";
@Override
public WxChatMsgTypeEnum getMsgType() {
return WxChatMsgTypeEnum.TEXT_MSG;
}
@Resource
private RedisUtil redisUtil;
@Override
public String dealMsg(Map<String, String> messageMap) {
log.info("接收到文本消息事件");
String context = messageMap.get("Content");
if (!KEY_WORD.equals(context)){
return "";
}
String fromUserName = messageMap.get("FromUserName");
String toUserName = messageMap.get("ToUserName");
Random random = new Random();
int num = random.nextInt(235234);
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;
}
}
然后我们给我们写好的callback接口加一些新的功能,重写一下
/**
* 公众号接口
*/
@RestController
@Slf4j
public class CallBackController {
private final String token = "buchuankuzi";
@Resource
private WxChatMsgFactory wxChatMsgFactory;
/**
* 回调消息校验
*/
@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);
String shaStr = SHA1.getSHA1(token, timestamp, nonce, "");
if (signature.equals(shaStr)) {
return echostr;
}
return "unknown";
}
/**
*
*/
@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);
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 handlerByMsgType = wxChatMsgFactory.getHandlerByMsgType(msgTypeKey);
String replyContent = handlerByMsgType.dealMsg(messageMap);
return replyContent;
}
}
到此,我们的公众号验证码发送就完成啦,我们再次扫码验证一下,出现下面这个界面就代表成功啦!
博主主页还有很多有趣的知识分享,大家感兴趣请移步主页!
标签:nonce,return,springboot,--,微信,signature,timestamp,public,String From: https://blog.csdn.net/indexqian/article/details/141068889