Java微信公众号扫描带参数二维码事件(消息加密)

时间:2024-02-16 08:45:03

最近又开始了微信二维码的开发,开个帖子记录一下


一、获取access_token

首先我们需要获取access_token,有效期7200s,一天获取上限为2000次

    public static String getWXAccessToken() {
        String accessTokenUrl = "https://api.weixin.qq.com/cgi-bin/token?" +
                "grant_type=client_credential" +
                // 此处填写你自己的appid
                "&appid=" + WXConstants.APPID +
                // 此处填写你自己的appsecret
                "&secret=" + WXConstants.APPSECRET;
        JSONObject jsonObject = HttpUtils.httpsRequest(accessTokenUrl, "GET", null);
        return (String) jsonObject.get("access_token");
    }

HttpUtils

    /**
     * 发送https请求
     * @param requestUrl 请求地址
     * @param requestMethod 请求方式(GET、POST)
     * @param data 提交的数据
     * @return JSONObject(通过JSONObject.get(key)的方式获取json对象的属性值)
     */
    public static JSONObject httpsRequest(String requestUrl, String requestMethod, String data) {
        JSONObject jsonObject = null;
        InputStream inputStream = null;
        InputStreamReader inputStreamReader = null;
        BufferedReader bufferedReader = null;
        try {
            URL url = new URL(requestUrl);
            HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
            conn.setDoOutput(true);
            conn.setDoInput(true);
            conn.setUseCaches(false);
            // 设置请求方式(GET/POST)
            conn.setRequestMethod(requestMethod);
            conn.connect();
            // 当data不为null时向输出流写数据
            if (null != data) {
                // getOutputStream方法隐藏了connect()方法
                OutputStream outputStream = conn.getOutputStream();
                // 注意编码格式
                outputStream.write(data.getBytes("UTF-8"));
                outputStream.close();
            }
            // 从输入流读取返回内容
            inputStream = conn.getInputStream();
            inputStreamReader = new InputStreamReader(inputStream, "utf-8");
            bufferedReader = new BufferedReader(inputStreamReader);
            String str = null;
            StringBuffer buffer = new StringBuffer();
            while ((str = bufferedReader.readLine()) != null) {
                buffer.append(str);
            }
            conn.disconnect();
            jsonObject = JSONObject.fromObject(buffer.toString());
            return jsonObject;
        } catch (Exception e) {
            logger.error("发送https请求失败,失败", e);
            return null;
        } finally {
            // 释放资源
            try {
                if(null != inputStream) {
                    inputStream.close();
                }
                if(null != inputStreamReader) {
                    inputStreamReader.close();
                }
                if(null != bufferedReader) {
                    bufferedReader.close();
                }
            } catch (IOException e) {
                logger.error("释放资源失败,失败", e);
            }
        }
    }

重点内容

  • 我们需要现在微信公众平台设置 IP白名单,也就是本机的外网IP地址,否则无法获取access_token
    这里写图片描述

二、获取微信公众号二维码

  • 微信提供了两种二维码
    • 一种是临时二维码,没有个数限制
    • 一种是永久二维码,有个数限制,最多为10w个
/**
     * 获取微信公众号二维码
     * @param codeType 二维码类型 "1": 临时二维码  "2": 永久二维码
     * @param sceneId 场景值ID
     * @param fileName 图片名称
     */
    public static void getWXPublicQRCode(String codeType, Integer sceneId, String fileName) {
        String wxAccessToken = getWXAccessToken();
        Map<String, Object> map = new HashMap<>();
        if ("1".equals(codeType)) { // 临时二维码
            map.put("expire_seconds", 604800);
            map.put("action_name", "QR_SCENE");
            Map<String, Object> sceneMap = new HashMap<>();
            Map<String, Object> sceneIdMap = new HashMap<>();
            sceneIdMap.put("scene_id", sceneId);
            sceneMap.put("scene", sceneIdMap);
            map.put("action_info", sceneMap);
        } else if ("2".equals(codeType)) { // 永久二维码
            map.put("action_name", "QR_LIMIT_SCENE");
            Map<String, Object> sceneMap = new HashMap<>();
            Map<String, Object> sceneIdMap = new HashMap<>();
            sceneIdMap.put("scene_id", sceneId);
            sceneMap.put("scene", sceneIdMap);
            map.put("action_info", sceneMap);
        }
        String data = JSON.toJSONString(map);
        // 得到ticket票据,用于换取二维码图片
        JSONObject jsonObject = HttpUtils.httpsRequest("https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=" + wxAccessToken, "POST", data);
        String ticket = (String) jsonObject.get("ticket");
        // WXConstants.QRCODE_SAVE_URL: 填写存放图片的路径
        HttpUtils.httpsRequestPicture("https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=" + URLEncoder.encode(ticket),
            "GET", null, WXConstants.QRCODE_SAVE_URL, fileName, "png");
    }

