服务器之家:专注于VPS、云服务器配置技术及软件下载分享
分类导航

PHP教程|ASP.NET教程|Java教程|ASP教程|编程技术|正则表达式|C/C++|IOS|C#|Swift|Android|VB|R语言|JavaScript|易语言|vb.net|

服务器之家 - 编程语言 - Java教程 - 手把手教你用 SpringBoot 开发微信公众号后台

手把手教你用 SpringBoot 开发微信公众号后台

2022-12-08 17:14江南一点雨 Java教程

松哥今天要和大家聊一个有意思的话题,就是使用 Spring Boot 开发微信公众号后台。很多小伙伴可能注意到松哥的微信公众号后台有一个回复关键字如 666​ 或者 888 可以获取学习资料的功能,这是松哥基于 Spring Boot 写的一个简单后

Hello 各位小伙伴,松哥今天要和大家聊一个有意思的话题,就是使用 Spring Boot 开发微信公众号后台。

很多小伙伴可能注意到松哥的微信公众号后台有一个回复关键字如 666 或者 888 可以获取学习资料的功能,这是松哥基于 Spring Boot 写的一个简单后台,今天我们就来简单聊聊这个如何实现。

1. 实现思路

其实松哥这个回复关键字获取学习资料实现原理很简单,说白了,就是一个数据查询操作而已,回复的口令是查询关键字,回复的内容则是查询结果。这个原理很简单。

另一方面大家需要明白微信公众号后台开发消息发送的一个流程,大家看下面这张图:

手把手教你用 SpringBoot 开发微信公众号后台

这是大家在公众号后台回复关键字的情况。那么这个消息是怎么样一个传递流程呢?我们来看看下面这张图:

手把手教你用 SpringBoot 开发微信公众号后台

这张图,我给大家稍微解释下:

  • 首先javaboy4096 这个字符从公众号上发送到了微信服务器
  • 接下来微信服务器会把javaboy4096 转发到我自己的服务器上
  • 我收到javaboy4096 这个字符之后,就去数据库中查询,将查询的结果,按照腾讯要求的 XML 格式进行返回
  • 微信服务器把从我的服务器收到的信息,再发回到微信上,于是小伙伴们就看到了返回结果了

大致的流程就是这个样子。

接下来我们就来看一下实现细节。

2. 公众号后台配置

开发的第一步,是微信服务器要验证我们自己的服务器是否有效。

首先我们登录微信公众平台官网后,在公众平台官网的 开发-基本设置 页面,勾选协议成为开发者,然后点击“修改配置”按钮,填写:

  • 服务器地址(URL)
  • Token
  • EncodingAESKey

手把手教你用 SpringBoot 开发微信公众号后台

这里的 URL 配置好之后,我们需要针对这个 URL 开发两个接口,一个是 GET 请求的接口,这个接口用来做服务器有效性验证,另一个则是 POST 请求的接口,这个用来接收微信服务器发送来的消息。也就是说,微信服务器的消息都是通过 POST 请求发给我的。

Token 可由开发者可以任意填写,用作生成签名(该 Token 会和接口 URL 中包含的 Token 进行比对,从而验证安全性)。

EncodingAESKey 由开发者手动填写或随机生成,将用作消息体加解密密钥。

同时,开发者可选择消息加解密方式:明文模式、兼容模式和安全模式。明文模式就是我们自己的服务器收到微信服务器发来的消息是明文字符串,直接就可以读取并且解析,安全模式则是我们收到微信服务器发来的消息是加密的消息,需要我们手动解析后才能使用。

3. 开发

公众号后台配置完成后,接下来我们就可以写代码了。

3.1 服务器有效性校验

我们首先来创建一个普通的 Spring Boot 项目,创建时引入 spring-boot-starter-web 依赖,项目创建成功后,我们创建一个 Controller ,添加如下接口:

@GetMapping("/verify_wx_token") public void login(HttpServletRequest request, HttpServletResponse response) throws UnsupportedEncodingException { request.setCharacterEncoding("UTF-8"); String signature = request.getParameter("signature"); String timestamp = request.getParameter("timestamp"); String nonce = request.getParameter("nonce"); String echostr = request.getParameter("echostr"); PrintWriter out = null; try { out = response.getWriter(); if (CheckUtil.checkSignature(signature, timestamp, nonce)) { out.write(echostr); } } catch (IOException e) { e.printStackTrace(); } finally { out.close(); } }

关于这段代码,我做如下解释:

首先通过 request.getParameter 方法获取到微信服务器发来的 signature、timestamp、nonce 以及 echostr 四个参数,这四个参数中:signature 表示微信加密签名,signature 结合了开发者填写的 token 参数和请求中的timestamp参数、nonce参数;timestamp 表示时间戳;nonce 表示随机数;echostr 则表示一个随机字符串。

开发者通过检验 signature 对请求进行校验,如果确认此次 GET 请求来自微信服务器,则原样返回 echostr 参数内容,则接入生效,成为开发者成功,否则接入失败。

具体的校验就是松哥这里的 CheckUtil.checkSignature 方法,在这个方法中,首先将token、timestamp、nonce 三个参数进行字典序排序,然后将三个参数字符串拼接成一个字符串进行 sha1 加密,最后开发者获得加密后的字符串可与 signature 对比,标识该请求来源于微信。

校验代码如下:

public class CheckUtil { private static final String token = "123456"; public static boolean checkSignature(String signature, String timestamp, String nonce){ String[] str = new String[]{token, timestamp, nonce}; //排序
        Arrays.sort(str); //拼接字符串
        StringBuffer buffer = new StringBuffer(); for (int i = 0; i < str.length; i++) { buffer.append(str[i]); } //进行sha1加密
        String temp = SHA1.encode(buffer.toString()); //与微信提供的signature进行匹对
        return signature.equals(temp); } } public class SHA1 { private static final char[] HEX_DIGITS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; private static String getFormattedText(byte[] bytes){ int len = bytes.length; StringBuilder buf = new StringBuilder(len * 2); for (int j = 0; j < len; j++) { buf.append(HEX_DIGITS[(bytes[j] >> 4) & 0x0f]); buf.append(HEX_DIGITS[bytes[j] & 0x0f]); } return buf.toString(); } public static String encode(String str){ if (str == null) { return null; } try { MessageDigest messageDigest = MessageDigest.getInstance("SHA1"); messageDigest.update(str.getBytes()); return getFormattedText(messageDigest.digest()); } catch (Exception e) { throw new RuntimeException(e); } } }

OK,完成之后,我们的校验接口就算是开发完成了。接下来就可以开发消息接收接口了。

3.2 消息接收接口

接下来我们来开发消息接收接口,消息接收接口和上面的服务器校验接口地址是一样的,都是我们一开始在公众号后台配置的地址。只不过消息接收接口是一个 POST 请求。

我在公众号后台配置的时候,消息加解密方式选择了明文模式,这样我在后台收到的消息直接就可以处理了。微信服务器给我发来的普通文本消息格式如下:

<xml> <ToUserName>[CDATA[toUser]]>ToUserName> <FromUserName>[CDATA[fromUser]]>FromUserName> <CreateTime>1348831860CreateTime> <MsgType>[CDATA[text]]>MsgType> <Content>[CDATA[this is a test]]>Content> <MsgId>1234567890123456MsgId> xml>

这些参数含义如下:

参数

描述

ToUserName

开发者微信号

FromUserName

发送方帐号(一个OpenID)

CreateTime

消息创建时间 (整型)

MsgType

消息类型,文本为text

Content

文本消息内容

MsgId

消息id,64位整型

看到这里,大家心里大概就有数了,当我们收到微信服务器发来的消息之后,我们就进行 XML 解析,提取出来我们需要的信息,去做相关的查询操作,再将查到的结果返回给微信服务器。

这里我们先来个简单的,我们将收到的消息解析并打印出来:

