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