HttpUtils

 /**
     * 发送https请求,返回二维码图片
     * @param requestUrl 请求地址
     * @param requestMethod 请求方式(GET、POST)
     * @param data 提交的数据
     * @param savePath 图片保存路径
     * @param fileName 图片名称
     * @param fileType 图片类型
     */
    public static void httpsRequestPicture(String requestUrl, String requestMethod, String data, String savePath, String fileName, String fileType) {
        InputStream inputStream = null;
        try {
            URL url = new URL(requestUrl);
            HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
            conn.setDoOutput(true);
            conn.setDoInput(true);
            conn.setUseCaches(false);
            //设置请求方式(GET/POST)
            conn.setRequestMethod(requestMethod);
            conn.connect();
            //当data不为null时向输出流写数据
            if (null != data) {
                //getOutputStream方法隐藏了connect()方法
                OutputStream outputStream = conn.getOutputStream();
                //注意编码格式
                outputStream.write(data.getBytes("UTF-8"));
                outputStream.close();
            }
            // 从输入流读取返回内容
            inputStream = conn.getInputStream();
            logger.info("开始生成微信二维码...");
            WXPayUtils.inputStreamToMedia(inputStream, savePath, fileName, fileType);
            logger.info("微信二维码生成成功!!!");
            conn.disconnect();
        } catch (Exception e) {
            logger.error("发送https请求失败,失败", e);
        }finally {
            //释放资源
            try {
                if(null != inputStream) {
                    inputStream.close();
                }
            } catch (IOException e) {
                logger.error("释放资源失败,失败", e);
            }
        }
    }

WXPayUtils

    /**
     * 将输入流转换为图片
     * @param input 输入流
     * @param savePath 图片需要保存的路径
     * @param fileType 图片类型
     */
    public static void inputStreamToMedia(InputStream input, String savePath, String fileName, String fileType) throws Exception {
        String filePath = savePath + "/" + fileName + "." + fileType;
        File file = new File(filePath);
        FileOutputStream outputStream = new FileOutputStream(file);
        int length;
        byte[] data = new byte[1024];
        while ((length = input.read(data)) != -1) {
            outputStream.write(data, 0, length);
        }
        outputStream.flush();
        outputStream.close();
    }

重点内容

  • 上文中的 sceneId 即为我们可以携带的参数
    • 临时二维码时为32位非0整型,永久二维码时最大值为100000(目前参数只支持1–100000)

三、扫描二维码返回指定内容

    /**
     * 处理微信公众号请求信息
     * @param request
     * @return
     */
    @RequestMapping("/wxpublic/verify_wx_token")
    @ResponseBody
    public String handlePublicMsg(HttpServletRequest request) throws Exception {
        // 获得微信端返回的xml数据
        InputStream is = null;
        InputStreamReader isr = null;
        BufferedReader br = null;
        try {
            is = request.getInputStream();
            isr = new InputStreamReader(is, "utf-8");
            br = new BufferedReader(isr);
            String str = null;
            StringBuffer returnXml= new StringBuffer();
            while ((str = br.readLine()) != null) {
                //返回的是xml数据
                returnXml.append(str);
            }
            Map<String, String> encryptMap = WXPayUtils.xmlToMap(returnXml);
            // 得到公众号传来的加密信息并解密,得到的是明文xml数据
            String decryptXml = WXPublicUtils.decrypt(encryptMap.get("Encrypt"));
            // 将xml数据转换为map
            Map<String, String> decryptMap = WXPayUtils.xmlToMap(decryptXml);

            // 区分消息类型
            String msgType = decryptMap.get("MsgType");
            // 普通消息
            if ("text".equals(msgType)) { // 文本消息
                // todo 处理文本消息
            } else if ("image".equals(msgType)) { // 图片消息
                // todo 处理图片消息
            } else if ("voice".equals(msgType)) { //语音消息
                // todo 处理语音消息
            } else if ("video".equals(msgType)) { // 视频消息
                // todo 处理视频消息
            } else if ("shortvideo".equals(msgType)) { // 小视频消息
                // todo 处理小视频消息
            } else if ("location".equals(msgType)) { // 地理位置消息
                // todo 处理地理位置消息
            } else if ("link".equals(msgType)) { // 链接消息
                // todo 处理链接消息
            }
            // 事件推送
            else if ("event".equals(msgType)) { // 事件消息
                // 区分事件推送
                String event = decryptMap.get("Event");
                if ("subscribe".equals(event)) { // 订阅事件 或 未关注扫描二维码事件
                    // 返回消息时ToUserName的值与FromUserName的互换
                    Map<String, String> returnMap = new HashMap<>();
                    returnMap.put("ToUserName", decryptMap.get("FromUserName"));
                    returnMap.put("FromUserName", decryptMap.get("ToUserName"));
                    returnMap.put("CreateTime", new Date().getTime()+"");
                    returnMap.put("MsgType", "text");
                    returnMap.put("Content", "https://www.baidu.com");
                    String encryptMsg = WXPublicUtils.encryptMsg(WXPayUtils.mapToXml(returnMap), new Date().getTime()+"", WXPublicUtils.getRandomStr());
                    return encryptMsg;
                }  else if ("unsubscribe".equals(event)) { // 取消订阅事件
                    // todo 处理取消订阅事件
                } else if ("SCAN".equals(event)) { // 已关注扫描二维码事件
                    // 返回消息时ToUserName的值与FromUserName的互换
                    Map<String, String> returnMap = new HashMap<>();
                    returnMap.put("ToUserName", decryptMap.get("FromUserName"));
                    returnMap.put("FromUserName", decryptMap.get("ToUserName"));
                    returnMap.put("CreateTime", new Date().getTime()+"");
                    returnMap.put("MsgType", "text");
                    returnMap.put("Content", "https://www.baidu.com");
                    String encryptMsg = WXPublicUtils.encryptMsg(WXPayUtils.mapToXml(returnMap), new Date().getTime()+"", WXPublicUtils.getRandomStr());
                    return encryptMsg;
                } else if ("LOCATION".equals(event)) { // 上报地理位置事件
                    // todo 处理上报地理位置事件
                } else if ("CLICK".equals(event)) { // 点击菜单拉取消息时的事件推送事件
                    // todo 处理点击菜单拉取消息时的事件推送事件
                } else if ("VIEW".equals(event)) { // 点击菜单跳转链接时的事件推送
                    // todo 处理点击菜单跳转链接时的事件推送
                }
            }
        } catch (Exception e) {
            logger.error("处理微信公众号请求信息,失败", e);
        } finally {
            if (null != is) {
               is.close();
            }
            if (null != isr) {
                isr.close();
            }
            if (null != br) {
                br.close();
            }   
        }
        return null;
    }

