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

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

月光魔力鸭

2019-11-08 14:35 阅读 655 喜欢 0

实际上,之前有对接过的... 但是忘记了,而且也没做详细记录,同时语言又换了.. 成java了,所有这里记录下,包括从微信的授权、token、以及到js-sdk 开发为止。

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

相关地址

流程介绍

接口配置信息

接口配置信息

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数据,具体格式参考:undefined

这里只是做了普通文本消息的整理,其他的暂未用到,可以参考下。 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函数。

获取用户信息

在用户关注我们的公众号的时候,就可以通过接口获取到用户的基本信息啦。当然,后续的网页请求也会让用户去授权,同时也可以拿到用户信息。 以下为根据用户的openIdtoken 来获得用户的基本信息。 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;
	}

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

数据走向

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

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

网页对接

关于网页授权(与服务器授权基本类似)可以参考undefined

按照这几个步骤一步一步来即可

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

    //通过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;
	}

网页分享/界面自定义

这几个都是前端的东西,直接按照文档写就可以了,实在没什么说的... undefined

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

安全

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


这文章拖拖拖一直拖了俩月才想起来写.. 比较潦草,等过一段时间,我准备做一个服务号,到时候还会详细记录一下相关的坑和过程的。

转载请注明出处: https://chrunlee.cn/article/mp-weixin-java.html


感谢支持!

赞赏支持
提交评论
评论信息 (请文明评论)
暂无评论,快来快来写想法...
推荐
记录下关于通过tomcat来配置https ,使java web项目可以直接通过https来访问,之前的时候都是走nginx来配置https,由于特殊原因443端口不能开启(具体是啥不清楚,运维不让开),只能如此配置下。
nginx 监听的服务器的9000 端口,转的443 到tomcat 中,但是在java web jsp中获取的端口却还是443.
项目为nginx+tomcat 部署的,由于需要https环境,本来直接配置nginx https就可以了,结果在jsp 中获取request.getScheme 怎么都是http .. 可把我这个假运维愁坏了。
这只能算是一个小实现吧 ,也不算什么难点,就是加强下记忆,后续查找方便点而已,当然也有可能给你提供个小灵感啥的,那就再好不过了。
之前没接触过tigase,最近开始准备用这个来做IM ,开始预研..不过中间比较坎坷,虽然有忙别的事情,但是前前后后还是花了好几天的时间,资源太少,官网又看不懂,git还下不下来... 啥机制也不懂.. 真惆怅。
最近项目使用了spring-boot框架,在部署过程中出现了很多问题,这里记录下,防止后续遗忘。
关于即时通讯,现在各路APP基本都有,虽然不能说是核心,但是如果没有又总会觉的少点啥。 如果对即时通讯要求不高,且用户量不大,只是想要从无到有的话,可以直接使用现有的服务即可,有不少免费的足够支持小量用户的要求。但是如果用户量稍微较大,且需要一些定制服务又不想受制于人,那么搭建自己的即时通讯服务器可能是你最终的选择。但是,从头开发肯定是不可能的。
项目为长期稳定运行中,由于新增功能或修复BUG,修改代码后无法启动。根据添加的代码删减,最后确认出的问题。