原理

原理一定要先搞清楚。用户与公众号之间的信息交互是:用户发送的信息是先传送到微信服务器,微信服务器在以xml的格式发送给进行公众号。如图所示:

申请测试公众号

地址:传送

代码

pom.xml

<!-- dom4j-识别解析XML-用于微信发送消息 -->
<dependency>
    <groupId>dom4j</groupId>
    <artifactId>dom4j</artifactId>
    <version>1.6.1</version>
</dependency>
<!-- xstream-Java对象序列化到XML-用于微信发送消息 -->
<dependency>
    <groupId>com.thoughtworks.xstream</groupId>
    <artifactId>xstream</artifactId>
    <version>1.4.10</version>
</dependency>

工具类

import java.io.InputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.servlet.http.HttpServletRequest;

import com.xffjs.project.wx.domain.ArticleItem;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;

/**
 * @className: WeChatUtil
 * @description: 微信工具类
 * @author: xiaofei
 * @create: 2020年08月09日
 */
public class WeChatUtil {

    /**
     * 验证自动回复消息签名
     *
     * @param signature 微信加密签名
     * @param timestamp 时间戳
     * @param nonce     随机数
     * @return
     */
    public static boolean checkSignature(String signature, String timestamp, String nonce) {
        String[] arr = new String[]{WeChatContant.TOKEN, timestamp, nonce};
        // 将token、timestamp、nonce三个参数进行字典序排序
        sort(arr);
        StringBuilder content = new StringBuilder();
        for (int i = 0; i < arr.length; i++) {
            content.append(arr[i]);
        }
        MessageDigest md = null;
        String tmpStr = null;
        try {
            md = MessageDigest.getInstance("SHA-1");
            // 将三个参数字符串拼接成一个字符串进行sha1加密
            byte[] digest = md.digest(content.toString().getBytes());
            tmpStr = byteToStr(digest);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        content = null;
        // 将sha1加密后的字符串可与signature对比,标识该请求来源于微信
        return tmpStr != null ? tmpStr.equals(signature.toUpperCase()) : false;
    }

    /**
     * 将字节数组转换为十六进制字符串
     *
     * @param byteArray
     * @return
     */
    private static String byteToStr(byte[] byteArray) {
        String strDigest = "";
        for (int i = 0; i < byteArray.length; i++) {
            strDigest += byteToHexStr(byteArray[i]);
        }
        return strDigest;
    }

    /**
     * 将字节转换为十六进制字符串
     *
     * @param mByte
     * @return
     */
    private static String byteToHexStr(byte mByte) {
        char[] Digit = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
        char[] tempArr = new char[2];
        tempArr[0] = Digit[(mByte >>> 4) & 0X0F];
        tempArr[1] = Digit[mByte & 0X0F];

        String s = new String(tempArr);
        return s;
    }

    /**
     * 排序
     * @param a
     */
    private static void sort(String a[]) {
        for (int i = 0; i < a.length - 1; i++) {
            for (int j = i + 1; j < a.length; j++) {
                if (a[j].compareTo(a[i]) < 0) {
                    String temp = a[i];
                    a[i] = a[j];
                    a[j] = temp;
                }
            }
        }
    }

    /**
     * 解析微信发来的请求(xml)
     *
     * @param request
     * @return
     * @throws Exception
     */
    @SuppressWarnings({"unchecked"})
    public static Map<String, String> parseXml(HttpServletRequest request) throws Exception {
        // 将解析结果存储在HashMap中
        Map<String, String> map = new HashMap<String, String>(10);
        // 从request中取得输入流
        InputStream inputStream = request.getInputStream();
        // 读取输入流
        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());
        }
        // 释放资源
        inputStream.close();
        inputStream = null;
        return map;
    }

    /**
     * map转string
     */
    public static String mapToXML(Map map) {
        StringBuffer sb = new StringBuffer();
        sb.append("<xml>");
        mapToXML2(map, sb);
        sb.append("</xml>");
        try {
            return sb.toString();
        } catch (Exception e) {
        }
        return null;
    }

    private static void mapToXML2(Map map, StringBuffer sb) {
        Set set = map.keySet();
        for (Iterator it = set.iterator(); it.hasNext(); ) {
            String key = (String) it.next();
            Object value = map.get(key);
            if (null == value) {
                value = "";
            }
            if (value.getClass().getName().equals("java.util.ArrayList")) {
                ArrayList list = (ArrayList) map.get(key);
                sb.append("<" + key + ">");
                for (int i = 0; i < list.size(); i++) {
                    HashMap hm = (HashMap) list.get(i);
                    mapToXML2(hm, sb);
                }
                sb.append("</" + key + ">");

            } else {
                if (value instanceof HashMap) {
                    sb.append("<" + key + ">");
                    mapToXML2((HashMap) value, sb);
                    sb.append("</" + key + ">");
                } else {
                    sb.append("<" + key + "><![CDATA[" + value + "]]></" + key + ">");
                }

            }

        }
    }

    /**
     * 回复文本消息
     *
     * @param requestMap
     * @param content
     * @return
     */
    public static String sendTextMsg(Map<String, String> requestMap, String content) {
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("ToUserName", requestMap.get(WeChatContant.FromUserName));
        map.put("FromUserName", requestMap.get(WeChatContant.ToUserName));
        map.put("MsgType", WeChatContant.RESP_MESSAGE_TYPE_TEXT);
        map.put("CreateTime", System.currentTimeMillis());
        map.put("Content", content);
        return mapToXML(map);
    }

    /**
     * 回复图文消息
     *
     * @param requestMap
     * @param items
     * @return
     */
    public static String sendArticleMsg(Map<String, String> requestMap, List<ArticleItem> items) {
        if (items == null || items.size() < 1) {
            return "";
        }
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("ToUserName", requestMap.get(WeChatContant.FromUserName));
        map.put("FromUserName", requestMap.get(WeChatContant.ToUserName));
        map.put("MsgType", "news");
        map.put("CreateTime", System.currentTimeMillis());
        List<Map<String, Object>> Articles = new ArrayList<Map<String, Object>>();
        for (ArticleItem itembean : items) {
            Map<String, Object> item = new HashMap<String, Object>();
            Map<String, Object> itemContent = new HashMap<String, Object>();
            itemContent.put("Title", itembean.getTitle());
            itemContent.put("Description", itembean.getDescription());
            itemContent.put("PicUrl", itembean.getPicUrl());
            itemContent.put("Url", itembean.getUrl());
            item.put("item", itemContent);
            Articles.add(item);
        }
        map.put("Articles", Articles);
        map.put("ArticleCount", Articles.size());
        return mapToXML(map);
    }
}

