微信公告号实现原理简单介绍;

Posted SDingBa

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了微信公告号实现原理简单介绍;相关的知识,希望对你有一定的参考价值。

前段时间无聊玩了玩微信公告号的实现,现在简单介绍一下微信公告号的实现原理;
开发者模式:
开发者模式其实就是,使用自己的服务器,你可以选择任何一种后台web开发语言,我以java web的实现;
数据传递:
手机app微信客服端发送数据,数据先到微信服务器,然后微信服务器直

下面的json解析以JSONObject.fromObject(即JSONObject和JSONArray)的方式解析,最简单的方式,为简单案列使用的,对于复制项目,不推荐使用这种方式,建议使用gson包或者fastJSON包或者alibaba包,原理请自行查看

appID 和 appsecret;是在微信公告号官网可以查看到的,。我们利用他获取凭证,每获取的凭证有效期只能是2小时;
public final static String appID = “××××××××”;
public final static String appsecret = “××××××××”;
下面是获取平常或解析后的结果;

/**
     * 获取接口访问的凭证
     * @param appid
     * @param appsecret
     * @return Token的对象数据
     */
    public static Token getToken(String appid,String appsecret){
        Token token = null;
        String  requestUrl = token_url.replace("APPID", appid).replace(
                "APPSECRET",appsecret);

        JSONObject jsonObject = httpRequest(requestUrl, "GET", null);
         if(jsonObject != null){
             try {
                token = new Token();
                token.setAccessToken(jsonObject.getString("access_token"));
                token.setExpiresIn(jsonObject.getInt("expires_in"));
            } catch (Exception e) {
                token = null;
                logger.error("获取 token 失败,errcode : {} errmsg {}",
                        jsonObject.getInt("errcode"),jsonObject.getString("errmsg"));
            }

         }
        return token;

    }

对请求的封装:


    /**
     * 发送 https 请求
     * @param requestUrl  访问的url
     * @param requestMethod GET/POST
     * @param outputStr 是否有输出的数据流
     * @return JSON对象。服务器返回的数据,json对象
     */
    public static JSONObject httpRequest(String requestUrl,String requestMethod,
            String outputStr){
        JSONObject jsonObject = null;
        try {
            //创建SSL对象
            TrustManager[] tm = {new MyX509TrustManager()};
            SSLContext sslContext = SSLContext.getInstance("SSL","SunJSSE");
            sslContext.init(null,  tm, new SecureRandom());

            SSLSocketFactory ssf = sslContext.getSocketFactory();

            URL url = new URL(requestUrl);
            HttpsURLConnection conn =(HttpsURLConnection) url.openConnection();
            conn.setSSLSocketFactory(ssf);

            conn.setDoOutput(true);
            conn.setDoInput(true);
            conn.setUseCaches(false);
            //设置请求方式(get post)
            conn.setRequestMethod(requestMethod);
            //当 outputStr 不为null是 。想输出流写数据
            if(null != outputStr){
                OutputStream outputStream = conn.getOutputStream();
                outputStream.write(outputStr.getBytes("UTF-8"));
                outputStream.close();

            }
            //从输入流中读取返回的数据
            InputStream inputStream = conn.getInputStream();
            InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
            BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
            String str = "";
            StringBuffer buffer = new StringBuffer();
            while ((str = bufferedReader.readLine())!= null) {
                buffer.append(str);
            }
            //释放资源
            bufferedReader.close();
            inputStreamReader.close();
            inputStream.close();
            inputStream = null;
            conn.disconnect();
            jsonObject = JSONObject.fromObject(buffer.toString());




        } catch(ConnectException ceException){
            logger.error("链接超过时间:{}",ceException);
        }catch (Exception e) {
            logger.error("https 异常 :{}",e);
        }



        return jsonObject;

    }

有了凭证,就可以修改微信公告号的界面效果等很多功能了。比如修改界面;

/**
     * 创建 菜单
     * @param menu 菜单对象
     * @param accessToken 凭证
     * @return 返回是否 成功或者失败
     */
    public static boolean createMenu(Menu menu, String accessToken) {
        boolean result = false;
        String url = menu_create_url.replace("ACCESS_TOKEN", accessToken);
        // 菜单 对象 转化 成 json字符串
        String jsonMenu = JSONObject.fromObject(menu).toString();
        System.out.println(jsonMenu);
        // 发送post请求
        JSONObject jsonObject = CommonUtil.httpRequest(url, "POST", jsonMenu);

        if (jsonObject != null) {
            int errorCode = jsonObject.getInt("errcode");
            String errorMsg = jsonObject.getString("errmsg");
            if (0 == errorCode) {
                result = true;
            } else {
                result = false;
                logger.error("创建菜单失败 errorCode : {} errmsg : {}", errorCode,
                        errorMsg);
            }
        }

        return result;
    }
/**
     * 查询 菜单
     * @param accessToken
     * @return
     */
    public static String getMenu(String accessToken){
        String result = null;
        String requestUrl = menu_get_url.replace("ACCESS_TOKEN", accessToken);
        //发起get请求
        JSONObject jsonObject = CommonUtil.httpRequest(requestUrl, "GET", null);
        if(null != jsonObject){
            result = jsonObject.toString();
        }
        return result;
    }
/**
     * 删除 菜单 功能
     * @param accessToken
     * @return
     */
    public static boolean deleteMenu(String accessToken){
        boolean result = false;
        String requestUrl = menu_delete_url.replace("ACCESS_TOKEN",accessToken);
        // 
        JSONObject jsonObject = CommonUtil.httpRequest(requestUrl, "GET",null);
        if(jsonObject != null){
            int errorCode = jsonObject.getInt("errcode");
            String errorMsg = jsonObject.getString("errmsg");
            if(errorCode == 0){
                result = true;
            }else{
                result = false;
            }
        }       

        return result;
    }
// 创建菜单(post)
    public final static String menu_create_url = ""
            + "https://api.weixin.qq.com/cgi-bin/menu/create?access_token"
            + "=ACCESS_TOKEN";
    // 菜单查询(get)
    public final static String menu_get_url = "https://api.weixin.qq.com/cgi-bin/menu/get?access_token="
            + "ACCESS_TOKEN";
    // 菜单删除(GET)
    public final static String menu_delete_url = "https://api.weixin.qq.com/cgi-bin/menu/delete?access_token"
            + "=ACCESS_TOKEN";


下面说名一下 微信客服端访问的
创建自己的服务器,要在 微信公告号 处添加
URL(服务器地址);
Token(令牌);和其他的;
url地址写服务器的访问地址;
Token可以任意写,但是要和服务器代码里面的一样,因为微信的访问是安全加密的,通过一些列的加密后配对成功才可以发送接受消息;

 这是在代码里面写的;
 // 与接口 配置 信息的中 的 Token 要一致
private static String token = "AlphabetMan";

微信验证签名主要就是吧token和timetamp和nonce进行字典排序,然后进行sha1加密算法在转发成String;
网上找的实现原理:

/**
     * 验证签名 
     * @param signature
     * @param timestamp
     * @param nonce
     * @return
     */
    public static boolean checkSignature(String signature, String timestamp,
            String nonce) {
        String[] arr = new String[] { token, timestamp, nonce };
        // 将token timestamp nonce 三个参数进行字典排序
        Arrays.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 (Exception e) {
            e.printStackTrace();
        }
        content = null;
         return tmpStr != null ? tmpStr.equals(signature.toUpperCase()) : false;

    }
/**
     * 字节数组 将 字节数组 转化为 16 进制字符串
     * 
     * @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;

    }

    /**
     * 字节 将字节转化为 16 进制字符串
     * 
     * @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;
    }

在微信公告号逛网添加url的时候,会进行验证;微信官方规定以get方式进行访问;如下

/**
     * 请求校验 (确定请求来自微信服务器)
     */
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
         //微信加密签名
        String signature = req.getParameter("signature");
        String timestamp = req.getParameter("timestamp");
        String nonce = req.getParameter("nonce");
        String echostr = req.getParameter("echostr");
        System.out.println("get");
        PrintWriter out = resp.getWriter();
        if(SignUtil.checkSignature(signature,timestamp,nonce)){
            //调用核心服务类接收处理请求
            out.print(echostr);
        }
        out.close();
        out = null;
    }

微信服务器和微信手机app之间的通信,以及和自家服务器的通信规定是以xml文件的格式进行的传输;所有,要无时无刻不对xml文件解析;

