《微信企业号开发日志》本地调试程序二
上一节完成了微信回调测试,功能呢其实也没什么价值,只是个人使用。。
这一节,我们设置微信响应消息调试:
先来看看微信企业号官方的说明:来自 http://qydev.weixin.qq.com/wiki/index.php?title=%E5%9B%9E%E8%B0%83%E6%A8%A1%E5%BC%8F
-----------------------------------------------------------------------------------------------------------------------------------------------------------
企业号在回调企业URL时,会对消息体本身做AES加密,以XML格式POST到企业应用的URL上;企业在被动响应时,也需要对数据加密,以XML格式返回给微信。企业的回复支持文本、图片、语音、视频、图文等格式。
微信服务器在五秒内收不到响应会断掉连接,并且重新发起请求,总共重试三次。如果在调试中,发现员工无法收到响应的消息,可以检查是否消息处理超时。
关于重试的消息排重,有msgid的消息推荐使用msgid排重。事件类型消息推荐使用FromUserName + CreateTime排重。
假如企业无法保证在五秒内处理并回复,可以直接回复空串,企业号不会对此作任何处理,并且不会发起重试。这种情况下,可以使用发消息接口进行异步回复。
假设企业回调URL为http://api.3dept.com。
- 请求说明:
http://api.3dept.com/?msg_signature=ASDFQWEXZCVAQFASDFASDFSS×tamp=13500001234&nonce=123412323
- 回调数据格式:
<xml> <ToUserName><![CDATA[toUser]]</ToUserName> <AgentID><![CDATA[toAgentID]]</AgentID> <Encrypt><![CDATA[msg_encrypt]]</Encrypt> </xml>
1.msg_encrypt为经过加密的密文 2.AgentID为接收的应用id,可在应用的设置页面获取 3.ToUserName为企业号的CorpID
企业需要对msg_signature进行校验,并解密msg_encrypt,得出msg的原文。
- 被动响应给微信的数据格式:
<xml> <Encrypt><![CDATA[msg_encrypt]]></Encrypt> <MsgSignature><![CDATA[msg_signature]]></MsgSignature> <TimeStamp>timestamp</TimeStamp> <Nonce><![CDATA[nonce]]></Nonce> </xml>
1.msg_encrypt为经过加密的密文,算法参见附录 2.MsgSignature为签名,算法参见附录 3.TimeStamp为时间戳,Nonce为随机数,由企业自行生成
接收消息时的加解密处理
企业可以直接使用微信提供的库进行加解密的处理,目前提供的有c++/python/php/java/c#等语言版本。代码提供了解密、加密、验 证URL三个接口,企业可根据自身需要下载(参见附录)。以下为库函数的使用说明(以c++为例),更详细的加解密方案请参考附录。
1、解密函数
int DecryptMsg(const string &sMsgSignature, const string &sTimeStamp, const string &sNonce, const string &sPostData, string &sMsg);
- 参数说明
参数 | 必须 | 说明 |
---|---|---|
sMsgSignature | 是 | 从回调URL中获取的msg_signature参数 |
sTimeStamp | 是 | 从回调URL中获取的timestamp参数 |
sNonce | 是 | 从回调URL中获取的nonce参数 |
sPostData | 是 | 从回调URL中获取的整个post数据 |
sMsg | 是 | 用于返回解密后的msg,以xml组织 |
- 返回说明
请参阅附录加解密部分。
2、加密函数
int EncryptMsg(const string &sReplyMsg, const string &sTimeStamp, const string &sNonce, string &sEncryptMsg);
- 参数说明
参数 | 必须 | 说明 |
---|---|---|
sReplyMsg | 是 | 返回的消息体原文 |
sTimeStamp | 是 | 时间戳,调用方生成 |
sNonce | 是 | 随机数,调用方生成 |
sEncryptMsg | 是 | 用于返回的密文,以xml组织 |
-----------------------------------------------------------------------------------------------------------------------------------------------------------
试图模拟微信加密
- 被动响应给微信的数据格式:
<xml> <Encrypt><![CDATA[msg_encrypt]]></Encrypt> <MsgSignature><![CDATA[msg_signature]]></MsgSignature> <TimeStamp>timestamp</TimeStamp> <Nonce><![CDATA[nonce]]></Nonce> </xml>
1.msg_encrypt为经过加密的密文,算法参见附录 2.MsgSignature为签名,算法参见附录 3.TimeStamp为时间戳,Nonce为随机数,由企业自行生成
试图模拟msg_encrypt加密算法,官方这样说:
为了验证调用者的合法性,微信在回调url中增加了消息签名,以参数msg_signature标识,企业需要验证此参数的正确性后再解密。验证步骤:
1.企业计算签名:dev_msg_signature=sha1(sort(token、timestamp、nonce、msg_encrypt))。sort的含义是将参数按照字母字典排序,然后从小到大拼接成一个字符串
自己觉得搞不定,希望谁有这算法请告之,小弟谢过
无奈,只能跳过这个加密,本地就直接post明文啦,但在本地调试的时候也要跳过解密流程,如图
分析得到微信回调发送的XML明文为
<xml>
<ToUserName><![CDATA[xxxxxx]]></ToUserName> <FromUserName><![CDATA[xxxx]]></FromUserName> <CreateTime>1413794769</CreateTime> <MsgType><![CDATA[text]]></MsgType> <Content><![CDATA[cccc ]]></Content> <MsgId>4720174057645930000</MsgId> <AgentID>1</AgentID> </xml>
所以我们建立一个XML类:RequestXMLText.cs
代码如下
public class RequestXMLText { public string ToUserName { get; set; } public string FromUserName { get; set; } public string CreateTime { get; set; } public string MsgType { get; set; } public string Content { get; set; } public string MsgId { get; set; } public string AgentID { get; set; } string xmlformat = @"<xml> <ToUserName><![CDATA[{0}]]></ToUserName> <FromUserName><![CDATA[{1}]]></FromUserName> <CreateTime>{2}</CreateTime> <MsgType><![CDATA[{3}]]></MsgType> <Content><![CDATA[{4} ]]></Content> <MsgId>{5}</MsgId> <AgentID>{6}</AgentID> </xml>"; public string GetXML() { string Result = String.Format(xmlformat , ToUserName, FromUserName, CreateTime, MsgType, Content, MsgId, AgentID); return Result; } }
设计模拟微信发送消息的界面如下:
界面设计完成以后,需要给每个xml属性定义的textbox绑定Validted事件,我为了方便起见统一处理
在窗体load代码中写入:
//模拟微信发送消息 BoundXMLText XMLText = new BoundXMLText(); XMLText.Add(txt_ToUserName, BoundXMLText.ToUserName); XMLText.Add(txt_FromUserName, BoundXMLText.FromUserName); XMLText.Add(txt_CreateTime, BoundXMLText.CreateTime); XMLText.Add(txt_MsgType, BoundXMLText.MsgType); XMLText.Add(txt_Content, BoundXMLText.Content); XMLText.Add(txt_MsgId, BoundXMLText.MsgId); XMLText.Add(txt_AgentID, BoundXMLText.AgentID); XMLText.IniNotice(); XMLText.XMLChanged += XMLText_XMLChanged;
补充 XMLText_XMLChanged事件
void XMLText_XMLChanged(RequestXMLText xml) { txt_XMLText.Text = xml.GetXML(); }
BoundXMLText 类代码如下:
public delegate void XMLChanged(RequestXMLText xml); public class BoundXMLText { public const string ToUserName ="ToUserName"; public const string FromUserName = "FromUserName"; public const string CreateTime = "CreateTime"; public const string MsgType = "MsgType"; public const string Content = "Content"; public const string MsgId = "MsgId"; public const string AgentID = "AgentID"; public event XMLChanged XMLChanged; RequestXMLText xml=new RequestXMLText(); public RequestXMLText TxtXML { get { return xml; } } Dictionary<TextBox, string> lstTextBox = new Dictionary<TextBox, string>(); public void Add(TextBox txt, string Attribute) { if (lstTextBox.ContainsKey(txt)) return; lstTextBox.Add(txt, Attribute); } public void Remove(TextBox txt) { if (lstTextBox.ContainsKey(txt)) lstTextBox.Remove(txt); } public void IniNotice() { foreach (var key in lstTextBox.Keys) { key.Tag = lstTextBox[key]; key.Text = ConfigData.GetAttribute(lstTextBox[key]); key.Validated += key_Validated; } } TextBox CurrentTextBox; void key_Validated(object sender, EventArgs e) { CurrentTextBox = (sender as TextBox); string attribute = ConvertEx.ToString(CurrentTextBox.Tag); SetAttribute(attribute, CurrentTextBox.Text); if (XMLChanged != null) XMLChanged(TxtXML); } public void SetAttribute(string Attribte, string Value) { switch (Attribte) { case ToUserName: xml.ToUserName = Value; break; case FromUserName: xml.FromUserName = Value; break; case CreateTime: xml.CreateTime = Value; break; case MsgType: xml.MsgType = Value; break; case Content: xml.Content = Value; break; case MsgId: xml.MsgId = Value; break; case AgentID: xml.AgentID = Value; break; default: { throw new Exception("没有找到对应的字段:" + Attribte); } } } }
这样就完成了text绑定,达到的目的是,光标离开textbox后,xml会自动更新
最后写post发送事件
private void btn_SendText_Click(object sender, EventArgs e) { string URL=GenerateURL(); string Data=txt_XMLText.Text; if (String.IsNullOrEmpty(URL)) { return; } if (String.IsNullOrEmpty(Data)) { MessageBox.Show("需要Post的数据为空!,请填写内容!"); return; } txt_TextResult.Text = CommonTools.Post(URL, Data); }
本节完成
附上CommonTools类代码:
public class CommonTools { /// <summary> /// 获得消息创建时间 /// </summary> /// <returns></returns> public static string GetCreateTime() { return DateTime.Now.Subtract(new DateTime(1970, 1, 1, 8, 0, 0)).TotalSeconds.ToString(); } public static string GetWebData(string URL) { String ReCode = string.Empty; try { HttpWebRequest wNetr = (HttpWebRequest)HttpWebRequest.Create(URL); HttpWebResponse wNetp = (HttpWebResponse)wNetr.GetResponse(); wNetr.ContentType = "text/html"; wNetr.Method = "Get"; Stream Streams = wNetp.GetResponseStream(); StreamReader Reads = new StreamReader(Streams, Encoding.UTF8); ReCode = Reads.ReadToEnd(); //封闭临时不实用的资料 Reads.Dispose(); Streams.Dispose(); wNetp.Close(); } catch (Exception ex) { throw ex; } return ReCode; } public static string Post(string url, string data) { string returnData = null; try { byte[] buffer = Encoding.UTF8.GetBytes(data); HttpWebRequest webReq = (HttpWebRequest)WebRequest.Create(url); webReq.Method = "POST"; webReq.ContentType = "application/x-www-form-urlencoded"; webReq.ContentLength = buffer.Length; Stream postData = webReq.GetRequestStream(); webReq.Timeout = 99999999; //webReq.ReadWriteTimeout = 99999999; postData.Write(buffer, 0, buffer.Length); postData.Close(); HttpWebResponse webResp = (HttpWebResponse)webReq.GetResponse(); Stream answer = webResp.GetResponseStream(); StreamReader answerData = new StreamReader(answer); returnData = answerData.ReadToEnd(); } catch (Exception ex) { return "获取错误"; } return returnData.Trim() + "\n"; } }
本节完:
日志列表: