JFinal weixin提供了微信消息加解密的实现方式,这里对实现的过程进行简单的解析。
企业号回调模式的消息格式说明 企业号在回调企业URL时,会对消息体本身做AES加密,以XML格式POST到企业应用的URL上;企业在被动响应时,也需要对数据加密,以XML格式返回给微信。 微信服务器在五秒内收不到响应会断掉连接,并且重新发起请求,总共重试三次。如果在调试中,发现成员无法收到响应的消息,可以检查是否消息处理超时。
当接收成功后,http头部返回200表示接收ok,其他错误码一律当做失败并发起重试
关于重试的消息排重,有msgid的消息推荐使用msgid排重。事件类型消息推荐使用FromUserName + CreateTime排重。
假如企业无法保证在五秒内处理并回复,可以直接回复空串,企业号不会对此作任何处理,并且不会发起重试。这种情况下,可以使用发消息接口进行异步回复。
假设企业回调URL为http://api.3dept.com。
msg_encrypt 为经过加密的密文
AgentID 为接收的应用id,可在应用的设置页面获取
ToUserName 为企业号的 CorpID
1 2 3 4 5 <xml> <ToUserName><[CDATA[toUser]]</ToUserName> <AgentID><[CDATA[toAgentID]]</AgentID> <Encrypt><[CDATA[msg_encrypt]]</Encrypt> </xml>
企业需要对 msg_signature 进行校验,并解密 msg_encrypt ,得出 msg 的原文。
msg_encrypt 为经过加密的密文,算法参见附录
MsgSignature 为签名,算法参见附录
TimeStamp 为时间戳,Nonce 为随机数,由企业自行生成
1 2 3 4 5 6 <xml > <Encrypt > <[CDATA[msg_encrypt]] > </Encrypt > <MsgSignature > <[CDATA[msg_signature]] > </MsgSignature > <TimeStamp > timestamp</TimeStamp > <Nonce > <[CDATA[nonce]] > </Nonce > </xml >
jfinal-weixin的解密的代码实现(存在代码冗余)及改进方案 1、从request请求中读取消息
1 inMsgXml = HttpKit.readIncommingRequestData(getRequest());
readIncommingRequestData 方法通过 getReader() 读取,进行每行读取后进行append拼接;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public static String readIncommingRequestData (HttpServletRequest request) { BufferedReader br = null ; try { StringBuilder result = new StringBuilder(); br = request.getReader(); for (String line=null ; (line=br.readLine())!=null ;) { result.append(line).append("\n" ); } return result.toString(); } catch (IOException e) { throw new RuntimeException(e); } finally { if (br != null ) try {br.close();} catch (IOException e) {e.printStackTrace();} } }
2、在回调模式中可以配置是消息采用明文还是密文;需要去判断是否需要对进行进行解密;
1 2 3 4 5 6 if (ApiConfigKit.getApiConfig().isEncryptMessage()){ inMsgXml = MsgEncryptKit.decrypt(inMsgXml, getPara("timestamp" ), getPara("nonce" ), getPara("msg_signature" )); }
3、消息的解密的类 WXBizMsgCrypt 采用的是微信提供的方法;decryptMsg 方法提供了对消息进行解密接口,接口参数需要传递 msg_signature 、 timestamp 、 nonce 、 postData (密文,实际使用的发送过来的请求数据。);
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public String decryptMsg (String msgSignature, String timeStamp, String nonce, String postData) throws AesException { Object[] encrypt = XMLParse.extract(postData); String signature = SHA1.getSHA1(token, timeStamp, nonce, encrypt[1 ].toString()); if (!signature.equals(msgSignature)) { throw new AesException(AesException.ValidateSignatureError); } String result = decrypt(encrypt[1 ].toString()); return result; }
XMLParse.extract(postData) 提取消息,并将消息转换document,方便进行元素的提取;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 * 提取出xml数据包中的加密消息 * @param xmltext 待提取的xml字符串 * @return 提取出的加密消息字符串 * @throws AesException */ public static Object[] extract(String xmltext) throws AesException { Object[] result = new Object[4 ]; try { DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); DocumentBuilder db = dbf.newDocumentBuilder(); StringReader sr = new StringReader(xmltext); InputSource is = new InputSource(sr); Document document = db.parse(is); Element root = document.getDocumentElement(); NodeList nodelist1 = root.getElementsByTagName("Encrypt" ); NodeList nodelist2 = root.getElementsByTagName("ToUserName" ); NodeList nodelist3 = root.getElementsByTagName("AgentID" ); result[0 ] = 0 ; result[1 ] = nodelist1.item(0 ).getTextContent(); result[2 ] = nodelist2.item(0 ).getTextContent(); result[3 ] = nodelist3.item(0 ).getTextContent(); return result; } catch (Exception e) { e.printStackTrace(); throw new AesException(AesException.ParseXmlError); } }
4、MsgEncryptKit 调用 WXBizMsgCrypt.decryptMsg 方法进行消息的解密;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 private static final String format = "<xml><ToUserName><[CDATA[toUser]]></ToUserName><Encrypt><[CDATA[%1$s]]></Encrypt><AgentID><[CDATA[agentId]]></AgentID></xml>" ;public static String decrypt (String encryptedMsg, String timestamp, String nonce, String msgSignature) { try { ApiConfig ac = ApiConfigKit.getApiConfig(); DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); DocumentBuilder db = dbf.newDocumentBuilder(); StringReader sr = new StringReader(encryptedMsg); InputSource is = new InputSource(sr); Document document = db.parse(is); Element root = document.getDocumentElement(); NodeList nodelist1 = root.getElementsByTagName("Encrypt" ); String encrypt = nodelist1.item(0 ).getTextContent(); String fromXML = String.format(format, encrypt); String encodingAesKey = ac.getEncodingAesKey(); if (encodingAesKey == null ) throw new IllegalStateException("encodingAesKey can not be null, config encodingAesKey first." ); WXBizMsgCrypt pc = new WXBizMsgCrypt(ac.getToken(), encodingAesKey, ac.getCorpId()); return pc.decryptMsg(msgSignature, timestamp, nonce, fromXML); } catch (Exception e) { throw new RuntimeException(e); } }
这里的 encryptedMsg 为解密的密文,使用 String fromXML = String.format(format, encrypt) ;将密文重新 封装到XML格式 format 的字符串中 fromXML 。 然后在调用 WXBizMsgCrypt.decryptMsg() 方法中,XMLPraser.extract() 方法去解析xml文本;这里进行了重复性的工作。
改进方法是去除重复的密文的封装和拆解;在decrypt方法中不需要再进行读取密文,root.getElementsByTagName(“Encrypt”) ;然后进行封装,String fromXML = String.format(format, encrypt) 。 可以修改为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public static String decrypt (String encryptedMsg, String timestamp, String nonce, String msgSignature) { try { ApiConfig ac = ApiConfigKit.getApiConfig(); String encodingAesKey = ac.getEncodingAesKey(); if (encodingAesKey == null ) throw new IllegalStateException("encodingAesKey can not be null, config encodingAesKey first." ); WXBizMsgCrypt pc = new WXBizMsgCrypt(ac.getToken(), encodingAesKey, ac.getCorpId()); return pc.decryptMsg(msgSignature, timestamp, nonce, encryptedMsg); } catch (Exception e) { throw new RuntimeException(e); } }
或者 WXBizMsgCrypt.decryptMsg 的postData 为密文,去除 XMLParse.extract(postData) 方法;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public String decryptMsg (String msgSignature, String timeStamp, String nonce, String postData) throws AesException { String signature = SHA1.getSHA1(token, timeStamp, nonce, postData.toString()); if (!signature.equals(msgSignature)) { throw new AesException(AesException.ValidateSignatureError); } String result = decrypt(postData.toString()); return result; }
jfinal-weixin的加密的代码实现 1、先将发送的消息按照消息的格式进行封装
1 String outMsgXml = OutMsgXmlBuilder.build(outMsg);
2、判断消息是否需要加密
1 if (ApiConfigKit.getApiConfig().isEncryptMessage())
3、使用 MsgEncryptKit.encrypt 方法进行加密
1 2 3 4 5 6 7 8 9 10 11 12 13 14 outMsgXml = MsgEncryptKit.encrypt(outMsgXml,getPara("timestamp" ), getPara("nonce" )); public static String encrypt (String msg, String timestamp, String nonce) { try { ApiConfig ac = ApiConfigKit.getApiConfig(); WXBizMsgCrypt pc = new WXBizMsgCrypt(ac.getToken(), ac.getEncodingAesKey(), ac.getCorpId()); log.info("WXBizMsgCrypt pc" + pc.toString()); log.info(pc.encryptMsg(msg, timestamp, nonce)); return pc.encryptMsg(msg, timestamp, nonce); } catch (Exception e) { throw new RuntimeException(e); } }
具体的加密工作是由 WXBizMsgCrypt.encryptMsg 方法来进行操作;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public String encryptMsg (String replyMsg, String timeStamp, String nonce) throws AesException { String encrypt = encrypt(getRandomStr(), replyMsg); if (timeStamp == "" ) { timeStamp = Long.toString(System.currentTimeMillis()); } String signature = SHA1.getSHA1(token, timeStamp, nonce, encrypt); String result = XMLParse.generate(encrypt, signature, timeStamp, nonce); return result; }
这里 XMLParse.generate() 方法将密文封装为要求的xml格式的字符串。
1 2 3 4 5 6 7 public static String generate (String encrypt, String signature, String timestamp, String nonce) { String format = "<xml>\n" + "<Encrypt><![CDATA[%1$s]]></Encrypt>\n" + "<MsgSignature><![CDATA[%2$s]]></MsgSignature>\n" + "<TimeStamp>%3$s</TimeStamp>\n" + "<Nonce><![CDATA[%4$s]]></Nonce>\n" + "</xml>" ; return String.format(format, encrypt, signature, timestamp, nonce); }
这里得到一个教训,在学习别人的代码的时候,一定要认真去理解,不要认为别人写的代码就是完美的。要勇于去提出问题和进行代码的优化和修改。