公众号接收消息和被动回复加解密

洛塔服务号回复004获取代码。

功能说明

前面一片使用明文方式实现了接收普通消息和被动回复。这边还是使用前面的示例,实现相同的功能,不同的是公众号后台开启了加解密。

公众号配置

公众号后台基本配置-服务器配置中,修改消息加解密方式为安全模式,收到的普通消息需要解密,被动回复的消息需要加密。

  • URL:和上一篇的一模一样,本篇使用http://test.lootaa.com/lootaa-wechat/wx2
  • Token:任意填写,和代码中的一致
  • EncodingAESKey:随机生成即可,代码中要用
  • 消息加解密方式:安全模式(推荐)

其中,URL对应代码部署方式为,后台使用springboot来开发,nginx做端口转发。
nginx配置:

        location /lootaa-wechat/ {
                proxy_pass http://127.0.0.1:2022/lootaa-wechat/;
                proxy_set_header  X-Real-IP  $remote_addr;
                proxy_set_header Host $host;
        }

application.properties配置

server.port=2022
server.servlet.context-path=/lootaa-wechat

get方法访问配置(路径使用的http://test.lootaa.com/lootaa-wechat/wx2)

@RestController
public class Test004 {
	@GetMapping("wx2")
	public void wxGet(HttpServletRequest request, PrintWriter pw) {
		// 微信加密签名,需要使用本地计算出来的和这个对比,确认是微信发送的消息
		String signature = request.getParameter("signature");
		String timestamp = request.getParameter("timestamp"); // 时间戳
		String nonce = request.getParameter("nonce"); // 随机数
		String echostr = request.getParameter("echostr"); // 随机字符串
		// 将token、timestamp、nonce三个参数进行字典序排序
		List<String> list = new ArrayList<String>();
		list.add("lootaa"); // 公众号后台设置的token
		list.add(timestamp);
		list.add(nonce);
		Collections.sort(list);
		// 将三个参数字符串拼接成一个字符串进行sha1加密
		String tokenStr = "";
		for (int i = 0; i < list.size(); i++) {
			tokenStr += list.get(i);
		}
		String signatureStr = DigestUtils.sha1Hex(tokenStr);
		// 开发者获得加密后的字符串可与signature对比,标识该请求来源于微信
		if (signature.equals(signatureStr)) {
			pw.write(echostr); // 原样返回echostr参数内容
		} else {
			pw.write("");
		}
	}

打包部署到服务器后,将此路径和token填写到后台中,点击保存即可。

辅助类

为了方便解析,需要两个辅助类,分别是将接收到的request转化为Document(dom4j下的)、将Document转化为map。

  • 将request转化为Document
	public static Document getDocument(HttpServletRequest request) {
		SAXReader reader = new SAXReader();
		try {
			InputStream ins = request.getInputStream();
			Document doc = reader.read(ins);
			return doc;
		} catch (IOException e) {
			e.printStackTrace();
		} catch (DocumentException e) {
			e.printStackTrace();
		}
		return null;
	}
  • 将Document转化为map
	public static Map<String, String> docToMap(Document doc) {
		Map<String, String> map = new HashMap<String, String>();
		Element root = doc.getRootElement();
		@SuppressWarnings("unchecked")
		List<Element> list = root.elements();
		for (Element element : list) {
			map.put(element.getName(), element.getText());
		}
		return map;
	}

对比区别

和明文情况对比,加解密除了需要加密方法和解密方法外,还有一个比较大的区别是,明文情况下中文乱码需要特殊处理,而加密情况下是不需要的(单独处理了反而会乱码)。

微信提供的辅助类

微信提供了多种语言版本的辅助类,可以点击这里直接下载。使用过程中,需要maven引入包。其中codec是必须引入的,dom4j是我测试代码解析使用的。

		<dependency>
			<groupId>commons-codec</groupId>
			<artifactId>commons-codec</artifactId>
		</dependency>
		
		<dependency>
            <groupId>dom4j</groupId>
            <artifactId>dom4j</artifactId>
            <version>1.6.1</version>
        </dependency>

微信提供的辅助类包括

  • AesException.java
  • ByteGroup.java
  • PKCS7Encoder.java
  • SHA1.java
  • WXBizMsgCrypt.java
  • XMLParse.java

接收普通消息解密

用到了上面提供的辅助类。得到map后,处理方式就和明文的基本一致了(除了被动回复需要加密)

		String token = "lootaa";
		String encodingAesKey = "FpKEYJDuwK92k2juU2z0sUvTmc3hB4W5wGLJEKay8oK";
		String appid = "wx276049d6a7551dca";
    	WXBizMsgCrypt pc = new WXBizMsgCrypt(token, encodingAesKey, appid);
        String timestamp = request.getParameter("timestamp");    
        String nonce = request.getParameter("nonce");  
        String msgSignature = request.getParameter("msg_signature");  
		Document doc = getDocument(request);
		String result2 = pc.decryptMsg(msgSignature, timestamp, nonce, doc.asXML());
		System.out.println("解密后明文: " + result2);
		Map<String, String> map = docToMap(DocumentHelper.parseText(result2));

被动回复加密

result就是明文情况下要返回的消息,直接调用下encryptMsg即可(pc是加密时候的WXBizMsgCrypt、timestamp和nonce是从request中接收到的原样信息)。

result = pc.encryptMsg(result, timestamp, nonce);

接收文字

接收消息的方法名称和校验的名称要一致,不过类型要用post。加上注解接口:@PostMapping(“wx”),本篇最后放有完整代码。

测试接收文字并原样返回(接收和回复类型没有任何绑定关系,仅为了方便测试)。

		String messageType = map.get("MsgType");
		if (Objects.equals(messageType, "text")) { // 文本消息
			String content = map.get("Content"); // 消息文本
			String result = "<xml>" + "<ToUserName><![CDATA[" + map.get("FromUserName") + "]]></ToUserName>"
					+ "<FromUserName><![CDATA[" + map.get("ToUserName") + "]]></FromUserName>" + "<CreateTime>"
					+ System.currentTimeMillis() + "</CreateTime>" + "<MsgType><![CDATA[text]]></MsgType>"
					+ "<Content><![CDATA[" + content + "]]></Content></xml>";
			result = pc.encryptMsg(result, timestamp, nonce);
			pw.write(result);
		}

接收图片

接收图片部分基本没坑,将mediaId直接返回即可。

		if (Objects.equals(messageType, "image")) { // 图片消息
//			String pictureUrl = map.get("PicUrl"); //图片地址,可以进行保存等操作
			String mediaId = map.get("MediaId");
			String result = "<xml>" + "<ToUserName><![CDATA[" + map.get("FromUserName") + "]]></ToUserName>"
					+ "<FromUserName><![CDATA[" + map.get("ToUserName") + "]]></FromUserName>" + "<CreateTime>"
					+ System.currentTimeMillis() + "</CreateTime>" + "<MsgType><![CDATA[image]]></MsgType>"
					+ "<Image><MediaId><![CDATA[" + mediaId + "]]></MediaId></Image></xml>";
			result = pc.encryptMsg(result, timestamp, nonce);
			pw.write(result);
		}

接收语音

接收语音部分基本没坑,将mediaId直接返回即可。

		if (Objects.equals(messageType, "voice")) { // 语音消息
			String mediaId = map.get("MediaId"); // 可以使用获取临时素材接口,得到语音文件
			String result = "<xml>" + "<ToUserName><![CDATA[" + map.get("FromUserName") + "]]></ToUserName>"
					+ "<FromUserName><![CDATA[" + map.get("ToUserName") + "]]></FromUserName>" + "<CreateTime>"
					+ System.currentTimeMillis() + "</CreateTime>" + "<MsgType><![CDATA[voice]]></MsgType>"
					+ "<Voice><MediaId><![CDATA[" + mediaId + "]]></MediaId></Voice></xml>";
			result = pc.encryptMsg(result, timestamp, nonce);
			pw.write(result);
		}

接收视频

接收视频比较简单,但是原样返回是不行的,会报错“该公众号服务出现故障,请稍后重试”,出现错误的原因分析着可能是mediaId的问题。可以改用以下两种方式中的任意一种

  • 方式一:调用保存素材接口将收到的语音保存本地,再调用上传素材接口,获取新的mediaId(没测试)
  • 方式二:再后台上传一个视频(需要审核),调用查询视频素材接口获取到mediaId
    我使用方式二拿到了对应的mediaId,就能直接返回了。
		if (Objects.equals(messageType, "video") || Objects.equals(messageType, "shortvideo")) { // 视频消息或小视频消息
			String mediaId = map.get("MediaId"); // 可以使用获取临时素材接口,得到视频文件。但是这个media不能直接返回,会报错
			System.out.println(mediaId);
			String result = "<xml>" + "<ToUserName><![CDATA[" + map.get("FromUserName") + "]]></ToUserName>"
					+ "<FromUserName><![CDATA[" + map.get("ToUserName") + "]]></FromUserName>" + "<CreateTime>"
					+ System.currentTimeMillis() + "</CreateTime>" + "<MsgType><![CDATA[video]]></MsgType>"
					+ "<Video><MediaId><![CDATA["
					+ "6XvVazVT3F4qsVQ-QW23lhZjulUCZMHWn5eRyL69G8spweQ-CEfvW57HyiFA8c2y"
					+ "]]></MediaId><Title><![CDATA[标题]]></Title><Description><![CDATA[描述]]></Description></Video></xml>";
			result = pc.encryptMsg(result, timestamp, nonce);
			pw.write(result);
		}

接收地理位置、回复音乐

接收地理位置没有肯定,但是回复音乐有坑。

  • 文档大小写问题:官方文档中,语音的地址参数写的MusicURL,但是给的demo中用的MusicUrl,大小写不一致,测试发现应该使用小写的这个,用大写的会收不到消息(用MusicUrl)。
  • mp3文件问题:mp3文件必须是可以直接下载类型的,不能是预览类型。简单理解就是用浏览器打开mp3文件,直接调用下载并保存了就可以用,如果是在浏览器上预览并可以点击播放的就不能用。如果是tomcat中放mp3文件测试,需要修改查看类型(tomcat配置中的那篇有介绍方式)
		if (Objects.equals(messageType, "location")) { // 地理位置消息
			String result = "<xml>" + "<ToUserName><![CDATA[" + map.get("FromUserName") + "]]></ToUserName>"
					+ "<FromUserName><![CDATA[" + map.get("ToUserName") + "]]></FromUserName>" + "<CreateTime>"
					+ System.currentTimeMillis() + "</CreateTime>" + "<MsgType><![CDATA[music]]></MsgType>"
					+ "<Music><Title><![CDATA[标题]]></Title><Description><![CDATA[描述]]></Description><MusicUrl><![CDATA[http://www.luotayixing.com/bobao/kh1648863098972.mp3]]></MusicUrl><HQMusicUrl><![CDATA[http://www.luotayixing.com/bobao/kh1648863098972.mp3]]></HQMusicUrl><ThumbMediaId>6XvVazVT3F4qsVQ-QW23lhyagwQKJCDlX8N72L6s3vWt3wRdr_kyv57ykWM073PS</ThumbMediaId></Music></xml>";
			result = pc.encryptMsg(result, timestamp, nonce);
			pw.write(result);
		}

接收链接、回复图文

接收链接的方式比较特别,不是随便打开一个网址就能发送到公众号的(直接转发会发现只能发送给好友或者群)。而复制链接再发送给公众号,相当于发送的是文字,并不是链接。
发送链接的方式:打开要发送的文档,右上角选择收藏,然后从公众号中发送消息选择收藏,点击对应链接文件。

		if (Objects.equals(messageType, "link")) { // 链接消息
			String title = map.get("Title");
			String description = map.get("Description");
			String url = map.get("Url");
			String result = "<xml>" + "<ToUserName><![CDATA[" + map.get("FromUserName") + "]]></ToUserName>"
					+ "<FromUserName><![CDATA[" + map.get("ToUserName") + "]]></FromUserName>" + "<CreateTime>"
					+ System.currentTimeMillis() + "</CreateTime>"
					+ "<MsgType><![CDATA[news]]></MsgType><ArticleCount>1</ArticleCount>"
					+ "<Articles><item><Title><![CDATA[" + title
					+ "]]></Title><Description><![CDATA[" + description
					+ "]]></Description><PicUrl><![CDATA[https://profile.csdnimg.cn/3/7/F/0_m0_58095675]]></PicUrl><Url><![CDATA["
					+ url + "]]></Url></item></Articles></xml>";
			result = pc.encryptMsg(result, timestamp, nonce);
			pw.write(result);
		}

完整代码

package com.lootaa.wechat;

import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.codec.digest.DigestUtils;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

import com.lootaa.wechat.util.WXBizMsgCrypt;

/**
 * 前置条件:基本配置中开启了服务器配置 
 * 完整项目源码可关注公众号"lootaayun"(洛塔),回复004获取
 */
@RestController
public class Test004 {

	@GetMapping("wx2")
	public void wxGet(HttpServletRequest request, PrintWriter pw) {
		// 微信加密签名,需要使用本地计算出来的和这个对比,确认是微信发送的消息
		String signature = request.getParameter("signature");
		String timestamp = request.getParameter("timestamp"); // 时间戳
		String nonce = request.getParameter("nonce"); // 随机数
		String echostr = request.getParameter("echostr"); // 随机字符串
		// 将token、timestamp、nonce三个参数进行字典序排序
		List<String> list = new ArrayList<String>();
		list.add("lootaa"); // 公众号后台设置的token
		list.add(timestamp);
		list.add(nonce);
		Collections.sort(list);
		// 将三个参数字符串拼接成一个字符串进行sha1加密
		String tokenStr = "";
		for (int i = 0; i < list.size(); i++) {
			tokenStr += list.get(i);
		}
		String signatureStr = DigestUtils.sha1Hex(tokenStr);
		// 开发者获得加密后的字符串可与signature对比,标识该请求来源于微信
		if (signature.equals(signatureStr)) {
			pw.write(echostr); // 原样返回echostr参数内容
		} else {
			pw.write("");
		}
	}

	public static Map<String, String> docToMap(Document doc) {
		Map<String, String> map = new HashMap<String, String>();
		Element root = doc.getRootElement();
		@SuppressWarnings("unchecked")
		List<Element> list = root.elements();
		for (Element element : list) {
			map.put(element.getName(), element.getText());
		}
		return map;
	}

	public static Document getDocument(HttpServletRequest request) {
		SAXReader reader = new SAXReader();
		try {
			InputStream ins = request.getInputStream();
			Document doc = reader.read(ins);
			return doc;
		} catch (IOException e) {
			e.printStackTrace();
		} catch (DocumentException e) {
			e.printStackTrace();
		}
		return null;
	}

	/**
	 * 如果不能确保5秒钟之内回复消息,微信会发起重试。对于此种情况,两种解决方法 1. 所有消息都有MsgId字段,用来做判断避免重复处理 2.
	 * 先返回一条固定消息,然后再启线程单独处理(比如处理完成后在用客服消息接口发送给用户)
	 */
	@PostMapping("wx2")
	public void wxPost(HttpServletRequest request, HttpServletResponse response, PrintWriter pw)
			throws Exception {
		String token = "lootaa";
		String encodingAesKey = "FpKEYJDuwK92k2juU2z0sUvTmc3hB4W5wGLJEKay8oK";
		String appid = "wx276049d6a7551dca";
    	WXBizMsgCrypt pc = new WXBizMsgCrypt(token, encodingAesKey, appid);
        String timestamp = request.getParameter("timestamp");    
        String nonce = request.getParameter("nonce");  
        String msgSignature = request.getParameter("msg_signature");  
		Document doc = getDocument(request);
		String result2 = pc.decryptMsg(msgSignature, timestamp, nonce, doc.asXML());
		System.out.println("解密后明文: " + result2);
		Map<String, String> map = docToMap(DocumentHelper.parseText(result2));
		String messageType = map.get("MsgType");
		if (Objects.equals(messageType, "text")) { // 文本消息
			String content = map.get("Content"); // 消息文本
			String result = "<xml>" + "<ToUserName><![CDATA[" + map.get("FromUserName") + "]]></ToUserName>"
					+ "<FromUserName><![CDATA[" + map.get("ToUserName") + "]]></FromUserName>" + "<CreateTime>"
					+ System.currentTimeMillis() + "</CreateTime>" + "<MsgType><![CDATA[text]]></MsgType>"
					+ "<Content><![CDATA[" + content + "]]></Content></xml>";
			result = pc.encryptMsg(result, timestamp, nonce);
			pw.write(result);
		}
		if (Objects.equals(messageType, "image")) { // 图片消息
//			String pictureUrl = map.get("PicUrl"); //图片地址,可以进行保存等操作
			String mediaId = map.get("MediaId");
			String result = "<xml>" + "<ToUserName><![CDATA[" + map.get("FromUserName") + "]]></ToUserName>"
					+ "<FromUserName><![CDATA[" + map.get("ToUserName") + "]]></FromUserName>" + "<CreateTime>"
					+ System.currentTimeMillis() + "</CreateTime>" + "<MsgType><![CDATA[image]]></MsgType>"
					+ "<Image><MediaId><![CDATA[" + mediaId + "]]></MediaId></Image></xml>";
			result = pc.encryptMsg(result, timestamp, nonce);
			pw.write(result);
		}
		if (Objects.equals(messageType, "voice")) { // 语音消息
			String mediaId = map.get("MediaId"); // 可以使用获取临时素材接口,得到语音文件
			String result = "<xml>" + "<ToUserName><![CDATA[" + map.get("FromUserName") + "]]></ToUserName>"
					+ "<FromUserName><![CDATA[" + map.get("ToUserName") + "]]></FromUserName>" + "<CreateTime>"
					+ System.currentTimeMillis() + "</CreateTime>" + "<MsgType><![CDATA[voice]]></MsgType>"
					+ "<Voice><MediaId><![CDATA[" + mediaId + "]]></MediaId></Voice></xml>";
			result = pc.encryptMsg(result, timestamp, nonce);
			pw.write(result);
		}
		if (Objects.equals(messageType, "video") || Objects.equals(messageType, "shortvideo")) { // 视频消息或小视频消息
			String mediaId = map.get("MediaId"); // 可以使用获取临时素材接口,得到视频文件。但是这个media不能直接返回,会报错
			System.out.println(mediaId);
			String result = "<xml>" + "<ToUserName><![CDATA[" + map.get("FromUserName") + "]]></ToUserName>"
					+ "<FromUserName><![CDATA[" + map.get("ToUserName") + "]]></FromUserName>" + "<CreateTime>"
					+ System.currentTimeMillis() + "</CreateTime>" + "<MsgType><![CDATA[video]]></MsgType>"
					+ "<Video><MediaId><![CDATA["
					+ "6XvVazVT3F4qsVQ-QW23lhZjulUCZMHWn5eRyL69G8spweQ-CEfvW57HyiFA8c2y"
					+ "]]></MediaId><Title><![CDATA[标题]]></Title><Description><![CDATA[描述]]></Description></Video></xml>";
			result = pc.encryptMsg(result, timestamp, nonce);
			pw.write(result);
		}
		if (Objects.equals(messageType, "location")) { // 地理位置消息
			String result = "<xml>" + "<ToUserName><![CDATA[" + map.get("FromUserName") + "]]></ToUserName>"
					+ "<FromUserName><![CDATA[" + map.get("ToUserName") + "]]></FromUserName>" + "<CreateTime>"
					+ System.currentTimeMillis() + "</CreateTime>" + "<MsgType><![CDATA[music]]></MsgType>"
					+ "<Music><Title><![CDATA[标题]]></Title><Description><![CDATA[描述]]></Description><MusicUrl><![CDATA[http://www.luotayixing.com/bobao/kh1648863098972.mp3]]></MusicUrl><HQMusicUrl><![CDATA[http://www.luotayixing.com/bobao/kh1648863098972.mp3]]></HQMusicUrl><ThumbMediaId>6XvVazVT3F4qsVQ-QW23lhyagwQKJCDlX8N72L6s3vWt3wRdr_kyv57ykWM073PS</ThumbMediaId></Music></xml>";
			result = pc.encryptMsg(result, timestamp, nonce);
			pw.write(result);
		}
		if (Objects.equals(messageType, "link")) { // 链接消息
			String title = map.get("Title");
			String description = map.get("Description");
			String url = map.get("Url");
			String result = "<xml>" + "<ToUserName><![CDATA[" + map.get("FromUserName") + "]]></ToUserName>"
					+ "<FromUserName><![CDATA[" + map.get("ToUserName") + "]]></FromUserName>" + "<CreateTime>"
					+ System.currentTimeMillis() + "</CreateTime>"
					+ "<MsgType><![CDATA[news]]></MsgType><ArticleCount>1</ArticleCount>"
					+ "<Articles><item><Title><![CDATA[" + title
					+ "]]></Title><Description><![CDATA[" + description
					+ "]]></Description><PicUrl><![CDATA[https://profile.csdnimg.cn/3/7/F/0_m0_58095675]]></PicUrl><Url><![CDATA["
					+ url + "]]></Url></item></Articles></xml>";
			result = pc.encryptMsg(result, timestamp, nonce);
			pw.write(result);
		}

	}

}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lootaa

你的鼓励是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值