官方文档:

文本消息
<xml>
 <ToUserName><![CDATA[toUser]]></ToUserName>
 <FromUserName><![CDATA[fromUser]]></FromUserName>
 <CreateTime>1348831860</CreateTime>
 <MsgType><![CDATA[text]]></MsgType>
 <Content><![CDATA[this is a test]]></Content>
 <MsgId>1234567890123456</MsgId>
 </xml>

对xml文件进行解析,方法很多,大约流行的有4中,自行了解,

public static Map<String, String> parseXml(HttpServletRequest request)
            throws Exception {
        // 将解析结果,存储在hashMap中,
        Map<String, String> map = new HashMap<String, String>();
        // 从 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 element : elementList) {
            map.put(element.getName(), element.getText());
        }
        inputStream.close();
        inputStream = null;

        return map;

    }

以及要有生成xml的函数,但是xml文件是(cdata)

    /**
     * 扩展 xsstream 使其支持 cdata
     */
    private static XStream xstream = new XStream(new XppDriver(){
        @Override
        public HierarchicalStreamWriter createWriter(Writer out) {
            // TODO Auto-generated method stub
            return new PrettyPrintWriter(out){
                boolean cdata = true;
                @Override
                public void startNode(String name, Class clazz) {
                    // TODO Auto-generated method stub
                    super.startNode(name, clazz);
                }

                @Override
                protected void writeText(QuickWriter writer, String text) {
                    if (cdata) {
                        writer.write("<![CDATA[");
                        writer.write(text);
                        writer.write("]]>");
//                      System.out.println(" cdate = true");
                    } else {
                        writer.write(text);
//                      System.out.println(" cdate = false");
                    }
                }

            };
        }
    });
/**
     * 文本消息对象,转化 XML
     */
    public static String messageToXml(TextMessage textMessage){
        xstream.alias("xml", textMessage.getClass());
        return  xstream.toXML(textMessage);
    }

    /**
     * 图像,消息对象 转化成 xml
     */
    public static String messageToXml(ImageMessage imageMessage){
        xstream.alias("xml", imageMessage.getClass());
        return xstream.toXML(imageMessage);
    }
    /**
     * 语音消息 --》 xml
     */
    public static String messageToXml(VoiceMessage voiceMessage){
        xstream.alias("xml", voiceMessage.getClass());
        return xstream.toXML(voiceMessage);
    }
    /**
     * 视频
     */
    public static String messageToXml(VideoMessage videoMessage){
        xstream.alias("xml", videoMessage.getClass());
        return xstream.toXML(videoMessage);
    }

当然上面的生成对应的要有对应的deam对象;

下面是最核心的类

public class CoreService {

