文章目录
前言
微信公众平台是运营者通过公众号为微信用户提供资讯和服务的平台,而公众平台开发接口则是提供服务的基础,开发者在公众平台网站中创建公众号、获取接口权限后,可以通过阅读本文来帮助开发。
为了识别用户,每个用户针对每个公众号会产生一个安全的OpenID,如果需要在多公众号、移动应用之间做用户共通,则需前往微信开放平台,将这些公众号和应用绑定到一个开放平台账号下,绑定后,一个用户虽然对多个公众号和应用有多个不同的OpenID,但他对所有这些同一开放平台账号下的公众号和应用,只有一个UnionID
名词解释:
OpenID: 是一个以用户为中心的数字身份识别框架,它具有开放、分散性。OpenID 的创建基于这样一个概念:我们可以通过 URI (又叫 URL 或网站地址)来认证一个网站的唯一身份,同理,我们也可以通过这种方式来作为用户的身份认证。
UnionID:如果开发者拥有多个移动应用、网站应用、和公众帐号(包括小程序),可通过 UnionID 来区分用户的唯一性,因为只要是同一个微信开放平台帐号下的移动应用、网站应用和公众帐号(包括小程序),用户的 UnionID 是唯一的。换句话说,同一用户,对同一个微信开放平台下的不同应用,UnionID是相同的。
一、 微信公众号基本配置
1、注册并登陆微信公众号
https://mp.weixin.qq.com/
2、服务器配置
设置于开发——>基本配置——>服务器配置
填写配置URL 和 Token:自主设置用于验证开发者服务器。
3、 创建微信连接检验工具包:CheckUtil
提交信息后,微信服务器将发送GET请求到填写的服务器地址URL上,GET请求携带参数如下表所示:
signature 微信加密签名,signature结合了开发者填写的token参数和请求中的timestamp参数、nonce参数。
timestamp 时间戳
nonce 随机数
echostr 随机字符串
开发者通过检验signature对请求进行校验(下面有校验方式)。若确认此次GET请求来自微信服务器,请原样返回echostr参数内容,则接入生效,成为开发者成功,否则接入失败。加密/校验流程如下:
1、将token、timestamp、nonce三个参数进行字典序排序
2、将三个参数字符串拼接成一个字符串进行sha1加密
3、开发者获得加密后的字符串可与signature对比,标识该请求来源于微信
package com.vx.Util;
import java.security.MessageDigest;
import java.util.Arrays;
public class CheckUtil {
public static final String tooken = "WeChat"; //开发者自行定义Tooken
public static boolean checkSignature(String signature,String timestamp,String nonce){
//1.定义数组存放tooken,timestamp,nonce
String[] arr = {tooken,timestamp,nonce};
//2.对数组进行排序
Arrays.sort(arr);
//3.生成字符串
StringBuffer sb = new StringBuffer();
for(String s : arr){
sb.append(s);
}
//4.sha1加密,网上均有现成代码
String temp = getSha1(sb.toString());
//5.将加密后的字符串,与微信传来的加密签名比较,返回结果
return temp.equals(signature);
}
public static String getSha1(String str){
if(str==null||str.length()==0){
return null;
}
char hexDigits[] = {'0','1','2','3','4','5','6','7','8','9',
'a','b','c','d','e','f'};
try {
MessageDigest mdTemp = MessageDigest.getInstance("SHA1");
mdTemp.update(str.getBytes("UTF-8"));
byte[] md = mdTemp.digest();
int j = md.length;
char buf[] = new char[j*2];
int k = 0;
for (int i = 0; i < j; i++) {
byte byte0 = md[i];
buf[k++] = hexDigits[byte0 >>> 4 & 0xf];
buf[k++] = hexDigits[byte0 & 0xf];
}
return new String(buf);
} catch (Exception e) {
// TODO: handle exception
return null;
}
}
}
4、创建controller层用来接收和返回参数
package com.vx.controller;
import com.vx.Util.CheckUtil;
import org.apache.log4j.Logger;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
@Controller
@RequestMapping("/wechat")
public class WeChatController {
private static Logger logger = Logger.getLogger(WeChatController. class );
/**
* 用于接受get请求参数,返回验证参数
*/
@RequestMapping (value = "security" , method = RequestMethod.GET)
public void doGet(HttpServletRequest request, HttpServletResponse response,
@RequestParam(value = "signature", required=true)String signature,
@RequestParam (value = "timestamp" , required = true ) String timestamp,
@RequestParam (value = "nonce" , required = true ) String nonce,
@RequestParam (value = "echostr" , required = true ) String echostr) {
if(CheckUtil.checkSignature(signature, timestamp, nonce)){
try {
PrintWriter out = response.getWriter();
out.print(echostr);
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
二、消息的接收与处理
1、接收普通消息
当普通微信用户向公众账号发消息时,微信服务器将POST消息的XML数据包到开发者填写的URL上。
文本消息
<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>
参数说明:
ToUserName 开发者微信号
FromUserName 发送方帐号(一个OpenID)
CreateTime 消息创建时间 (整型)
MsgType 消息类型,文本为text
Content 文本消息内容
MsgId 消息id,64位整型
图片消息
<xml>
<ToUserName><![CDATA[toUser]]></ToUserName>
<FromUserName><![CDATA[fromUser]]></FromUserName>
<CreateTime>1348831860</CreateTime>
<MsgType><![CDATA[image]]></MsgType>
<PicUrl><![CDATA[this is a url]]></PicUrl>
<MediaId><![CDATA[media_id]]></MediaId>
<MsgId>1234567890123456</MsgId>
</xml>
ToUserName 开发者微信号
FromUserName 发送方帐号(一个OpenID)
CreateTime 消息创建时间 (整型)
MsgType 消息类型,图片为image
PicUrl 图片链接(由系统生成)
MediaId 图片消息媒体id,可以调用获取临时素材接口拉取数据。
MsgId 消息id,64位整型
语音消息
<xml>
<ToUserName><![CDATA[toUser]]></ToUserName>
<FromUserName><![CDATA[fromUser]]></FromUserName>
<CreateTime>1357290913</CreateTime>
<MsgType><![CDATA[voice]]></MsgType>
<MediaId><![CDATA[media_id]]></MediaId>
<Format><![CDATA[Format]]></Format>
<Recognition>< ![CDATA[腾讯微信团队] ]></Recognition>
<MsgId>1234567890123456</MsgId>
</xml>
ToUserName 开发者微信号
FromUserName 发送方帐号(一个OpenID)
CreateTime 消息创建时间 (整型)
MsgType 语音为voice
MediaId 语音消息媒体id,可以调用获取临时素材接口拉取数据。
Format 语音格式,如amr,speex等
Recognition 语音识别结果,UTF8编码
MsgId 消息id,64位整型
视频消息
<xml>
<ToUserName><![CDATA[toUser]]></ToUserName>
<FromUserName><![CDATA[fromUser]]></FromUserName>
<CreateTime>1357290913</CreateTime>
<MsgType><![CDATA[video]]></MsgType>
<MediaId><![CDATA[media_id]]></MediaId>
<ThumbMediaId><![CDATA[thumb_media_id]]></ThumbMediaId>
<MsgId>1234567890123456</MsgId>
</xml>
ToUserName 开发者微信号
FromUserName 发送方帐号(一个OpenID)
CreateTime 消息创建时间 (整型)
MsgType 视频为video
MediaId 视频消息媒体id,可以调用获取临时素材接口拉取数据。
ThumbMediaId 视频消息缩略图的媒体id,可以调用多媒体文件下载接口拉取数据。
MsgId 消息id,64位整型
小视频消息
<xml>
<ToUserName><![CDATA[toUser]]></ToUserName>
<FromUserName><![CDATA[fromUser]]></FromUserName>
<CreateTime>1357290913</CreateTime>
<MsgType><![CDATA[shortvideo]]></MsgType>
<MediaId><![CDATA[media_id]]></MediaId>
<ThumbMediaId><![CDATA[thumb_media_id]]></ThumbMediaId>
<MsgId>1234567890123456</MsgId>
</xml>
ToUserName 开发者微信号
FromUserName 发送方帐号(一个OpenID)
CreateTime 消息创建时间 (整型)
MsgType 小视频为shortvideo
MediaId 视频消息媒体id,可以调用获取临时素材接口拉取数据。
ThumbMediaId 视频消息缩略图的媒体id,可以调用获取临时素材接口拉取数据。
MsgId 消息id,64位整型
地理位置消息
<xml>
<ToUserName><![CDATA[toUser]]></ToUserName>
<FromUserName><![CDATA[fromUser]]></FromUserName>
<CreateTime>1351776360</CreateTime>
<MsgType><![CDATA[location]]></MsgType>
<Location_X>23.134521</Location_X>
<Location_Y>113.358803</Location_Y>
<Scale>20</Scale>
<Label><![CDATA[位置信息]]></Label>
<MsgId>1234567890123456</MsgId>
</xml>
ToUserName 开发者微信号
FromUserName 发送方帐号(一个OpenID)
CreateTime 消息创建时间 (整型)
MsgType 消息类型,地理位置为location
Location_X 地理位置纬度
Location_Y 地理位置经度
Scale 地图缩放大小
Label 地理位置信息
MsgId 消息id,64位整型
链接信息
<xml>
<ToUserName><![CDATA[toUser]]></ToUserName>
<FromUserName><![CDATA[fromUser]]></FromUserName>
<CreateTime>1351776360</CreateTime>
<MsgType><![CDATA[link]]></MsgType>
<Title><![CDATA[公众平台官网链接]]></Title>
<Description><![CDATA[公众平台官网链接]]></Description>
<Url><![CDATA[url]]></Url>
<MsgId>1234567890123456</MsgId>
</xml>
ToUserName 接收方微信号
FromUserName 发送方微信号,若为普通用户,则是一个 OpenID
CreateTime 消息创建时间
MsgType 消息类型,链接为link
Title 消息标题
Description 消息描述
Url 消息链接
MsgId 消息id,64位整型
接受事件推送
关注与取消关注
<xml>
<ToUserName><![CDATA[toUser]]></ToUserName>
<FromUserName><![CDATA[FromUser]]></FromUserName>
<CreateTime>123456789</CreateTime>
<MsgType><![CDATA[event]]></MsgType>
<Event><![CDATA[subscribe]]></Event>
</xml>
ToUserName 开发者微信号
FromUserName 发送方帐号(一个OpenID)
CreateTime 消息创建时间 (整型)
MsgType 消息类型,event
Event 事件类型,subscribe(订阅)、unsubscribe(取消订阅)
自定义菜单栏事件
用户点击自定义菜单后,微信会把点击事件推送给开发者,请注意,点击菜单弹出子菜单,不会产生上报。
点击菜单拉取消息时的事件推送
在这里插入代码片<xml>
<ToUserName><![CDATA[toUser]]></ToUserName>
<FromUserName><![CDATA[FromUser]]></FromUserName>
<CreateTime>123456789</CreateTime>
<MsgType><![CDATA[event]]></MsgType>
<Event><![CDATA[CLICK]]></Event>
<EventKey><![CDATA[EVENTKEY]]></EventKey>
</xml>
ToUserName 开发者微信号
FromUserName 发送方帐号(一个OpenID)
CreateTime 消息创建时间 (整型)
MsgType 消息类型,event
Event 事件类型,CLICK
EventKey 事件KEY值,与自定义菜单接口中KEY值对应
点击菜单跳转链接时的事件推送
<xml>
<ToUserName><![CDATA[toUser]]></ToUserName>
<FromUserName><![CDATA[FromUser]]></FromUserName>
<CreateTime>123456789</CreateTime>
<MsgType><![CDATA[event]]></MsgType>
<Event><![CDATA[VIEW]]></Event>
<EventKey><![CDATA[www.qq.com]]></EventKey>
</xml>
ToUserName 开发者微信号
FromUserName 发送方帐号(一个OpenID)
CreateTime 消息创建时间 (整型)
MsgType 消息类型,event
Event 事件类型,VIEW
EventKey 事件KEY值,设置的跳转URL
2、消息接受处理
XML格式处理
微信的消息体是采用xml格式,那么我在这里写了一个MessageUtil去做消息格式的处理,大致代码如下:
package com.vx.Util;
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;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import javax.servlet.http.HttpServletRequest;
import java.io.InputStream;
import java.io.Writer;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class MessageUtil {
/**
* 返回消息类型:文本
*/
public static final String RESP_MESSAGE_TYPE_TEXT = "text" ;
/**
* 返回消息类型:音乐
*/
public static final String RESP_MESSAGE_TYPE_MUSIC = "music" ;
/**
* 返回消息类型:图文
*/
public static final String RESP_MESSAGE_TYPE_NEWS = "news" ;
/**
* 请求消息类型:文本
*/
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_LINK = "link" ;
/**
* 请求消息类型:地理位置
*/
public static final String REQ_MESSAGE_TYPE_LOCATION = "location" ;
/**
* 请求消息类型:音频
*/
public static final String REQ_MESSAGE_TYPE_VOICE = "voice" ;
/**
* 请求消息类型:推送
*/
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" ;
/**
* 事件类型:CLICK(自定义菜单点击事件)
*/
public static final String EVENT_TYPE_CLICK = "CLICK" ;
/**
* 解析微信发来的请求(XML)
*/
@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;
}
@SuppressWarnings ( "unused" )
private static XStream xstream = new XStream(new XppDriver() {
public HierarchicalStreamWriter createWriter(Writer out) {
return new PrettyPrintWriter(out) {
// 对所有xml节点的转换都增加CDATA标记
boolean cdata = true ;
@SuppressWarnings ( "rawtypes" )
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);
}
}
};
}
});
}
消息接收
WechatController下添加post方法用于做消息的接收和处理
@RequestMapping (value = "security" , method = RequestMethod.POST)
// post方法用于接收微信服务端消息
public void DoPost(HttpServletRequest request,HttpServletResponse response) {
try {
Map<String, String> map = MessageUtil.parseXml(request);
} catch (Exception e) {
logger.error(e,e);
}
}
消息处理
建立消息实体
建立消息实体以方便我们后面的使用
消息实体基础类
@Data
public class BaseMessage {
// 开发者微信号
private String ToUserName;
// 发送方帐号(一个OpenID)
private String FromUserName;
// 消息创建时间 (整型)
private long CreateTime;
// 消息类型(text/image/location/link/video/shortvideo)
private String MsgType;
// 消息id,64位整型
private long MsgId;
}
文本消息
public class TextMessage extends BaseMessage {
//文本内容
private String Content;
}
链接消息
@Data
public class LinkMessage extends BaseMessage{
// 消息标题
private String Title;
// 消息描述
private String Description;
// 消息链接
private String Url;
}
视频消息
@Data
public class VideoMessage {
private String MediaId; // 视频消息媒体id,可以调用多媒体文件下载接口拉取数据
private String ThumbMediaId; // 视频消息缩略图的媒体id,可以调用多媒体文件下载接口拉取数据
}
其他消息
其他消息类似根据前面不同消息不同内容进行添加属性处理
消息分类处理
按照上面收到想消息类别分别做不同的分发处理,这里我们建立了自己的业务分发器(EventDispatcher、MsgDispatcher),分别做普通消息处理和事件消息处理
EventDispatcher
public class EventDispatcher {
public static String processEvent(Map<String, String> map) {
if (map.get( "Event" ).equals(MessageUtil.EVENT_TYPE_SUBSCRIBE)) { //关注事件
System.out.println( "==============这是关注事件!" );
}
if (map.get( "Event" ).equals(MessageUtil.EVENT_TYPE_UNSUBSCRIBE)) { //取消关注事件
System.out.println( "==============这是取消关注事件!" );
}
if (map.get( "Event" ).equals(MessageUtil.EVENT_TYPE_CLICK)) { //自定义菜单点击事件
System.out.println( "==============这是自定义菜单点击事件!" );
}
return "事件消息" ;
}
}
MsgDispatcher
public class MsgDispatcher {
public static String processMessage(Map<String,String> map){
if (map.get("MsgType").equals(MessageUtil.REQ_MESSAGE_TYPE_TEXT)){
System.out.println("==============这是文本消息");
}
if (map.get( "MsgType" ).equals(MessageUtil.REQ_MESSAGE_TYPE_IMAGE)) { // 图片消息
System.out.println( "==============这是图片消息!" );
}
if (map.get( "MsgType" ).equals(MessageUtil.REQ_MESSAGE_TYPE_LINK)) { // 链接消息
System.out.println( "==============这是链接消息!" );
}
if (map.get( "MsgType" ).equals(MessageUtil.REQ_MESSAGE_TYPE_LOCATION)) { // 位置消息
System.out.println( "==============这是位置消息!" );
}
if (map.get( "MsgType" ).equals(MessageUtil.REQ_MESSAGE_TYPE_VIDEO)) { // 视频消息
System.out.println( "==============这是视频消息!" );
}
if (map.get( "MsgType" ).equals(MessageUtil.REQ_MESSAGE_TYPE_VOICE)) { // 语音消息
System.out.println( "==============这是语音消息!" );
}
return "普通事件";
}
}
这个时候我们需要把我们的消息入口【WechatController.java】中的post方法做些修改
public void DoPost(HttpServletRequest request,HttpServletResponse response) {
try {
Map<String, String> map = MessageUtil.parseXml(request);
String msgType = map.get("MsgType");
if (MessageUtil.REQ_MESSAGE_TYPE_EVENT.equals(msgType)){
EventDispatcher.processEvent(map); //进入事件处理
} else {
MsgDispatcher.processMessage(map); //进入消息处理
}
} catch (Exception e) {
logger.error(e,e);
}
}
消息回复
这里我们要讲述的是我们在给用户回复的消息类型,在这里也可以大致分为两类:文本消息和图文消息
回复消息基类
@Data
public class BaseMessage {
// 接收方帐号(收到的OpenID)
private String ToUserName;
// 开发者微信号
private String FromUserName;
// 消息创建时间 (整型)
private long CreateTime;
// 消息类型(text/music/news)
private String MsgType;
}
文本消息类
@Data
public class TextMessage extends BaseMessage{
// 回复的消息内容
private String Content;
}
图片消息类
@Data
public class ImageMessage extends BaseMessage {
// 通过素材管理中的接口上传多媒体文件,得到的id。
private String MediaId;
}
消息回复格式转换
MessageUtil 类中添加消息转换方法
/**
* @Description: 文本消息对象转换成xml
*/
public static String textMessageToXml(TextMessage textMessage) {
xstream.alias( "xml" , textMessage.getClass());
return xstream.toXML(textMessage);
}
/**
* @Description: 图文消息对象转换成xml
*/
public static String newsMessageToXml(ImageMessage imageMessage) {
xstream.alias( "xml" , imageMessage.getClass());
return xstream.toXML(imageMessage);
}
消息内容填充
dispatcher消息中根据自己需求进行内容填充
String openid=map.get( "FromUserName" ); //用户openid
String mpid=map.get( "ToUserName" ); //公众号原始ID
if(map.get("Content").equals("文本")){
TextMessage textMessage = new TextMessage();//文本回复类
textMessage.setFromUserName(openid);
textMessage.setToUserName(mpid);
textMessage.setCreateTime(new Date().getTime());
textMessage.setMsgType(MessageUtil.RESP_MESSAGE_TYPE_TEXT);
textMessage.setContent("这是一个文本消息回复");
return MessageUtil.textMessageToXml(textMessage);
}
if(map.get("Content").equals("图片")){
ImageMessage imageMessage = new ImageMessage();//图片回复类
imageMessage.setFromUserName(openid);
imageMessage.setToUserName(mpid);
imageMessage.setCreateTime(new Date().getTime());
imageMessage.setMsgType(MessageUtil.REQ_MESSAGE_TYPE_IMAGE);
//通过素材管理中的接口上传多媒体文件,得到的id。
imageMessage.setMediaId("http://assets.processon.com/chart_image/616f8c10e401fd16dd9090ea.png");
return MessageUtil.newsMessageToXml(imageMessage);
}