@PostMapping("/verify_wx_token") public void handler(HttpServletRequest request, HttpServletResponse response) throws Exception { request.setCharacterEncoding("UTF-8"); response.setCharacterEncoding("UTF-8"); PrintWriter out = response.getWriter(); Map<String, String> parseXml = MessageUtil.parseXml(request); String msgType = parseXml.get("MsgType"); String content = parseXml.get("Content"); String fromusername = parseXml.get("FromUserName"); String tousername = parseXml.get("ToUserName"); System.out.println(msgType); System.out.println(content); System.out.println(fromusername); System.out.println(tousername); } public static Map<String, String> parseXml(HttpServletRequest request) throws Exception { Map<String, String> map = new HashMap<String, String>(); InputStream inputStream = request.getInputStream(); SAXReader reader = new SAXReader(); Document document = reader.read(inputStream); Element root = document.getRootElement(); List<Element> elementList = root.elements(); for (Element e : elementList) map.put(e.getName(), e.getText()); inputStream.close(); inputStream = null; return map; }

大家看到其实都是一些常规代码,没有什么难度。

做完这些之后,我们将项目打成 jar 包在服务器上部署启动。启动成功之后,确认微信的后台配置也没问题,我们就可以在公众号上发一条消息了,这样我们自己的服务端就会打印出来刚刚消息的信息。

4. 消息分类

在讨论如何给微信服务器回复消息之前,我们需要先来了解下微信服务器发来的消息主要有哪些类型以及我们回复给微信的消息都有哪些类型。

在前文中大家了解到,微信发送来的 xml 消息中有一个 MsgType 字段,这个字段就是用来标记消息的类型。这个类型可以标记出这条消息是普通消息还是事件消息还是图文消息等。

普通消息主要是指:

  • 文本消息
  • 图片消息
  • 语音消息
  • 视频消息
  • 小视频消息
  • 地址位置消息
  • 链接消息

不同的消息类型,对应不同的 MsgType,这里我还是以普通消息为例,如下:

消息类型

MsgType

文本消息

text

图片消息

image

语音消息

voice

视频消息

video

小视频消息

shortvideo

地址位置消息

location

链接消息

link

大家千万不要以为不同类型消息的格式是一样的,其实是不一样的,也就是说,MsgType 为 text 的消息和 MsgType 为 image 的消息,微信服务器发给我们的消息内容是不一样的,这样带来一个问题就是我无法使用一个 Bean 去接收不同类型的数据,因此这里我们一般使用 Map 接收即可。

这是消息的接收,除了消息的接收之外,还有一个消息的回复,我们回复的消息也有很多类型,可以回复普通消息,也可以回复图片消息,回复语音消息等,不同的回复消息我们可以进行相应的封装。因为不同的返回消息实例也是有一些共同的属性的,例如消息是谁发来的,发给谁,消息类型,消息 id 等,所以我们可以将这些共同的属性定义成一个父类,然后不同的消息再去继承这个父类。

5. 返回消息类型定义

首先我们来定义一个公共的消息类型:

public class BaseMessage { private String ToUserName; private String FromUserName; private long CreateTime; private String MsgType; private long MsgId; //省略 getter/setter }

在这里:

  • ToUserName 表示开发者的微信号
  • FromUserName 表示发送方账号(用户的 OpenID)
  • CreateTime 消息的创建时间
  • MsgType 表示消息的类型
  • MsgId 表示消息 id

这是我们的基本消息类型,就是说,我们返回给用户的消息,无论是什么类型的消息,都有这几个基本属性。然后在此基础上,我们再去扩展出文本消息、图片消息 等。

我们来看下文本消息的定义:

public class TextMessage extends BaseMessage {
    private String Content;
    //省略 getter/setter
}

文本消息在前面消息的基础上多了一个 Content 属性,因此文本消息继承自 BaseMessage ,再额外添加一个 Content 属性即可。

其他的消息类型也是类似的定义,我就不一一列举了,至于其他消息的格式,大家可以参考微信开放文档(http://1t.click/aPXK)。

6. 返回消息生成

消息类型的 Bean 定义完成之后,接下来就是将实体类生成 XML。

首先我们定义一个消息工具类,将常见的消息类型枚举出来:

/**  * 返回消息类型:文本  */ public static final String RESP_MESSAGE_TYPE_TEXT = "text"; /**  * 返回消息类型:音乐  */ public static final String RESP_MESSAGE_TYPE_MUSIC = "music"; /**  * 返回消息类型:图文  */ public static final String RESP_MESSAGE_TYPE_NEWS = "news"; /**  * 返回消息类型:图片  */ public static final String RESP_MESSAGE_TYPE_Image = "image"; /**  * 返回消息类型:语音  */ public static final String RESP_MESSAGE_TYPE_Voice = "voice"; /**  * 返回消息类型:视频  */ public static final String RESP_MESSAGE_TYPE_Video = "video"; /**  * 请求消息类型:文本  */ public static final String REQ_MESSAGE_TYPE_TEXT = "text"; /**  * 请求消息类型:图片  */ public static final String REQ_MESSAGE_TYPE_IMAGE = "image"; /**  * 请求消息类型:链接  */ public static final String REQ_MESSAGE_TYPE_LINK = "link"; /**  * 请求消息类型:地理位置  */ public static final String REQ_MESSAGE_TYPE_LOCATION = "location"; /**  * 请求消息类型:音频  */ public static final String REQ_MESSAGE_TYPE_VOICE = "voice"; /**  * 请求消息类型:视频  */ public static final String REQ_MESSAGE_TYPE_VIDEO = "video"; /**  * 请求消息类型:推送  */ public static final String REQ_MESSAGE_TYPE_EVENT = "event"; /**  * 事件类型:subscribe(订阅)  */ public static final String EVENT_TYPE_SUBSCRIBE = "subscribe"; /**  * 事件类型:unsubscribe(取消订阅)  */ public static final String EVENT_TYPE_UNSUBSCRIBE = "unsubscribe"; /**  * 事件类型:CLICK(自定义菜单点击事件)  */ public static final String EVENT_TYPE_CLICK = "CLICK"; /**  * 事件类型:VIEW(自定义菜单 URl 视图)  */ public static final String EVENT_TYPE_VIEW = "VIEW"; /**  * 事件类型:LOCATION(上报地理位置事件)  */ public static final String EVENT_TYPE_LOCATION = "LOCATION"; /**  * 事件类型:LOCATION(上报地理位置事件)  */ public static final String EVENT_TYPE_SCAN = "SCAN";

大家注意这里消息类型的定义,以 RESP 开头的表示返回的消息类型,以 REQ 表示微信服务器发来的消息类型。然后在这个工具类中再定义两个方法,用来将返回的对象转换成 XML:

public static String textMessageToXml(TextMessage textMessage){ xstream.alias("xml", textMessage.getClass()); return xstream.toXML(textMessage); } private static XStream xstream = new XStream(new XppDriver() { public HierarchicalStreamWriter createWriter(Writer out){ return new PrettyPrintWriter(out) { boolean cdata = true; @SuppressWarnings("rawtypes") public void startNode(String name, Class clazz){ super.startNode(name, clazz); } protected void writeText(QuickWriter writer, String text){ if (cdata) { writer.write("); writer.write(text); writer.write("]]>"); } else { writer.write(text); } } }; } });

textMessageToXML 方法用来将 TextMessage 对象转成 XML 返回给微信服务器,类似的方法我们还需要定义 imageMessageToXml、voiceMessageToXml 等,不过定义的方式都基本类似,我就不一一列出来了。

7. 返回消息分发

由于用户发来的消息可能存在多种情况,我们需要分类进行处理,这个就涉及到返回消息的分发问题。因此我在这里再定义一个返回消息分发的工具类,如下:

public class MessageDispatcher { public static String processMessage(Map<String, String> map){ String openid = map.get("FromUserName"); //用户 openid
        String mpid = map.get("ToUserName"); //公众号原始 ID
        if (map.get("MsgType").equals(MessageUtil.REQ_MESSAGE_TYPE_TEXT)) { //普通文本消息
            TextMessage txtmsg = new TextMessage(); txtmsg.setToUserName(openid); txtmsg.setFromUserName(mpid); txtmsg.setCreateTime(new Date().getTime()); txtmsg.setMsgType(MessageUtil.RESP_MESSAGE_TYPE_TEXT); txtmsg.setContent("这是返回消息"); return MessageUtil.textMessageToXml(txtmsg); } return null; } public String processEvent(Map<String, String> map){ //在这里处理事件 } }

这里我们还可以多加几个 elseif 去判断不同的消息类型,我这里因为只有普通文本消息,所以一个 if 就够用了。

在这里返回值我写死了,实际上这里需要根据微信服务端传来的 Content 去数据中查询,将查询结果返回,数据库查询这一套相信大家都能搞定,我这里就不重复介绍了。

最后在消息接收 Controller 中调用该方法,如下:

@PostMapping(value = "/verify_wx_token",produces = "application/xml;charset=utf-8") public String handler(HttpServletRequest request, HttpServletResponse response) throws Exception { request.setCharacterEncoding("UTF-8"); Map<String, String> map = MessageUtil.parseXml(request); String msgType = map.get("MsgType"); if (MessageUtil.REQ_MESSAGE_TYPE_EVENT.equals(msgType)) { return messageDispatcher.processEvent(map); }else{ return messageDispatcher.processMessage(map); } }

在 Controller 中,我们首先判断消息是否是事件,如果是事件,进入到事件处理通道,如果不是事件,则进入到消息处理通道。

注意,这里需要配置一下返回消息的编码,否则可能会出现中文乱码。

如此之后,我们的服务器就可以给公众号返回消息了。

好了,本文我们就先说到这里。

原文地址:https://mp.weixin.qq.com/s/oTWnGXLkS3DDFf4sMWNt-g

延伸 · 阅读

精彩推荐
  • Java教程Java Swing组件实现进度监视功能示例

    Java Swing组件实现进度监视功能示例

    这篇文章主要介绍了Java Swing组件实现进度监视功能,结合完整实例形式详细分析了Java基于Swing组件实现进度条显示功能的具体操作技巧与相关注意事项,需要...

    米格战斗机8522021-04-02
  • Java教程java及C++中传值传递、引用传递和指针方式的理解

    java及C++中传值传递、引用传递和指针方式的理解

    为什么 Java 只有值传递,但 C++ 既有值传递,又有引用传递呢?今天我们就来探讨下这个问题,有需要的朋友可以参考下 ...

    hebedich3422019-11-29
  • Java教程分析并发编程之LongAdder原理

    分析并发编程之LongAdder原理

    LongAdder类是JDK1.8新增的一个原子性操作类。AtomicLong通过CAS算法提供了非阻塞的原子性操作,相比受用阻塞算法的同步器来说性能已经很好了,但是JDK开发组...

    tera5962021-09-18
  • Java教程Java线程通信之wait-notify通信方式详解

    Java线程通信之wait-notify通信方式详解

    这篇文章主要为大家详细介绍了Java线程通信之wait-notify通信方式,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下,...

    小小茶花女10522022-08-13
  • Java教程Java日常练习题,每天进步一点点(43)

    Java日常练习题,每天进步一点点(43)

    下面小编就为大家带来一篇Java基础的几道练习题(分享)。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧,希望可以...

    牛哄哄的柯南9222021-10-27
  • Java教程java 代理机制的实例详解

    java 代理机制的实例详解

    这篇文章主要介绍了java 代理机制的实例详解的相关资料,这里说明下如何实现代理机制,帮助大家理解掌握这部分内容,需要的朋友可以参考下...

    sheungxin2592020-12-20
  • Java教程spring boot容器启动流程

    spring boot容器启动流程

    spring cloud是基于spring boot快速搭建的,今天咱们就看看spring boot容器启动流程,需要的朋友跟随脚本之家小编一起学习吧...

    只会一点java6112021-03-26
  • Java教程Spring Boot RabbitMQ 延迟消息实现完整版示例

    Spring Boot RabbitMQ 延迟消息实现完整版示例

    本篇文章主要介绍了Spring Boot RabbitMQ 延迟消息实现完整版示例,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧...

    Sam哥哥12092021-04-25