    /**
     * 核心服务。处理 数据,并且换回数据。
     * @param request
     * @return
     */
    public static String processRequest(HttpServletRequest request) {
        // xml格式消息数据
        String respXml = null;
        // 默认返回文本消息内容
        String respContent = "未知数据类型";
        try {
            // 调用 parseXml 方法解析请求消息
            Map<String, String> requestMap = MessageUtil.parseXml(request);
            // 发送 账号
            String fromUserName = requestMap.get("FromUserName");
            // 开发着微信号
            String toUserName = requestMap.get("ToUserName");
            // 消息类型
            String msgType = requestMap.get("MsgType");

            // 回复文本消息
            TextMessage textMessage = new TextMessage();
            textMessage.setToUserName(fromUserName);
            textMessage.setFromUserName(toUserName);
            textMessage.setCreateTime(new Date().getTime());
            textMessage.setMsgType(MessageUtil.RESP_MESSAGE_TYPE_TEXT);

            /**
             * 信息类型
             */
            if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_TEXT)) {
                //TODO 乱码   。。。
                respContent = "你发送的是 文本 消息\n";
                respContent += "帅的回复是: " ;
//              if(requestMap.get("Content").equals("n")){
//                  System.out.println("xinagtong");
//              }else {
//                  System.out.println("不想同");
//              }

                if(requestMap.get("Content").equals("n")){
                    Article article = new Article();
                    article.setTitle("开源中国");
                    article.setDescription("开源中国社区成立于2008.8.8是目前最大的开源社区,"
                            + "\n\n 开源中国的dsfadfadsfadsfasdf多少发多发多少发多发的法规fgsfgaf"
                            + "dafsafad啊的发的发的嘎达"
                            + "dfadadsfadfa。\n\n"
                            + "dsjfkajdkfjad;f空间的发来快点就是拉福建阿斯顿;了大家撒裂缝空间"
                            + "打开来房间里的。");
                    article.setPicUrl("");
                    article.setUrl("http://m.oschina.net");
                    List<Article> articleList = new ArrayList<Article>();
                    articleList.add(article);
                    //创建图文消息
                    NewsMessage newsMessage = new NewsMessage();
                    newsMessage.setToUserName(fromUserName);
                    newsMessage.setFromUserName(toUserName);
                    newsMessage.setCreateTime(new Date().getTime());;
                    newsMessage.setMsgType(MessageUtil.RESP_MESSAGE_TYPE_NEWS);
                    newsMessage.setArticleCount(articleList.size());
                    newsMessage.setArticles(articleList);
                    respXml = MessageUtil.messageToXml(newsMessage);
                    return respXml;
                }else{
                    //TODO 存在的问题是,有空个的时候,会出现无法返回数据,
                    //提示,该公共号暂时无法提供服务,请稍后再试。
                    String title = URLDecoder.decode(requestMap.get("Content"), "utf-8");
                    title = title.replaceAll(" ", "");
                    respContent  += new TulingController().getTulingRe(title);
                }

//              respContent = URLEncoder.encode(respContent, "UTF-8");
//              respContent = "luan ma ??";
            } else if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_IMAGE)) {
                respContent = "你发送的是 图片 消息\n";
                respContent += "n : 消息推送\n"
                                        + "k : 没啥\n";
            } else if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_VOICE)) {
                respContent = "你发送的是 语音 消息";
            } else if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_VIDEO)) {
                respContent = "你发送的是 视频 消息";
            } else if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_LOCATION)) {
                respContent = "你发送的是 地址 消息";
            } else if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_LINK)) {
                respContent = "你发送的是链接片 消息";
                //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
            }   else if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_EVENT)) {                      //事件推送
                //事件类型
                String eventType = requestMap.get("Event");

                //关注
                if (eventType.equals(MessageUtil.EVENT_MESSAGE_TYPE_SUBSCRIBE)) {
                    respContent = "hello ,欢迎关注公告号,我们致力打招最好的东西给你,"
                            + "从现在开始,以修复部公共 20160331 !!!" ;
                    textMessage.setContent(respContent);
                    respXml = MessageUtil.messageToXml(textMessage);
                    return respXml;
                    //将消息对象转化成xml
                    //respXml = MessageUtil.messageToXml(textMessage);
                }else if (eventType.equals(MessageUtil.EVENT_MESSAGE_TYPE_UNSUBSCRIBE)) {
                     //TODO 取消订阅后,用户不会在收到公共账号发送的消息,因此不需要回复
                }else if (eventType.equals(MessageUtil.EVENT_MESSAGE_TYPE_SCAN) ){
                     //TODO 处理二维码扫描事件;
                    respContent = "二维码扫";
                }else if (eventType.equals(MessageUtil.EVENT_MESSAGE_TYPE_LOCATION)) {
                     //TODO 处理上报的地理位置事件
                    respContent = "地理位置事件";
                }else if (eventType.equals(MessageUtil.EVENT_MESSAGE_TYPE_CLICK)) {
                     //TODO 处理菜单单击事件
                    //事件key值,与创建菜单的key值对应

                    String eventKey = requestMap.get("EventKey");
                    if(eventKey.equals("oschina")){

                        Article article = new Article();
                        article.setTitle("开源中国");
                        article.setDescription("开源中国社区成立于2008.8.8是目前最大的开源社区,"
                                + "\n\n 开源中国的dsfadfadsfadsfasdf多少发多发多少发多发的法规fgsfgaf"
                                + "dafsafad啊的发的发的嘎达"
                                + "dfadadsfadfa。\n\n"
                                + "dsjfkajdkfjad;f空间的发来快点就是拉福建阿斯顿;了大家撒裂缝空间"
                                + "打开来房间里的。");
                        article.setPicUrl("");
                        article.setUrl("http://m.oschina.net");

                        Article article2 = new Article();
                        article2.setTitle("开源中国");
                        article2.setDescription("dkfajldjfjgj;ajdfljd 安静就放假啊的积分卡倒计时疯狂"
                                + "的奶粉克拉克;了"
                                + "的刷卡的激发4"
                                + "报告发掘地根据"
                                + "建安费; "
                                + "的深刻了激发的经费拉附近路东方了看见");
                        article2.setPicUrl("");

                        article2.setUrl("");


                        List<Article> articleList = new ArrayList<Article>();

                        articleList.add(article);
                        articleList.add(article2);
                        //创建图文消息
                        NewsMessage newsMessage = new NewsMessage();
                        newsMessage.setToUserName(fromUserName);
                        newsMessage.setFromUserName(toUserName);
                        newsMessage.setCreateTime(new Date().getTime());;
                        newsMessage.setMsgType(MessageUtil.RESP_MESSAGE_TYPE_NEWS);
                        newsMessage.setArticleCount(articleList.size());
                        newsMessage.setArticles(articleList);


                        respXml = MessageUtil.messageToXml(newsMessage);
                        return respXml;

                    }else if (eventKey.equals("iteye")) {
                        respContent = "iteye  事件";
                        textMessage.setContent("Iteye 创办于 2003,9.javaEye,从最初的讨论java技术为主的技术"
                                + "论坛,已朱静发展成为涵盖整个软件开发领域的综合性网站、\n\n"
                                + "http://www.iteye.com");
                        respXml = MessageUtil.messageToXml(textMessage);
                        return respXml;
                    }
//                  else {
//                      respContent = "else limian ";
//                      //设置文本消息 内容
////                        textMessage.setContent("sdsdasda");
////                        //将文本消息转化成xml
////                        respXml = MessageUtil.messageToXml(textMessage);
//                  }
//                  respContent = "ccccc";
                }
            }