微信常量

/**
 * @className: WeChatContant
 * @description: 微信常量
 * @author: xiaofei
 * @create: 2020年08月09日
 */
public class WeChatContant {

    /**
     * 获取token
     * "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={appid}&secret={secret}"
     *
     * 获取用户信息--这个功能需要微信认证
     * "https://api.weixin.qq.com/cgi-bin/user/info?access_token={token}&openid={openid}&lang=zh_CN"
     * */

    /**
     * 微信消息加密密钥
     */
    public static final String TOKEN = "自己定义密钥";
    /**
     * 文本消息
     */
    public static final String RESP_MESSAGE_TYPE_TEXT = "text";
    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_VOICE = "voice";
    /**
     * 视频消息
     */
    public static final String REQ_MESSAGE_TYPE_VIDEO = "video";
    /**
     * 地址位置
     */
    public static final String REQ_MESSAGE_TYPE_LOCATION = "location";
    /**
     * 链接消息
     */
    public static final String REQ_MESSAGE_TYPE_LINK = "link";
    /**
     * 事件推送
     */
    public static final String REQ_MESSAGE_TYPE_EVENT = "event";
    /**
     * 用户关注公众号事件
     */
    public static final String EVENT_TYPE_SUBSCRIBE = "subscribe";
    /**
     * 用户取消关注公众号事件
     */
    public static final String EVENT_TYPE_UNSUBSCRIBE = "unsubscribe";
    /**
     * 用户扫描带参数二维码事件
     */
    public static final String EVENT_TYPE_SCAN = "scan";
    /**
     * 上报地理位置事件
     */
    public static final String EVENT_TYPE_LOCATION = "location";
    /**
     * 菜单点击事件
     */
    public static final String EVENT_TYPE_CLICK = "click";

    /**
     * 发送方帐号(一个OpenID)
     */
    public static final String FromUserName = "FromUserName";
    /**
     * 开发者微信号
     */
    public static final String ToUserName = "ToUserName";
    /**
     * 消息类型,event
     */
    public static final String MsgType = "MsgType";
    /**
     * 内容
     */
    public static final String Content = "Content";
    /**
     * 事件类型,CLICK
     */
    public static final String Event = "Event";

}

model

/**
 * @className: WeChatArticleItem
 * @description: 微信消息回复
 * @author: xiaofei
 * @create: 2020年08月09日
 */
public class WeChatArticleItem {

    private String title;
    private String description;
    private String picUrl;
    private String url;

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public String getPicUrl() {
        return picUrl;
    }

    public void setPicUrl(String picUrl) {
        this.picUrl = picUrl;
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }
}

WeChatService

import javax.servlet.http.HttpServletRequest;

/**
 * @className: WeChatService
 * @description: 微信消息service
 * @author: xiaofei
 * @create: 2020年08月09日
 */
