今天心血来潮就信手拈来学了下微信消息加解密的知识,忽然觉得微信真的好强大。可能在大部分项目微信消息的加解密都用不上,但是仍然不排除有使用到的情况,如涉及金钱方面的微信应用包括商城类、金融类还有其他安全级别要求很高的微信应用。针对这些情况我觉得还是有必要分享下我的学习心得。
一 功能概述
公众号消息加解密是公众平台为了进一步加强公众号安全保障,提供的新机制。开发者需注意,公众账号主动调用API的情况将不受影响。只有被动回复用户的消息时,才需要进行消息加解密。具体包括:
1.新增消息体签名验证,用于公众平台和公众账号验证消息体的正确性 2.针对推送给微信公众账号的普通消息和事件消息,以及推送给设备公众账号的设备消息进行加密 3.公众账号对密文消息的回复也要求加密
这段文字描述摘抄至微信公众平台开发者文档,从上面的描述可以看出消息加密旨在提高安全性。
二 公众平台如何设置消息加解密
点击左侧的开发者中心,找到相关的配置项即可,如下所示
三 编程实现
消息加解密支持的开发语言还是挺多的如java、C++、C#、php等,鉴于本人只懂java,因此代码部分将会全部使用java
1 有关消息加解密的关键代码
代码微信公众平台提供了下载链接,但是由于java开发环境的复杂性,直接使用微信提供的jar包会出现问题,因此不建议使用jar包而是直接使用源码
2 代码编写之前的注意事项
(1)替换JCE策略文件
这主要是因为采用AES加密时,如果密钥长度大于128,会抛异常:java.security.InvalidKeyException: Illegal key size。下面是JCE的下载地址:
Java版本 JCE无限制权限策略文件下载地址
1.8 http://www.oracle.com/technetwork/java/javase/downloads/jce8-download-2133166.html
1.7 http://www.oracle.com/technetwork/java/javase/downloads/jce-7-download-432124.html
1.6 http://www.oracle.com/technetwork/java/javase/downloads/jce-6-download-429243.html
下载后替换%JDK_HOME%\jre\lib\security目录下对应的文件。
(2)使用微信提供的源代码
首先把zip解压缩,之后复制commons-codec这个jar包到项目,之后复制src下面的源代码粘贴到src下面
3 微信消息加解密需调整的代码
(1) 新增处理用户请求消息的2组方法
public static WXBizMsgCrypt getWxCrypt() {
WXBizMsgCrypt crypt=null;
try {
crypt = new WXBizMsgCrypt(SignUtil.token,"xxxxx","xxxxx");
} catch (AesException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return crypt;
}
这个方法用于取得微信消息加解密的实例
public static Map<String, String> parseXmlCrypt(HttpServletRequest request) throws Exception {
// 将解析结果存储在HashMap中
Map<String, String> map = new HashMap<String, String>();
// 从request中取得输入流
InputStream inputStream = request.getInputStream();
BufferedReader reader=new BufferedReader(new InputStreamReader(inputStream));
String line;
StringBuffer buf=new StringBuffer();
while((line=reader.readLine())!=null){
buf.append(line);
}
reader.close();
inputStream.close();
WXBizMsgCrypt wxCeypt=MessageUtil.getWxCrypt();
// 微信加密签名
String msgSignature = request.getParameter("msg_signature");
// 时间戳
String timestamp = request.getParameter("timestamp");
// 随机数
String nonce = request.getParameter("nonce");
String respXml=wxCeypt.decryptMsg(msgSignature, timestamp, nonce, buf.toString());
//SAXReader reader = new SAXReader();
Document document =DocumentHelper.parseText(respXml);
// 得到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;
}
该方法主要用于把用户发来的消息进行解密处理
/*
* 微信控制器(入口)
*/
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 将请求、响应的编码均设置为UTF-8(防止中文乱码)
request.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
String encryptType = request.getParameter("encrypt_type");
// 微信加密签名
String signature = request.getParameter("signature");
// 时间戳
String timestamp = request.getParameter("timestamp");
// 随机数
String nonce = request.getParameter("nonce");
// 响应消息
try{
PrintWriter out = response.getWriter();
if (SignUtil.checkSignature(signature, timestamp, nonce)) {
Map<String, String> requestMap = null;
if("aes".equals(encryptType)){
requestMap=MessageUtil.parseXmlCrypt(request);
String respXml=CoreService.processRequest(requestMap);
respXml=MessageUtil.getWxCrypt().encryptMsg(respXml,timestamp,nonce);
System.out.println(respXml);
out.print(respXml);
}else{
requestMap=MessageUtil.parseXml(request);
String respXml=CoreService.processRequest(requestMap);
//respXml=MessageUtil.getWxCrypt().encryptMsg(respXml,timestamp,nonce);
out.print(respXml);
System.out.println(respXml);
}
// 调用核心业务类接收消息、处理消息
//String respMessage = CoreService.processRequest(request);
out.close();
}
}catch(Exception e){
e.printStackTrace();
}
}
相比之前的代码加了一个encrypt_type,为了考虑程序的健壮性,程序根据encrypt_type的值进行了判断,使得程序同时支持消息的明文模式和安全模式,同时CoreService里的方法也进行了一定的调整(主要体现在参数上面)
package com.debug.weixin.service;
import java.util.Date;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import com.debug.weixin.message.res.CustomServiceMessage;
import com.debug.weixin.message.res.TextMessage;
import com.debug.weixin.pojo.TransInfo;
import com.debug.weixin.util.MessageUtil;
public class CoreService {
/**
* 处理微信发来的请求
*
* @param request
* @return
*/
public static String processRequest( Map<String, String> requestMap) {
String respMessage = null;
TextMessage tm=null;
try {
// 默认返回的文本消息内容
String respContent = "请求处理异常,请稍候尝试!";
// xml请求解析
//Map<String, String> requestMap = MessageUtil.parseXml(request);
// 发送方帐号(open_id)
String fromUserName = requestMap.get("FromUserName");
// 公众帐号
String toUserName = requestMap.get("ToUserName");
// 消息类型
String msgType = requestMap.get("MsgType");
// 回复文本消息
tm= new TextMessage();
tm.setToUserName(fromUserName);
tm.setFromUserName(toUserName);
tm.setCreateTime(new Date().getTime());
tm.setMsgType(MessageUtil.RESP_MESSAGE_TYPE_TEXT);
System.out.println("msgType:"+msgType);
// 文本消息
if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_TEXT)) {
String content=requestMap.get("Content");
if(content.startsWith("人工客服")){
CustomServiceMessage cus=new CustomServiceMessage();
cus.setToUserName(fromUserName);
cus.setFromUserName(toUserName);
cus.setCreateTime(new Date().getTime());
cus.setMsgType(MessageUtil.TRANSFER_CUSTOMER_SERVICE);
TransInfo t=new TransInfo();
t.setKfAccount("debug@php-wechat-crm");
cus.setTransInfo(t);
respMessage=MessageUtil.customMessageToXml(cus);
}else{
respContent = "您发送的是文本消息!";
}
}
// 图片消息
else if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_IMAGE)) {
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_VOICE)) {
respContent = "您发送的是音频消息!";
}
// 事件推送
else if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_EVENT)) {
// 事件类型
String eventType = requestMap.get("Event");
// 订阅
if (eventType.equals(MessageUtil.EVENT_TYPE_SUBSCRIBE)) {
String ticket=requestMap.get("Ticket");
if(ticket!=null&&!"".equals(ticket)){
respContent="谢谢您的关注!,你的ticket是:"+ticket;
}else{
respContent="谢谢您的关注!";
}
}
// 取消订阅
else if (eventType.equals(MessageUtil.EVENT_TYPE_UNSUBSCRIBE)) {
// TODO 取消订阅后用户再收不到公众号发送的消息,因此不需要回复消息
}
// 自定义菜单点击事件
else if (eventType.equals(MessageUtil.EVENT_TYPE_CLICK)) {
// TODO 自定义菜单权没有开放,暂不处理该类消息
String eventKey=requestMap.get("EventKey");
if("language".equalsIgnoreCase(eventKey.trim())){
}
}else if(eventType.equals("SCAN")){
String eventKey=requestMap.get("EventKey");
String ticket=requestMap.get("Ticket");
respContent="你的ticket是:"+ticket;
}
}
tm.setContent(respContent);
//respMessage = MessageUtil.textMessageToXml(textMessage);
} catch (Exception e) {
e.printStackTrace();
}
if(respMessage==null){
return MessageUtil.textMessageToXml(tm);
}else{
return respMessage;
}
//return respMessage;
}
}