【公众号开发】(1)
文章目录
- 【公众号开发】(1)
- 1. 获得一个测试号
- 2. 公众号开发原理
- 3. 创建开发者服务器
- 4. 内网穿透
- 5. 验证开发者服务器的url
- 5.1 公众号服务器对url发送get请求
- 5.2 消息的验证逻辑
- 5.3 代码实现
- 6. 接受并解析用户发来的消息
- 6.1 查看post请求的数据格式
- 6.2 解析xml字符串
- 6.3 将xml字符串解析结构构造回复消息
- 6.3.1 消息对象
- 6.3.2 将map封装成消息对象
- 6.3.3 将消息对象进行序列化为xml
- 6.3.4 在controller实现(调用方法)
公众号的使用和作用不必多说,很多企业都会选择公众号这种与客户粘性大的方式去推广自己或者其他的动作…
而本专栏主要讲解的就是公众号的基本开发,不囊括所有,但是可以从0到1,之后的1生万物由你完成!
我这里不列举公众号开发能咋开发,你可以查看开发者文档:开发前必读 / 首页 (qq.com)
这些是微信公众号自带的功能,但我们要满足我们自己的需求,当然是需要进行开发的
这些功能就是字面意思~
本专栏已开发为主,很少有理论的讲解,直接进入实战吧,公众号有啥功能我们可以开发,进入实战即可见分晓~
正式的公众号的申请需要门槛,所以为了方便我们开发者上手,专门有一个“接口测试公众号”用来练习,只不过没有一些高级的接口,这些无伤大雅,有了基础就可以去学习高级接口了
开始开发 / 接口测试号申请 (qq.com)
按照指引注册即可,如果已经注册,登录微信即可
这些消息可能你会有一些没有,不过没关系,之后慢慢展开
大概就是这样的,微信公众号并不是什么功能都能实现,只能实现一些公众号提供的接口(就是刚才那些)
因为用户并不是直接访问开发者,开发者也不是直接响应给用户,微信公众号提供较为统一的页面给用户,和较为统一的接口给开发者
这样的介入,提高公众号的统一性,用户可以依照经验使用,又有公众号的特性,提供的接口给开发者开发自定义!
我们先创建一个项目(SpringBoot),作为开发者服务器
这里就是创建好的了
我导入了lombok依赖(在一开始选上就好)
我的配置文件为application.yml:
spring: application: name: 微信公众号测试 # 项目的名称 output: ansi: enabled: ALWAYS # 配置控制台输出的效果,彩色 # JSON序列化配置 jackson: date-format: yyyy-MM-dd HH:mm:ss # ⽇期格式 default-property-inclusion: NON_NULL # 不为null序列化 server: port: 8080 logging: pattern: dateformat: MM-dd HH:mm:ss # 日期显示格式 level: root: info # 日志默认级别 file: path: D:/log/project/wx
由于微信公众号服务器在千里之外,要想访问我们的开发者服务器,必然是要通过外网去访问的,所以我们需要一个外网ip!
首先,云服务器是必然的,最终一定是要部署到云服务器去维持服务,但是我们开发阶段,频繁的修改和测验,云服务器的方式太麻烦了,所以我们要用内网穿透工具,将我们的内网映射成外网,让外接可以访问!
官网:NATAPP-内网穿透 基于ngrok的国内高速内网映射工具
安装到本地,并解压缩,到达natapp.exe的所在目录下:
右键在命令行打开:
回到官网注册/登录
这两项去做一下:
购买免费隧道:
进入配置页:
可以看到刚才的信息了,复制authtoken(等一下要用):
回到命令行输入(authtoken输入自己的):
./natapp -authtoken=72810d6d1ab6e1fe
这个就是映射的地址(综合了ip与端口的字符串):
不过注意的是,按ctrl c或者直接关闭或者电脑关机(甚至你没有关闭,过一段时间就变了),这个字符串的映射就失效了,并且下次生成的也不一样哦!
我们写一个接口,检测一下这个字符串是否有效:
浏览器访问:
命令行有请求的记录:
其实就是这个地方的接口配置设置:
点击修改:
但是点击提交后,并不会显示成功:
这是因为微信公众号服务器要确保这个url是开发者的url,有这些好处(我认为):
具体是啥原因,太底层不需要了解,我们“坐享其成,遵循规矩”
点击提交:
但是你现在纳闷了,“凭什么我不行?”,这是因为你的服务器并没有进行相关操作~
公众号会对我们填写的url的根目录(就是后面不带路由,也就是/
)发送一个GET请求,这个请求的目的就是验证
但是浏览器访问是这样的:
原因是缺少参数,我们可以在开发者文档查看:开始开发 / 接入指南 (qq.com)
重点看这里:
他的意思就是,他的请求携带了这四个参数,而我们开发者如果返回echostr这个回显字符串的话,那么就是验证成功,公众号也就确认你是,并且授权了,所以就可以匹配成功
那么我们要咋样才可以返回echostr呢?
原理就是:
这里 Token是只有公众号和开发者知道
所以以此与timestamp和nonce拼接之后的字符串, 加密之后的字符串作为建议身份的标志 非常合理科学
一般加密是不可逆的,所以我们并没有办法将signature解密出Token,而是将 加密出一个字符串与之对比!
加密算法:
- timestamp、nonce、Token按字典序排列
- 将排列后的字符串进行拼接
- 将拼接字符串进行加密(加密结果为字节数组)
- 字节数组每个字节按照十六进制进行拼接
最后,加密拼接字符串与signature进行比较
所需依赖
<dependency>
<groupId>org.dom4jgroupId>
<artifactId>dom4jartifactId>
<version>2.1.3version>
dependency>
@GetMapping("/")
public String check(@RequestParam("signature") String signature,
@RequestParam("timestamp") String timestamp,
@RequestParam("nonce") String nonce,
@RequestParam("echostr") String echostr) {
if(!StringUtils.hasLength(signature) || !StringUtils.hasLength(timestamp)
|| !StringUtils.hasLength(nonce) || !StringUtils.hasLength(echostr)) {
return "";
}
// 1) 将token 、 timestamp 、 nonce 三个参数进行字典序排序
String token = "maras103";
String[] list = {token, timestamp, nonce};
Arrays.sort(list);
// 2) 将三个字符串以此顺序进行拼接
StringBuilder builder = new StringBuilder();
for(String s : list) {
builder.append(s);
}
// 2.1) 加密
try {
MessageDigest messageDigest = MessageDigest.getInstance("sha1");
byte[] digest = messageDigest.digest(builder.toString().getBytes(StandardCharsets.UTF_8));
// 2.2) 将加密后的byte数组转换为signature一样的格式(每个字节都转换为十六进制进行拼接)
builder = new StringBuilder();
for(byte b : digest) {
// builder.append(Integer.toHexString(b));不能这么弄因为这样弄b如果是负,那么就凉凉
// 这样写保证两位十六进制都存在并且正确
builder.append(Integer.toHexString((b >> 4) & 15));//前四个字节转换为十六进制
builder.append(Integer.toHexString(b & 15));//后四个字节转换为十六进制
}
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
// 3) 核对请求是否来自于微信(加密解析后的字符串与signature比较)
if(builder.toString().equals(signature)) {
// 相同才返回回显字符串
// 并且返回echostr代表开发者确认了这是公众号服务器发来的请求,公众号就进行配置
return echostr;
} else {
// 否则返回null
return null;
}
}
用户有时候会发一些消息给公众号,那我们的服务器怎么针对这些消息做出一些自定义的动作呢?
原理是这样的
当用户发消息给公众号:
就相当于发请求给微服务公众号服务器,访问了它的一个接口,微信公众号就会打包我们的数据构造post请求访问我们的开发者服务器(根路径/
)
并且是以xml的格式!
获取请求的输入流对象,读取数据
@PostMapping("/")
public String receiveMessage(HttpServletRequest request) throws IOException {
ServletInputStream inputStream = request.getInputStream();
byte[] b = new byte[1024];
int len = 0;
while((len = inputStream.read(b)) != -1) {
System.out.println((new String(b, 0, len)));
}
return "";
}
注意:返回空字符串,代表什么都不回~
我们向公众号发一条消息:
@PostMapping("/")
public String receiveMessage(HttpServletRequest request) throws IOException {
Map<String, String> map = new HashMap<>();
SAXReader reader = new SAXReader();
// xml字符串解析方法
try {
//通过请求的输入流,获取Document对象
Document document = reader.read(request.getInputStream());
// 获取root节点
Element root = document.getRootElement();
// 获取所有子节点
List<Element> elements = root.elements();
// 遍历集合
for(Element e : elements) {
map.put(e.getName(), e.getStringValue());
}
log.info(map.toString());
} catch (DocumentException e) {
throw new RuntimeException(e);
}
return "";
}
查看日志:
解析成功~
注意:回复消息字符串必须是xml字符串格式,否则微信公众号不做任何动作
所需依赖
<dependency>
<groupId>com.thoughtworks.xstreamgroupId>
<artifactId>xstreamartifactId>
<version>1.4.11.1version>
dependency>
@Data
@XStreamAlias("xml")
public class TextMessage {
@XStreamAlias("ToUserName")
private String toUserName;
@XStreamAlias("FromUserName")
private String fromUserName;
@XStreamAlias("CreateTime")
private long createTime;
@XStreamAlias("MsgType")
private String msgType;
@XStreamAlias("Content")
private String content;
}
这个对象进行序列化一个对象为xml字符串的时候,默认以类名为父标签,各个属性的属性名为子标签,如果一个属性为自定义类型,则这个对象也是树形结构xml标签嵌套于此xml标签(套娃套娃套娃…)
public static TextMessage getReplyTextMessage(Map<String, String> map) {
TextMessage message = new TextMessage();
message.setToUserName(map.get("FromUserName"));
message.setFromUserName(map.get("ToUserName"));
message.setCreateTime(System.currentTimeMillis() / 1000);
message.setMsgType("text");
message.setContent("回复" + map.get("Content") + ":就不告诉你");
return message;
}
这里其实回复的消息可以是很多种(文本、图片、音频等等等…)
(这里简单的回复个文本消息)
(发起者和接收者的username进行调换)
(时间戳是秒级别为当前时间)
/**
* 将TextMessage对象序列化为xml字符串返回
* @param message
* @return
*/
public static String getXML(TextMessage message) {
//获取序列化工具XStream对象
XStream xStream = new XStream();
//指定类型
xStream.processAnnotations(TextMessage.class);
//转化为xml字符串
String xml = xStream.toXML(message);
//返回
return xml;
}
无非就是调用一些库方法
@PostMapping("/")
public String receiveMessage(HttpServletRequest request) throws IOException {
// ServletInputStream inputStream = request.getInputStream();
// byte[] b = new byte[1024];
// int len = 0;
// while((len = inputStream.read(b)) != -1) {
// System.out.println((new String(b, 0, len)));
// }
Map<String, String> map = new HashMap<>();
SAXReader reader = new SAXReader();
// xml字符串解析方法
try {
//通过请求的输入流,获取Document对象
Document document = reader.read(request.getInputStream());
// 获取root节点
Element root = document.getRootElement();
// 获取所有子节点
List<Element> elements = root.elements();
// 遍历集合
for(Element e : elements) {
map.put(e.getName(), e.getStringValue());
}
log.info(map.toString());
} catch (DocumentException e) {
throw new RuntimeException(e);
}
// 回复消息
// 1. 封装对象
TextMessage textMessage = TextMessage.getReplyTextMessage(map);
// 2. 序列化对象
String message = TextMessage.getXML(textMessage);
System.out.println(message);
return message;
}
公众号发消息测试:
成功啦!
我想这么一个用户与公众号交互的程序,让大家对公众号开发有了初步的认知!
文章到此结束!谢谢观看
可以叫我 小马,我可能写的不好或者有错误,但是一起加油鸭!代码:wx-demo · 游离态/马拉圈2023年10月 - 码云 - 开源中国 (gitee.com)