实际上,之前有对接过的... 但是忘记了,而且也没做详细记录,同时语言又换了.. 成java了,所有这里记录下,包括从微信的授权、token、以及到js-sdk 开发为止。
开发过程中使用的测试号进行开发,需要先配置一些地址信息等。
这里开发需要使用外网,我用的是frp
,可以查看这个https://chrunlee.cn/article/frp-centos-web-ssh.html
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
就可以啦。
这个域名是我们用来开发web的时候微信校验使用的,暂时先填写一个就可以,需要注意的地方是:该域名只写域名不带协议,也就是http
https
等都不用写入,例如:demo.chrunlee.cn
即可。
接下来,我们就要开始获取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+"×tamp="+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的保持和保护,所有的校验和生成都放在后端来做,尽量保证安全。
这文章拖拖拖一直拖了俩月才想起来写.. 比较潦草,等过一段时间,我准备做一个服务号,到时候还会详细记录一下相关的坑和过程的。
转载请注明出处: https://chrunlee.cn/article/mp-weixin-java.html