目录
大家好,我是jstart千语。现在主流的登录注册方式有短信验证登录、邮箱登录、微信扫码登录。但这些方式对于初学者来说成本过于高,比如短信登录需要和云平台对接,邮箱登录的配置也很麻烦。所以接下来我介绍一种基于微信公众号的方式进行登录注册的实现,这是一种成本很低的登录方式,只需要申请一个公众号认证即可,在开发阶段也可以申请一个测试的公众号,开发起来非常方便。
主要流程就是:用户关注公众号——>服务器端获取到用户唯一的openId——>用户给公众号发送“验证码”——>后端通过公众号给客户端发送合法的验证码——>用户输入验证码——>后端验证——>拿用户唯一的openId作为账号进行自动注册,给用户登录
这里其实用户关注公众号后就可以直接拿到openId,然后直接给用户登录或者注册了,这里为了让大家更好地理解公众号的对接流程(文本发送之类的),所以还采用了一个验证码的流程。 接下来主要涉及到与微信对接、监听公众号的状态、内网穿透。
前期准备
1、公众号申请
首先进入微信公众平台:微信公众平台https://mp.weixin.qq.com/
(1)右上角点击立即注册
官方文档指引:(个人)注册公众平台步骤 - 腾讯客服
(2)选择公众号
(3)按照流程一步一步注册申请即可:
2、测试号申请
(1)进入公众号开发者的文档:开发前必读 / 首页
(2)申请测试号:微信公众平台
3、填写微信服务的基本配置信息、
官方文档其实写的不太行,没有java版的:开始开发 / 接入指南
服务开发
1、接入微信
上面的url其中一个用处就是用来与微信接入的,但是这个url需要是公网上微信能访问的,在开发阶段项目肯定还没有上线的,所以可以使用内网穿透,内网穿透下面有讲。
(1)对接时:微信会对这个路径发送get请求(关注、文本消息是发送post请求到这个url)
直接自定义一个接口,注意请求路径uri要保持与上面设置的一致:
官方文档:开始开发 / 接入指南
//微信token、需要与公众号上配置的一致
private static final String token = "da4gdf5";
@GetMapping("/wx/callBack")
public String callback(@RequestParam("signature") String signature,
@RequestParam("timestamp") String timestamp,
@RequestParam("nonce") String nonce,
@RequestParam("echostr") String echostr) {
log.info("请求参数:signature:{};timestamp:{};nonce:{};echostr:{};",
signature, timestamp, nonce, echostr);
String sha1Str = SHA1.generateSHA1Signature(token, timestamp, nonce);
log.info("sha1Str:{}", sha1Str);
if (signature.equals(sha1Str)) {
return echostr;
}
return "unknown";
}
可以看到微信的get请求会发送四个请求参数,分别有什么作用呢
对接流程:
1、将token、timestamp、nonce三个参数进行字典序排序,放入数组中
2、然后将他们拼接成一个字符串,进行sha1加密,得到密文
3、将密文与参数中的signature比较,如果相同则说明这个请求确实是微信发过来的
4、校验通过,将形参中的 echostr 原样地进行返回
5、微信收到原样返回的echostr,接入成功
(2)将参数排序,拼接成字符串,然后再进行加密都封装进一个工具类中:
package com.jingdianjichi.wx.utils;
import lombok.extern.slf4j.Slf4j;
import java.security.MessageDigest;
import java.util.Arrays;
/**
* sha1生成签名工具
*
* @author: ChickenWing
* @date: 2023/11/5
*/
@Slf4j
public class SHA1 {
/**
* 生成SHA1签名
* @param token 公众号的token
* @param timestamp 微信服务器传过来的时间戳
* @param nonce 微信服务器传过来的随机数
* @return SHA1加密后的签名
*/
public static String generateSHA1Signature(String token, String timestamp, String nonce) {
// 1. 将token、timestamp、nonce三个参数按字典序排序
String[] params = new String[]{token, timestamp, nonce};
Arrays.sort(params);
// 2. 拼接成一个字符串
StringBuilder sb = new StringBuilder();
for (String param : params) {
sb.append(param);
}
// 3. 使用SHA1进行加密
String signature = sha1(sb.toString());
return signature;
}
/**
* 对字符串进行SHA1加密
* @param input 输入字符串
* @return SHA1加密后的结果
*/
private static String sha1(String input) {
try {
// 获取SHA1 MessageDigest实例
MessageDigest digest = MessageDigest.getInstance("SHA-1");
// 执行SHA1加密
byte[] hash = digest.digest(input.getBytes("UTF-8"));
// 将字节数组转化为十六进制字符串
StringBuilder hexString = new StringBuilder();
for (byte b : hash) {
// 将每个字节转化为两位十六进制字符串
String hex = Integer.toHexString(0xFF & b);
if (hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex);
}
return hexString.toString().toLowerCase(); // 返回小写形式的SHA1签名
} catch (Exception e) {
throw new RuntimeException("SHA1加密失败", e);
}
}
public static void main(String[] args) {
// 测试用例
String token = "your_token"; // 公众号的token
String timestamp = "timestamp_from_wechat"; // 微信服务器传过来的时间戳
String nonce = "nonce_from_wechat"; // 微信服务器传过来的随机数
String echostr = "echostr_from_wechat"; // 微信服务器传过来的随机字符串
String signature = generateSHA1Signature(token, timestamp, nonce);
System.out.println("SHA1 Signature: " + signature);
}
}
至此微信的对接就没问题了。如果报了异常,可以查看微信定义的全局返回码:开发前必读 / 全局返回码说明
测试:
2、内网穿透
这个get接口是给微信用的,所以要用内网穿透给微信调用得到。
(1)进入内网穿透的官方教程文档:
NATAPP1分钟快速新手图文教程 - NATAPP-内网穿透 基于ngrok的国内高速内网映射工具https://natapp.cn/article/natapp_newbie
(2)下载内网穿透的客户端:NATAPP-内网穿透 基于ngrok的国内高速内网映射工具
(3)下载之后,解压至任意目录,得到natapp.exe (linux下无需解压,直接 wget)
(4)下载配置文件使用本地配置文件config.ini - NATAPP-内网穿透 基于ngrok的国内高速内网映射工具
(5)将配置文件放到刚刚解压出来的内网穿透的客户端同目录下
(6)将内网穿透的authtoken复制,写入配置文件中
(7)windows系统的话直接双击运行exe文件
(8)然后就可以将这个地址复制到上面微信公众号服务配置的url里了
(9)测试
3、监听公众号消息
刚刚使用的是get请求接入微信。当公众号被关注、用户发消息给了公众号等,微信也会对同样的请求路径发送请求,但请求方式是post。
而且,公众号通过post请求发送过来的请求体,不再是json格式的,而是XML格式的了
事后要对XML格式的数据进行解析,因为XML数据里面就包含了用户唯一的openId,还有用户发送了什么消息。
详细官方文档(可以去看看,很详细):基础消息能力 / 接收普通消息
(1)微信发送的xml示例代码:下面是当用户关注了公众号时微信发送来的请求体
<xml>
<ToUserName><![CDATA[gh_a0946b2a49d2]]></ToUserName>
<FromUserName><![CDATA[ofo2kfilifn******fk4ion7oBIcY]]></FromUserName>
<CreateTime>1742795741</CreateTime>
<MsgType><![CDATA[event]]></MsgType>
<Event><![CDATA[subscribe]]></Event>
<EventKey><![CDATA[]]></EventKey>
</xml>
里面的类型代表什么?:
(2)当用户发送“验证码”给公众号时,微信发过来的请求体:
<xml>
<ToUserName><![CDATA[gh_a0946b2a49d2]]></ToUserName>
<FromUserName><![CDATA[ofo2kfilifn******fk4ion7oBIcY]]></FromUserName>
<CreateTime>1742796773</CreateTime>
<MsgType><![CDATA[text]]></MsgType>
<Content><![CDATA[验证码]]></Content>
<MsgId>24950849497629663</MsgId>
</xml>
里面的类型代表什么?:
示例代码(解析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 msg_signature) {
log.info("\n微信回调请求参数:requestBody:{};\nsignature:{};timestamp:{};nonce:{};echostr:{};",
requestBody, signature, timestamp, nonce, msg_signature);
//工具类解析 xml格式文本
Map<String, String> msgMap = MessageUtil.parseXml(requestBody);
//获取消息类型:
String msgType = msgMap.get("MsgType");
StringBuilder msgTypeKeySB = new StringBuilder();
msgTypeKeySB.append(msgType);
String event = msgMap.get("Content");//用户发送了文本消息时,event才不为空,否则为null
if (!StringUtils.isEmpty(event)){
msgTypeKeySB.append(".");
msgTypeKeySB.append(event);
}
log.info("msg:{}", msgTypeKeySB.toString());
}
上面这段代码,将用户的行为再取出来打印了一下日志做测试。
测试
可以自己扫一下自己那个测试号的二维码,然后点击关注或者发送一下文本消息看看
(1)用户发送“验证码” 给公众号时:
业务开发
可以看到xml中都有了ToUserName和FromUserName,微信服务给我们发过来的,其中ToUserName就是指我们的服务端,FromUserName就是指用户端,里面的内容就是用户唯一的openId。
所以我们想要给用户发送验证码时,只要模仿微信发过来的文本类型消息,也这样写类似的XML信息即可,然后XML信息里:ToUserName改为原来的FromUserName,FromUserName改为原来的ToUserName,这就表示是服务端给用户端发送请求了
XML代码示例:
这是抽取出来的一个方法,业务中传入解析xml返回的map参数,然后xml中的信息表示给用户发送欢迎消息。
@Override
public String dealMsg(Map<String, String> messageMap) {
log.info("触发用户关注事件");
String UserName = messageMap.get("FromUserName");
String adminName = messageMap.get("ToUserName");
String content = "感谢您的关注";
String backMsg = "<xml>\n" +
" <ToUserName><![CDATA[" + UserName + "]]></ToUserName>\n" +
" <FromUserName><![CDATA[" + adminName + "]]></FromUserName>\n" +
" <CreateTime>12345678</CreateTime>\n" +
" <MsgType><![CDATA[text]]></MsgType>\n" +
" <Content><![CDATA[" + content + "]]></Content>\n" +
"</xml>";
return backMsg;
}
至于具体的业务这里就不演示扩展了,比如可以生成一个验证码,然后放到redis里面,设置过期时间,然后将验证码通过公众号发送给用户,然后用户输入验证码,后端给他登录注册。
XML解析工具类
需要引入三个依赖:
<!--dom4j--> <dependency> <groupId>org.dom4j</groupId> <artifactId>dom4j</artifactId> <version>2.1.3</version> </dependency> <!--xstream 也是用于解析xml文本的--> <dependency> <groupId>com.thoughtworks.xstream</groupId> <artifactId>xstream</artifactId> <version>1.4.10</version> </dependency> <!--序列化--> <dependency> <groupId>com.google.code.gson</groupId> <artifactId>gson</artifactId> </dependency>
工具类:
package com.jingdianjichi.wx.utils;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
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;
}
}