目录
前期准备
https://res.wx.qq.com/op_res/-serEQ6xSDVIjfoOHcX78T1JAYX-pM_fghzfiNYoD8uHVd3fOeC0PC_pvlg4-kmP微信公众平台提供的 c++, php, java, python, c# 5 种语言的消息加解密示例代码下载:https://res.wx.qq.com/op_res/-serEQ6xSDVIjfoOHcX78T1JAYX-pM_fghzfiNYoD8uHVd3fOeC0PC_pvlg4-kmP
回复文本消息
<xml> <ToUserName><![CDATA[toUser]]></ToUserName> <FromUserName><![CDATA[fromUser]]></FromUserName> <CreateTime>12345678</CreateTime> <MsgType><![CDATA[text]]></MsgType> <Content><![CDATA[你好]]></Content> </xml>
回复图片消息
<xml> <ToUserName><![CDATA[toUser]]></ToUserName> <FromUserName><![CDATA[fromUser]]></FromUserName> <CreateTime>12345678</CreateTime> <MsgType><![CDATA[image]]></MsgType> <Image> <MediaId><![CDATA[media_id]]></MediaId> </Image> </xml>
服务器配置时需要校验token,所有先写好get接口配置到服务器
@GetMapping(value = "/api/test")
public void checkToken(HttpServletRequest request, HttpServletResponse response) {
//一、校验URL
// 准备校验参数
// 微信加密签名
String msgSignature = request.getParameter("signature");
// 时间戳
String timeStamp = request.getParameter("timestamp");
// 随机数
String nonce = request.getParameter("nonce");
// 随机字符串
String echoStr = request.getParameter("echostr");
PrintWriter out=null;
try {
// 校验url
// 创建加解密类
WXBizMsgCrypt wxcpt = new WXBizMsgCrypt(Env.TOKEN,Env.ENCODING_AES_KEY,Env.APP_ID);
// 进行url校验
//不抛异常就说明校验成功
String sEchoStr= wxcpt.verifyUrl_WXGZ(msgSignature, Env.TOKEN, timeStamp, nonce, echoStr);
// 若校验成功,则原样返回 echoStr
out = response.getWriter();
out.print(sEchoStr);
} catch (AesException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (out != null) {
out.close();
out = null;
}
}
}
/**
* @desc :微信公众号 验证url
*
* @param msgSignature 签名串,对应URL参数的msg_signature
* @param token 公众平台上,开发者设置的token
* @param timeStamp 时间戳,对应URL参数的timestamp
* @param nonce 随机数,对应URL参数的nonce
* @param echoStr 随机串,对应URL参数的echostr,在微信公众号中是明文的,直接原样返回给微信公众平台官方服务器
* @return String 验证成功后,原样返回echoStr
* @throws AesException 执行失败,请查看该异常的错误码和具体的错误信息
*/
public String verifyUrl_WXGZ(String msgSignature, String token , String timeStamp, String nonce,String echoStr) throws AesException {
// 进行SHA1加密
String signature = SHA1.getSHA1_WXGZ(token, timeStamp, nonce);
// 验证 token、timestamp、nonce进行SHA1加密生成的signature 是否与url传过来msgSignature相同
if (!signature.equals(msgSignature)) {
throw new AesException(AesException.ValidateSignatureError);
}
// 若不抛异常,则url验证成功,原样返回echoStr
String result = echoStr;
return result;
}
/**
* 微信公众号SHA1加密算法
* 用SHA1算法生成安全签名
* @param token 票据
* @param timestamp 时间戳
* @param nonce 随机字符串
* @return 安全签名
* @throws AesException
*/
public static String getSHA1_WXGZ(String token, String timestamp, String nonce) throws AesException
{
try {
String[] array = new String[] { token, timestamp, nonce };
StringBuffer sb = new StringBuffer();
// 将token、timestamp、nonce三个参数进行字典序排序
Arrays.sort(array);
// 将三个参数字符串拼接成一个字符串进行sha1加密
for (int i = 0; i < 3; i++) {
sb.append(array[i]);
}
String str = sb.toString();
// SHA1签名生成
MessageDigest md = MessageDigest.getInstance("SHA-1");
md.update(str.getBytes());
byte[] digest = md.digest();
StringBuffer hexstr = new StringBuffer();
String shaHex = "";
for (int i = 0; i < digest.length; i++) {
shaHex = Integer.toHexString(digest[i] & 0xFF);
if (shaHex.length() < 2) {
hexstr.append(0);
}
hexstr.append(shaHex);
}
return hexstr.toString();
} catch (Exception e) {
e.printStackTrace();
throw new AesException(AesException.ComputeSignatureError);
}
}
下一步,进行服务器配置,公众号管理页面——设置与开发——基本配置——服务器配置
代码实现
接口回调post请求,接收用户消息,并回复。
get,post两个的请求路径一致,就是服务器配置填写的URL。
@PostMapping("/api/test")
public void reply(HttpServletRequest request, HttpServletResponse response) throws IOException {
// 将请求、响应的编码均设置为UTF-8(防止中文乱码)
request.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
// 调用消息业务类接收消息、处理消息
String respMessage = replyMessageService.reply(request);
// 响应消息
PrintWriter out = response.getWriter();
out.print(respMessage);
out.close();
}
业务类实现
ReplyMessageService
/**
* @desc : 回复消息
*
* @param request
* @return String 回复消息的加密xml字符串
*/
public String reply( HttpServletRequest request ) {
// 解密:从request中获取消息明文
String xmlMsg = decryptMsg(request);
// 获取回复消息(明文)
String replyMsg = getReplyMsg( xmlMsg);
logger.info("replyMsg:" + replyMsg);
// 根据消息加密方式判断是否加密
String timeStamp = request.getParameter("timestamp"); // 时间戳
String nonce = request.getParameter("nonce"); // 随机数
String encryptType = request.getParameter("encrypt_type");
// 安全模式-加密:将回复消息加密
if(null != encryptType) {
WXBizMsgCrypt wxcpt;
try {
wxcpt = new WXBizMsgCrypt(Env.TOKEN,Env.ENCODING_AES_KEY,Env.APP_ID);
replyMsg=wxcpt.encryptMsg(replyMsg, timeStamp, nonce);
} catch (AesException e) {
e.printStackTrace();
}
}
return replyMsg;
}
/**
* @desc : 从request中获取消息明文
* 从request中获取加密消息,将其解密并返回
* @param request
* @return String 消息明文
*/
public static String decryptMsg(HttpServletRequest request) {
String postData=""; // 密文,对应POST请求的数据
String result=""; // 明文,解密之后的结果
String msgSignature = request.getParameter("msg_signature"); // 微信加密签名
String timeStamp = request.getParameter("timestamp"); // 时间戳
String nonce = request.getParameter("nonce"); // 随机数
String encryptType = request.getParameter("encrypt_type");
try {
// 获取加密的请求消息:使用输入流获得加密请求消息postData
ServletInputStream in = request.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
String tempStr = ""; //作为输出字符串的临时串,用于判断是否读取完毕
while(null != (tempStr = reader.readLine())){
postData += tempStr;
}
// 获取消息明文:对加密的请求消息进行解密获得明文
if(null!=encryptType) {
logger.info("安全模式:消息被加密");
WXBizMsgCrypt wxcpt = new WXBizMsgCrypt(Env.TOKEN,Env.ENCODING_AES_KEY,Env.APP_ID);
result = wxcpt.decryptMsg(msgSignature, timeStamp, nonce, postData);
}else {
logger.info("明文模式");
result = postData;
}
} catch (IOException e) {
e.printStackTrace();
} catch (AesException e) {
e.printStackTrace();
}
return result;
}
/**
* @desc :获取回复消息
*
* @param xmlMsg
* @return String 返回加密后的回复消息
*/
public String getReplyMsg(String xmlMsg){
String replyMsg = "success";
try {
// 解析微信发来的请求,解析xml字符串
Map<String, String> requestMap= WxMsgUtil.parseXml(xmlMsg);
// 获取请求参数
String fromUserName = requestMap.get("FromUserName");
String toUserName = requestMap.get("ToUserName");
// 消息类型与事件
String msgType = requestMap.get("MsgType");
String eventType = requestMap.get("Event");
String eventKey = requestMap.get("EventKey");
// 微信服务器在五秒内收不到响应会断掉连接,并重新发起请求,共重试三次,三次请求MsgId不变
String msgId = requestMap.get("MsgId");
String content = null;
String picUrl = null;
if(msgType.equals(WxMsgUtil.REQ_MESSAGE_TYPE_TEXT)){
content = requestMap.get("Content");
} else if (msgType.equals(WxMsgUtil.REQ_MESSAGE_TYPE_IMAGE)) {
picUrl = requestMap.get("PicUrl");
}
if(null != picUrl) {
// 用户发送的图片,消息处理
} else {
// 回复文本消息
ReplyTextMsg message = new ReplyTextMsg();
message.setToUserName(fromUserName);
message.setFromUserName(toUserName);
message.setCreateTime(new Date().getTime());
message.setMsgType(WxMsgUtil.RESP_MESSAGE_TYPE_TEXT);
// 获取回复消息的内容 :消息的分类处理
String replyContent = getReplyContentByMsgType(msgType, eventType, eventKey, msgUser, content, picUrl, null);
message.setContent(replyContent);
// 获取xml字符串: 将(被动回复消息型的)文本消息对象转成xml字符串
replyMsg = WxMsgUtil.textMessageToXml(message);
}
} catch (Exception e) {
e.printStackTrace();
}
return replyMsg;
}
/**
* @desc : 处理消息:根据消息类型获取回复内容
*
* @param msgType 消息类型
* @return String 回复内容
*/
public String getReplyContentByMsgType(String msgType,String eventType,String eventKey, WxMsgUser msgUser, String content, String picUrl, String msgId) throws Exception {
String replyContent = null;
//1.文本消息
if (msgType.equals(WxMsgUtil.REQ_MESSAGE_TYPE_TEXT)) {
// TODO 回复内容处理
replyContent = "文本消息";
}
//2.图片消息
else if (msgType.equals(WxMsgUtil.REQ_MESSAGE_TYPE_IMAGE)) {
replyContent = "图片消息";
}
//3.地理位置消息
else if (msgType.equals(WxMsgUtil.REQ_MESSAGE_TYPE_LOCATION)) {
replyContent = "地理位置消息";
}
//4.链接消息
else if (msgType.equals(WxMsgUtil.REQ_MESSAGE_TYPE_LINK)) {
replyContent = "链接消息";
}
//5.音频消息
else if (msgType.equals(WxMsgUtil.REQ_MESSAGE_TYPE_VOICE)) {
replyContent = "音频消息";
}
//6.事件推送
else if (msgType.equals(WxMsgUtil.REQ_MESSAGE_TYPE_EVENT)) {
replyContent = getReplyContentByEventType(eventType, eventKey);
}
//7.请求异常
else {
replyContent = "请求异常";
}
return replyContent;
}
/**
* @desc : 事件推送消息处理
*
* @param eventType 事件类型
* @param eventKey 事件key值
* @return String
*/
public String getReplyContentByEventType(String eventType,String eventKey){
String respContent = null;
// 订阅
if (eventType.equals(WxMsgUtil.EVENT_TYPE_SUBSCRIBE)) {
respContent = "欢迎订阅";
}
// 取消订阅
else if (eventType.equals(WxMsgUtil.EVENT_TYPE_UNSUBSCRIBE)) {
// 取消订阅后用户再收不到公众号发送的消息,因此不需要回复消息
}
//上报地理位置事件
else if(eventType.equals("LOCATION")){
}
// 自定义菜单点击事件
else if (eventType.equals(WxMsgUtil.EVENT_TYPE_CLICK)) {
}
return respContent;
}
返回图片消息,需要先将图片上传到临时素材,获取media_id
工具类
public class WxMsgUtil {
//返回消息类型:文本
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_MUSIC = "music";
//返回消息类型:图文
public static final String RESP_MESSAGE_TYPE_NEWS = "news";
//返回消息类型:客服
public static final String RESP_MESSAGE_TYPE_CUSTOM = "transfer_customer_service";
//请求消息类型:文本
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";
/**
* @desc : 解析微信发来的请求(XML),获取请求参数
*
* @param request
* @return
* @throws Exception Map<String,String>
*/
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;
}
/**
* @desc : 解析微信发来的请求(xmlStr),获取请求参数
*
* @param xmlStr
* @return
* @throws Exception Map<String,String>
*/
public static Map<String, String> parseXml(String xmlStr) throws Exception {
// 将解析结果存储在HashMap中
Map<String, String> map = new HashMap<String, String>();
// 将字符串转为Document
Document document = DocumentHelper.parseText(xmlStr);
// 获取根元素的所有子节点
// 得到xml根元素
Element root = document.getRootElement();
// 得到根元素的所有子节点
List<Element> elementList = root.elements();
// 遍历所有子节点
for (Element e : elementList) {
map.put(e.getName(), e.getText());
}
return map;
}
/**
* 文本消息对象转换成xml
*
* @param textMessage 文本消息对象
* @return xml
*/
public static String textMessageToXml(ReplyTextMsg textMessage) {
xstream.alias("xml", textMessage.getClass());
return xstream.toXML(textMessage);
}
/**
* 图片消息对象转换成xml
*
* @param textMessage 文本消息对象
* @return xml
*/
public static String imageMessageToXml(ReplyImageMsg imageMessage) {
String msg = "<xml>\n" +
" <ToUserName><![CDATA["+imageMessage.getToUserName()+"]]></ToUserName>\n" +
" <FromUserName><![CDATA["+imageMessage.getFromUserName()+"]]></FromUserName>\n" +
" <CreateTime>"+new Date().getTime()+"</CreateTime>\n" +
" <MsgType><![CDATA[image]]></MsgType>\n" +
" <Image>\n" +
" <MediaId><![CDATA["+imageMessage.getImage().getMediaId()+"]]></MediaId>\n" +
" </Image>\n" +
"</xml>";
return msg;
}
/**
* 转发客服消息对象转换成xml
*
* @param customMessage 文本消息对象
* @return xml
*/
public static String customMessageToXml(CustomMessage customMessage) {
xstream.alias("xml", customMessage.getClass());
return xstream.toXML(customMessage);
}
/**
* 音乐消息对象转换成xml
*
* @param musicMessage 音乐消息对象
* @return xml
*/
public static String musicMessageToXml(MusicMessage musicMessage) {
xstream.alias("xml", musicMessage.getClass());
return xstream.toXML(musicMessage);
}
/**
* 图文消息对象转换成xml
*
* @param newsMessage 图文消息对象
* @return xml
*/
public static String newsMessageToXml(NewsMessage newsMessage) {
xstream.alias("xml", newsMessage.getClass());
xstream.alias("item", new Article().getClass());
return xstream.toXML(newsMessage);
}
/**
* 扩展xstream,使其支持CDATA块
*
* @date 2013-05-19
*/
private static final XStream xstream = new XStream(new XppDriver() {
public HierarchicalStreamWriter createWriter(Writer out) {
return new PrettyPrintWriter(out) {
// 对所有xml节点的转换都增加CDATA标记
final boolean cdata = true;
@SuppressWarnings("unchecked")
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);
}
}
};
}
});
}