WXPayUtils

    /**
     * XML格式字符串转换为Map
     *
     * @param strXML XML字符串
     * @return XML数据转换后的Map
     * @throws Exception
     */
    public static Map<String, String> xmlToMap(String strXML) throws Exception {
        try {
            Map<String, String> data = new HashMap<>();
            DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
            DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
            InputStream stream = new ByteArrayInputStream(strXML.getBytes("UTF-8"));
            org.w3c.dom.Document doc = documentBuilder.parse(stream);
            doc.getDocumentElement().normalize();
            NodeList nodeList = doc.getDocumentElement().getChildNodes();
            for (int idx = 0; idx < nodeList.getLength(); ++idx) {
                Node node = nodeList.item(idx);
                if (node.getNodeType() == Node.ELEMENT_NODE) {
                    org.w3c.dom.Element element = (org.w3c.dom.Element) node;
                    data.put(element.getNodeName(), element.getTextContent());
                }
            }
            try {
                stream.close();
            } catch (Exception ex) {
                // do nothing
            }
            return data;
        } catch (Exception ex) {
            WXPayUtils.getLogger().warn("Invalid XML, can not convert to map. Error message: {}. XML content: {}", ex.getMessage(), strXML);
            throw ex;
        }
    }

    /**
     * 将Map转换为XML格式的字符串
     *
     * @param data Map类型数据
     * @return XML格式的字符串
     * @throws Exception
     */
    public static String mapToXml(Map<String, String> data) throws Exception {
        DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
        DocumentBuilder documentBuilder= documentBuilderFactory.newDocumentBuilder();
        org.w3c.dom.Document document = documentBuilder.newDocument();
        org.w3c.dom.Element root = document.createElement("xml");
        document.appendChild(root);
        for (String key: data.keySet()) {
            String value = data.get(key);
            if (value == null) {
                value = "";
            }
            value = value.trim();
            org.w3c.dom.Element filed = document.createElement(key);
            filed.appendChild(document.createTextNode(value));
            root.appendChild(filed);
        }
        TransformerFactory tf = TransformerFactory.newInstance();
        Transformer transformer = tf.newTransformer();
        DOMSource source = new DOMSource(document);
        transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
        transformer.setOutputProperty(OutputKeys.INDENT, "yes");
        StringWriter writer = new StringWriter();
        StreamResult result = new StreamResult(writer);
        transformer.transform(source, result);
        String output = writer.getBuffer().toString(); //.replaceAll("\n|\r", "");
        try {
            writer.close();
        } catch (Exception ex) {

        }
        return output;
    }

重点内容

  • 由于我开启了信息加密,所以微信发送给我的消息是加密的,同理,我这边返回给微信的消息也需要加密,如何嫌麻烦的话,可以将 消息加解密方式 设置为 明文模式
  • 消息加解密的方法有点多,我把它压缩上传了,有需要的小伙伴可以自行下载
  • 通过 decryptMap.get(“EventKey”) 即可得到二维码携带的参数

四、最终效果

不出意外的话,当扫描二维码的时候,公众号上会返回 https://www.baidu.com
这里写图片描述
至此,就大功告成了!!!