public interface WeChatService {
    /**
     * 核心处理方法
     *
     * @param request
     * @return
     */
    String processRequest(HttpServletRequest request);
}

WeChatServiceImpl

import com.xffjs.common.utils.wx.WeChatContant;
import com.xffjs.common.utils.wx.WeChatUtil;
import com.xffjs.project.wx.service.WeChatService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import javax.servlet.http.HttpServletRequest;
import java.util.Map;

/**
 * @className: WeChatServiceImpl
 * @description: 微信消息service核心服务类
 * @author: xiaofei
 * @create: 2020年08月09日
 */
@Service
public class WeChatServiceImpl implements WeChatService {

    private final static Logger logger = LoggerFactory.getLogger(WeChatServiceImpl.class);

    @Override
    public String processRequest(HttpServletRequest request) {
        // xml格式的消息数据
        String respXml = null;
        // 默认返回的文本消息内容
        String respContent;
        try {
            // 调用parseXml方法解析请求消息
            Map<String, String> requestMap = WeChatUtil.parseXml(request);
            logger.debug("*****************微信消息解析*****************");
            logger.debug("openid:" + requestMap.get(WeChatContant.ToUserName));
            logger.debug("内容:" + requestMap.get(WeChatContant.Content));
            logger.debug("Map:" + requestMap.toString());
            logger.debug("*****************微信消息解析*****************");
            // 消息类型
            String msgType = (String) requestMap.get(WeChatContant.MsgType);
            String mes = null;
            String openid = requestMap.get(WeChatContant.ToUserName);
            switch (msgType) {
                // 文本消息识别
                case WeChatContant.REQ_MESSAGE_TYPE_TEXT:
                    logger.debug("*****************文本消息识别*****************");
                    respContent = "您发送的是文本消息!" ;
                    respXml = WeChatUtil.sendTextMsg(requestMap, respContent);
                    break;
                // 事件推送
                case WeChatContant.REQ_MESSAGE_TYPE_EVENT:
                    // 获取事件类型
                    String eventType = (String) requestMap.get(WeChatContant.Event);
                    if (eventType.equalsIgnoreCase(WeChatContant.EVENT_TYPE_SUBSCRIBE)) {
                        // 进行关注
                        respContent = "感谢您关注公众号!";
                        respXml = WeChatUtil.sendTextMsg(requestMap, respContent);
                    }
                    break;
                default:
                    mes = mes == null ? "" : "未能正确识别!";
                    break;
            }
            if (respXml == null) {
                respXml = WeChatUtil.sendTextMsg(requestMap, mes);
            }
            return respXml;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "";
    }
}

这里面还有很多不同类型的消息,这里我这列举一个消息类型和一个事件。WeChatContant(所有类型都在这里面,需要什么可以自己添加)

Controller

import com.xffjs.common.utils.StringUtils;
import com.xffjs.common.utils.wx.WeChatUtil;
import com.xffjs.framework.aspectj.lang.annotation.VLog;
import com.xffjs.project.wx.service.WeChatService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;

/**
 * @className: WeChatController
 * @description: 微信Controller
 * @author: xiaofei
 * @create: 2020年08月09日
 */
@Controller
public class WeChatController {

    private final static Logger logger = LoggerFactory.getLogger(WeChatController.class);

    @Autowired
    private WeChatService weChatService;

    /**
     * 微信验证自动回复消息签名
     */
    @GetMapping(value = "/wx/weixin")
    @VLog(title = "校验微信消息签名")
    public void checkSignature(HttpServletRequest request, HttpServletResponse response) {
        PrintWriter out = null;
        try {
            request.setCharacterEncoding("UTF-8");
            String signature = request.getParameter("signature");
            String timestamp = request.getParameter("timestamp");
            String nonce = request.getParameter("nonce");
            String echostr = request.getParameter("echostr");
            if (StringUtils.isEmptys(signature, timestamp, nonce, echostr)) {
                out = response.getWriter();
                out.write("error");
            }else{
                out = response.getWriter();
                if (WeChatUtil.checkSignature(signature, timestamp, nonce)) {
                    out.write(echostr);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
            logger.error("");
        } finally {
            out.close();
        }
    }

    /**
     * 处理用户发送的消息
     */
    @PostMapping(value = "/wx/weixin")
    @ResponseBody
    public String responseEvent(HttpServletRequest request) {
        return weChatService.processRequest(request);
    }
}

这里有两个方法,第一个是get请求方式,用于一会的接口校验,第二个用于处理用户发送的消息。

测试

1、这个映射工具之前文章里面有:传送
2、url必须以http://或https://开头,分别支持80端口和443端口。
3、Token必须为英文或数字,长度为3-32字符。

以上配置完成以后,往下拉会有一个测试号二维码,扫一下

这个关注公众号默认的回复就是我们刚刚配置的。