//          //设置文本消息 内容
            textMessage.setContent(respContent);
            //将文本消息转化成xml
            respXml = MessageUtil.messageToXml(textMessage);

        } catch (Exception e) {
            e.printStackTrace();
        }

        return respXml;
    }
}

如果对数据进行处理和封装的入口就是这个类。
通过if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_TEXT)) 判断发送来的消息是那种格式的,获取后,在放回给用户即可,

比如,以 加载一个机器人案列,我们使用图灵机器人的接口;

在判断完是字符串的信息后,访问图灵的接口;

String title = URLDecoder.decode(requestMap.get("Content"), "utf-8");
                    title = title.replaceAll(" ", "");
                    respContent  += new TulingController().getTulingRe(title);
                    //respContent是最后封装返回的数据;
public String getTulingRe(String info){
        //调用图灵机器人接口api,获取结果
                //http://www.tuling123.com/openapi/api   key:42bca29888818ceea7a214eaadbeb9e7
//              String url = "http://www.tuling123.com/openapi/api?key=需要去图灵官网注册获取&info="+info;
         String url = "http://www.tuling123.com/openapi/api?key=42bca29××××××××××××××××××××××××&info="+info;
        String tlResult = HttpGetRequest.get(url);

        //瑙f瀽鍥剧伒缁撴灉鏁版嵁锛屾彁鍙栨墍闇?唴瀹?
        JSONObject json = JSONObject.fromObject(tlResult);
        tlResult = json.getString("text");

        return tlResult;

    }


public static String get(String url){
        try{
            HttpGet request = new HttpGet(url);
            //执行http get请求
            HttpResponse response = HttpClients.createDefault().execute(request);

            //根据返回码判断返回是否成功
            String result = "";
            if(response.getStatusLine().getStatusCode() == 200){
                result = EntityUtils.toString(response.getEntity());
            }
            return result;
        }catch(Exception e){
            e.printStackTrace();
            return "";
        }
    }

ok

以上是关于微信公告号实现原理简单介绍;的主要内容,如果未能解决你的问题,请参考以下文章

如何实现从Android第三方平台推送微信公众号

利用微信公众号实现商品的展示和支付

微信啥小程序可以发公告文章

Django实现微信公众号简单自动回复(复读机)

微信消息推送神器pushplus介绍,让消息推送更简单

微信消息推送神器pushplus介绍,让消息推送更简单