微信公众号对接记录java版本

微信公众号对接记录java版本

点点

2021-04-28 21:43 阅读 267 喜欢 0

开发过程中使用的测试号进行开发,需要先配置一些地址信息等。 这里开发需要使用外网,用的是frp.

相关地址

测试号: http://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=sandbox/login&token=217603174&lang=zh_CN 微信官网:http://mp.weixin.qq.com 开发者文档:https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1445241432

流程介绍

URL 为微信调用的地址,在这里,微信服务器会发送信息到这个地址中,我们要做的就是正确的返回微信的token校验。 微信的token校验为GET访问,示例代码如下: WeChatController.java

 /***
 * 验证wechat token 地址,根据接受的数据进行字典加密后如果正确返回echostr
 * @url /wechat/validateToken
 * @param signature
 * @param timestamp
 * @param nonce
 * @param echostr
 * @return
 */
@GetMapping("/api")
@ResponseBody
public String validateToken(String signature,String timestamp,String nonce,String echostr) {
    try {
        if(SignUtil.checkSignature(signature, timestamp, nonce)) {
            return echostr;
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
    return "token validate failed!";
}

验证token的工具类,SignUtil.java.

 /** 
 * 验证签名 
 *  
 * @param signature 
 * @param timestamp 
 * @param nonce 
 * @return 
 */  
public static boolean checkSignature(String signature, String timestamp, String nonce) {  
    String[] arr = new String[] { ConstantUtil.WECHAT_TOKEN/**该值为上方我们自己填写的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 (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;  
}  

启动我们的项目,同时启动frp,在浏览器中访问对应的域名地址后校验token就可以啦。

js安全域名

这个域名是我们用来开发web的时候微信校验使用的,暂时先填写一个就可以,需要注意的地方是:该域名只写域名不带协议,也就是http https 等都不用写入,例如:demo.chrunlee.cn 即可。

token获取

接下来,我们就要开始获取token,通过token来调用微信的接口获取数据。关于如何保存token这个方法比较多,存文件、session、数据库等都可以,只要尽可能的保证token有效即可。

access_token是公众号的全局唯一接口调用凭据,公众号调用各接口时都需使用access_token。开发者需要进行妥善保存。access_token的存储至少要保留512个字符空间。access_token的有效期目前为2个小时,需定时刷新,重复获取将导致上次获取的access_token失效。 我们可以做定时刷新,也可以在使用的时候校验超期时间来刷新,总之token的获取次数每天是有限的,如果超过后,当天就无法获取了哦,测试的时候一定要注意了。

token的获取很简单,按照官方文档调去API接口即可。 WeixinUtil.java

  // 获取access_token的接口地址(GET) 限200(次/天)
public final static String access_token_url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET";

/**
 * 获取access_token
 * 
 * @param appid 凭证
 * @param appsecret 密钥
 * @return
 */
public static AccessToken getAccessToken(String appid, String appsecret) {
    AccessToken accessToken = null;

    String requestUrl = access_token_url.replace("APPID", appid).replace("APPSECRET", appsecret);
    JSONObject jsonObject = httpRequest(requestUrl, "GET", null);
    // 如果请求成功
    if (null != jsonObject) {
        try {
            accessToken = new AccessToken();
            accessToken.setToken(jsonObject.getString("access_token"));
            accessToken.setExpiresIn(jsonObject.getInt("expires_in"));
        } catch (JSONException e) {
            accessToken = null;
            // 获取token失败
            log.error("获取token失败 errcode:{} errmsg:{}", jsonObject.getInt("errcode"), jsonObject.getString("errmsg"));
        }
    }
    return accessToken;
}
/**
 * 发起https请求并获取结果
 * 
 * @param requestUrl 请求地址
 * @param requestMethod 请求方式(GET、POST)
 * @param outputStr 提交的数据
 * @return JSONObject(通过JSONObject.get(key)的方式获取json对象的属性值)
 */
public static JSONObject httpRequest(String requestUrl, String requestMethod, String outputStr) {
    JSONObject jsonObject = null;
    StringBuffer buffer = new StringBuffer();
    try {
        // 创建SSLContext对象,并使用我们指定的信任管理器初始化
        TrustManager[] tm = { new MyX509TrustManager() };
        SSLContext sslContext = SSLContext.getInstance("SSL", "SunJSSE");
        sslContext.init(null, tm, new java.security.SecureRandom());
        // 从上述SSLContext对象中得到SSLSocketFactory对象
        SSLSocketFactory ssf = sslContext.getSocketFactory();

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

        httpUrlConn.setDoOutput(true);
        httpUrlConn.setDoInput(true);
        httpUrlConn.setUseCaches(false);

        // 设置请求方式(GET/POST)
        httpUrlConn.setRequestMethod(requestMethod);

        if ("GET".equalsIgnoreCase(requestMethod))
            httpUrlConn.connect();

        // 当有数据需要提交时
        if (null != outputStr) {
            OutputStream outputStream = httpUrlConn.getOutputStream();
            // 注意编码格式,防止中文乱码
            outputStream.write(outputStr.getBytes("UTF-8"));
            outputStream.close();
        }


        // 将返回的输入流转换成字符串
        InputStream inputStream = httpUrlConn.getInputStream();
        InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "utf-8");
        BufferedReader bufferedReader = new BufferedReader(inputStreamReader);

        String str = null;
        while ((str = bufferedReader.readLine()) != null) {
            buffer.append(str);
        }
        bufferedReader.close();
        inputStreamReader.close();
        // 释放资源
        inputStream.close();
        inputStream = null;
        httpUrlConn.disconnect();
        jsonObject = JSONObject.fromObject(buffer.toString());
    } catch (ConnectException ce) {
        log.error("Weixin server connection timed out.");
    } catch (Exception e) {
        log.error("https request error:{}", e);
    }
    return jsonObject;
}

接受消息

当用户在公众号发送消息的时候,微信会将消息内容转发到我们的服务器地址上,地址则是第一步我们配置的接口地址。 微信发送的数据是以xml的格式来的,所以我们要解析xml数据,具体格式参考:微信官方文档#消息管理。

这里只是做了普通文本消息的整理,其他的暂未用到,可以参考下。 MessageUtil.java

package com.boyuyun.wechat.util;

import java.io.InputStream;
import java.io.Writer;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;

import org.apache.commons.net.nntp.Article;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;

import com.boyuyun.wechat.resp.ImageMessage;
import com.boyuyun.wechat.resp.TextMessage;
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.core.util.QuickWriter;
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
import com.thoughtworks.xstream.io.xml.PrettyPrintWriter;
import com.thoughtworks.xstream.io.xml.XppDriver;


/**
* 消息处理工具类
* 
*/
public class MessageUtil {

// 请求消息类型:文本
    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";

    // 事件类型:subscribe(订阅)
    public static final String EVENT_TYPE_SUBSCRIBE = "subscribe";
    // 事件类型:unsubscribe(取消订阅)
    public static final String EVENT_TYPE_UNSUBSCRIBE = "unsubscribe";
    // 事件类型:scan(用户已关注时的扫描带参数二维码)
    public static final String EVENT_TYPE_SCAN = "scan";
    // 事件类型:LOCATION(上报地理位置)
    public static final String EVENT_TYPE_LOCATION = "LOCATION";
    // 事件类型:CLICK(自定义菜单)
    public static final String EVENT_TYPE_CLICK = "CLICK";

    // 响应消息类型:文本
    public static final String RESP_MESSAGE_TYPE_TEXT = "text";
    // 响应消息类型:图片
    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 RESP_MESSAGE_TYPE_MUSIC = "music";
    // 响应消息类型:图文
    public static final String RESP_MESSAGE_TYPE_NEWS = "news";

    /*   新加变量    */
    public static final String RESP_MESSAGE_TYPE_VIEW = "view";

    /**
     * 扩展xstream使其支持CDATA
     */
    /*private static XStream xstream = new XStream(new XppDriver() {
        public HierarchicalStreamWriter createWriter(Writer out) {
            return new PrettyPrintWriter(out) {
                // 对所有xml节点的转换都增加CDATA标记
                boolean cdata = true;

                public void startNode(String name, Class clazz) {
                    super.startNode(name, clazz);
                }

                protected void writeText(QuickWriter writer, String text) {
                    if (cdata) {
                        writer.write("<![CDATA[");
                        writer.write(text);
                        writer.write("]]>");
                    } else {
                        writer.write(text);
                    }
                }
            };
        }
    });*/
    /**
     * 解析微信发来的请求(XML)
     * 
     * @param request
     * @return Map<String, String>
     * @throws Exception
     */
    @SuppressWarnings("unchecked")
    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 e : elementList)
            map.put(e.getName(), e.getText());

        // 释放资源
        inputStream.close();
        inputStream = null;

        return map;
    }



    /**
     * 文本消息对象转换成xml
     * 
     * @param textMessage 文本消息对象
     * @return xml
     */
    public static String messageToXml(TextMessage textMessage) {
        XStream xstream = new XStream();
        xstream.alias("xml", textMessage.getClass());
        return xstream.toXML(textMessage);
    }

    /**
     * 图片消息对象转换成xml
     * 
     * @param imageMessage 图片消息对象
     * @return xml
     */
    public static String messageToXml(ImageMessage imageMessage) {
        XStream xstream = new XStream();
        xstream.alias("xml", imageMessage.getClass());
        return xstream.toXML(imageMessage);
    }
 }

包括用户发送的消息、用户关注或取消关注都会触发调用该接口。 返回消息的时候,只需要对照官网文档中的xml结构返回数据即可。 关于封装可以参考MessageUtil.java中的messageToXml函数。

获取用户信息

在用户关注我们的公众号的时候,就可以通过接口获取到用户的基本信息啦。当然,后续的网页请求也会让用户去授权,同时也可以拿到用户信息。 以下为根据用户的openId 和 token 来获得用户的基本信息。 WeixinUtil.java

 //根据openId 获取微信用户基本信息
public static String SERVER_USERINFO_URL = "https://api.weixin.qq.com/cgi-bin/user/info?access_token=${tokenStr}&openid=${openId}&lang=zh_CN";

/***
 * 根据用户的openId 和 token 获取用户基本信息
 * @param token
 * @param openId
 * @return
 */
public static WeiLoginInfoPo getUserInfoByServer(String token,String openId) {
    String requestUrl = SERVER_USERINFO_URL.replace("${tokenStr}", token).replace("${openId}", openId);
    JSONObject jo = httpRequest(requestUrl, "GET", null);
    //将json转成对象返回。
    System.out.println(jo.toString());
    JSONArray ja = jo.getJSONArray("tagid_list");
    String tagIdStr = "";
    for (Iterator iterator = ja.iterator(); iterator.hasNext();) {
        String object = (String) iterator.next();
        tagIdStr += object;
    }
    WeiLoginInfoPo info = new WeiLoginInfoPo();
    info.setSubscribe(jo.getInt("subscribe"));
    info.setOpenId(jo.getString("openid"));
    info.setNickName(jo.getString("nickname"));
    info.setSex(jo.getInt("sex"));
    info.setLanguage(jo.getString("language"));
    info.setCity(jo.getString("city"));
    info.setProvince(jo.getString("province"));
    info.setCountry(jo.getString("country"));
    info.setHeadimgurl(jo.getString("headimgurl"));
    info.setSubscribe_time(jo.getLong("subscribe_time"));
    info.setRemark(jo.getString("remark"));
    info.setGroupid(jo.getInt("groupid"));
    info.setTagidList(tagIdStr);
    info.setSubscribe_scene(jo.getString("subscribe_scene"));
    info.setQr_scene(jo.getInt("qr_scene"));
    info.setQr_scene_str(jo.getString("qr_scene_str"));
    return info;
}

具体官方文档可参考:微信官方文档#获取用户基本信息 用户基本信息获取后可以保存在数据库中,用于后续的身份校验或记录。

数据走向

接下来用户就开始进入我们的应用界面啦,大体的流程如下:

用户点击链接进入我们的系统后台 后台去跳转oauth2地址,去校验用户认证信息 微信调用回调地址到我们系统中,此时我们就可以获取web token数据,然后获取js ticket。 获得ticket后进行存储,返回到页面端,提供给weixin.js来使用。 此时用户访问的页面中,我们就可以通过js sdk来进行调用微信的接口函数啦。

网页对接

关于网页授权(与服务器授权基本类似)可以参考微信官方文档#网页授权

我这边的代码会掺杂一些我的业务代码,仅供参考:

//通过accesstoken 获取js-sdk 开发的ticket
public static String WEB_JS_TICKET_URL = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=${ACCESS_TOKEN}&type=jsapi";
/***
 * 通过token获得js-sdk 的ticket,以accessToken的形式返回
 * @param token
 * @return
 */
public AccessToken getTicketByToken(String token) {
    String requestUrl = WEB_JS_TICKET_URL.replace("${ACCESS_TOKEN}", token);
    JSONObject jo = httpRequest(requestUrl, "GET", null);
    //将json转成对象返回。
    System.out.println(jo.toString());
    String ticket = jo.getString("ticket");
    Integer expireIn = jo.getInt("expires_in");
    AccessToken accessToken = new AccessToken();
    accessToken.setExpiresIn(expireIn);
    accessToken.setToken(ticket);
    System.out.println("---用户进入页面获得ticket : "+ticket);
    return accessToken;
}
/**
 * 根据传递过来的schoolWeixin 数据产生ticket并返回
 * @return
 */
public JsTicket getTicketBySchool(SchoolWeixin school,String url) {
    /**
     * 1.校验ticket是否过期,如果过期,通过token生成并更新
     * 2.拿到ticket,然后随机生成数据,进行前面
     * 3.返回数据内容
     */
    String ticket = school.getTicket();
    Date ticketdate = school.getTicketdate();
    Date now = new Date();
    JsTicket jsTicket = new JsTicket();
    if(null == ticketdate || ticketdate.getTime() < now.getTime()) {
        //重新拿ticket
        String token = getTokenStr(school);
        AccessToken tokenPo = getTicketByToken(token);
        //更新token
        Date now2 = new Date();
        Date ticketdate2 = new Date(now2.getTime() + tokenPo.getExpiresIn()* 1000);
        school.setTicket(tokenPo.getToken());
        school.setTicketdate(ticketdate2);
        weService.updateSchoolTicket(school);
        jsTicket.setTicket(tokenPo.getToken());
    }else {
        jsTicket.setTicket(ticket);
    }

    String noncestr = getRandomStr(6);//随机字符串四五位
    Long timestamp = new Date().getTime();
    String timestampstr = timestamp.toString();
    //计算signature
    String str = "jsapi_ticket="+jsTicket.getTicket()+"&noncestr="+noncestr+"&timestamp="+timestampstr+"&url="+url;

    MessageDigest md = null;
    String tmpStr = "";
    try {
        md = MessageDigest.getInstance("SHA-1");
        // 将三个参数字符串拼接成一个字符串进行sha1加密  
        byte[] digest = md.digest(str.getBytes());  
        tmpStr = byteToStr(digest);
    } catch (NoSuchAlgorithmException e) {
        e.printStackTrace();
    }
    jsTicket.setAppId(school.getAppId());
    jsTicket.setSignature(tmpStr);
    jsTicket.setNoncestr(noncestr);
    jsTicket.setTimestamp(timestampstr);
    jsTicket.setUrl(url);

    return jsTicket;
}

网页分享/界面自定义

这几个都是前端的东西,直接按照文档写就可以了 微信官方文档#JS-SDK

 //引入weixin.js
wx.config({
debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出      来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅  在pc端时才会打印。
 appId: '', // 必填,公众号的唯一标识
 timestamp: , // 必填,生成签名的时间戳
 nonceStr: '', // 必填,生成签名的随机串
 signature: '',// 必填,签名
 jsApiList: [] // 必填,需要使用的JS接口列表
});

安全

总之,在开发过程中一定要注意token的保持和保护,所有的校验和生成都放在后端来做,尽量保证安全。

转载请注明出处: http://sdxlp.cn/article/jiru.html


如果对你有用的话,请赏给作者一个馒头吧 ...或帮点下页面底部的广告,感谢!!

赞赏支持
提交评论
评论信息(请文明评论)
暂无评论,快来快来写想法...
推荐
随着科技的快速发展,人工智能(AI)已经在各个领域大放异彩。其中,AI换脸技术以其独特的魅力引起了广泛的关注。这项技术主要运用深度学习算法,通过分析人脸的几何结构,将生成的模型映射到另一张人脸图像上,实现人脸的巧妙转换。
直播带货已经成为当今的主流,许多小伙伴都喜欢在快手小黄车中购买主播卖的商品,我们在交易过程中需要与客服进行联系,那么我们应该怎么查看与客服的聊天信息呢?今天,小编就教小伙伴们在快手小黄车中查看客服信息。
生活与工作中,我们在使用微信时,肯定需要发送比较大(不能发送超过100mb的视频)的视频文件,很早的时候都是用QQ与微信的互传,这样就是有点麻烦,点点这期跟小伙伴们分享一个比较简单的方法。
日常生活中会用到通讯工具,常用的就是QQ,微信,钉钉。有时候想要保存一些微信聊天记录,要如何才能永久储存呢?办法有很多,这个方法是最省钱省事保存最久的,可以将它们导入到word中,下面是具体办法,小编带小伙伴走一遭!
在日常生活中微信作为现在使用最多的即时通讯软件之一,微信电脑版,截图应该是小伙伴们使用比较多需要经常用到的功能了,但对于那些不常使用电脑版微信的朋友,怎么使用微信截图还是不太清楚?
现在最流行的就是在手机上就可以打卡了,早就不用什么打卡机了,还得输入指纹多麻烦,钉钉可以说是现如今大小企业用的最多一款智能移动办公平台了,但是还有不少的人也许还不清楚了解钉钉自动上下班打卡有多方便。
有时候抓图有点不好抓啊!现在教各位一个简单的东东。嘿
智能化的时代,上班打卡必备神器,再也不怕扣钱了