最近在帮同事弄他商店的会员系统,牵扯到激活会员卡后自动将信息保存到系统数据库中的功能。这其中就需要对微信服务器那边发送的消息进行接收和处理。然后就需要在公众号基本配置里配置服务器设置并启动,于是他公众号里的自定义菜单也要在系统里实现,瞬间感觉亏大发了。

服务器地址需要指向你所实现的一个验证微信签名的方法。无论是哪种消息加解密方式,验证签名微信服务所使用的都是Get方式,到接收微信发送的消息的时候是POST过来的。下面方法中加解密的类可以在微信公众平台上的Demo下载到,不需要自己处理,直接拿来用即可。
注意,使用wxcpt.DectyptMsg()解密的时候,第一个参数“签名”填的不是随url中的signature里获取到的签名,而是消息签名msg_signature,明文方式里没有这个,但是安全模式有。
public void WXcheckSignature()
{
string sReqMsgSig;
string echostr;
string sReqTimeStamp;
string encrypt_type;
string sReqNonce;
sReqMsgSig = HttpContext.Current.Request["signature"];
echostr = HttpContext.Current.Request["echostr"];
sReqTimeStamp = HttpContext.Current.Request["timeStamp"];
sReqNonce = HttpContext.Current.Request["nonce"];
encrypt_type = HttpContext.Current.Request["encrypt_type"]; //明文状态下没有这个的
if (HttpContext.Current.Request.HttpMethod == "POST"){
if (encrypt_type.ToLower() == "aes")
{
Tencent.WXBizMsgCrypt wxcpt = new Tencent.WXBizMsgCrypt(m_sToken, m_sEncodingAESKey, m_sAppID);
//获取加密的post消息
Stream stream = System.Web.HttpContext.Current.Request.InputStream;
stream.Position = 0;
byte[] xmlData = new byte[stream.Length];
stream.Read(xmlData, 0, xmlData.Length);
string sReqData = Encoding.UTF8.GetString(xmlData);
XmlDocument m_doc = new XmlDocument();
m_doc.LoadXml(sReqData);
XmlNode m_root = m_doc.FirstChild;
string msgSignature = HttpContext.Current.Request["msg_signature"];
string sMsg = ""; //解析之后的明文
int ret = 0;
ret = wxcpt.DecryptMsg(msgSignature, sReqTimeStamp, sReqNonce, sReqData, ref sMsg);
if (ret != 0)
{
System.Console.WriteLine("ERR: Decrypt fail, ret: " + ret);
}
else
{
EventInfoAnalysis(sMsg);
}
//string fromUser = root["FromUserName"].InnerText;
}
}
else //验证微信签名
{
string[] ArrTmp = { m_sToken, sReqTimeStamp, sReqNonce };
Array.Sort(ArrTmp); //字典排序
string tmpStr = string.Join("", ArrTmp);
tmpStr = FormsAuthentication.HashPasswordForStoringInConfigFile(tmpStr, "SHA1");
tmpStr = tmpStr.ToLower();
if (tmpStr.Equals(sReqMsgSig))
{
HttpContext.Current.Response.ContentEncoding = Encoding.UTF8;
HttpContext.Current.Response.Write(echostr);
}
}
}
在设置后保存时就会立刻开始验证了,所以要保证你所写的服务器地址能访问到。如果是自己电脑上测试,可以用“花生壳”什么的挂到外网上,不过花生壳是收费的。
<xml>
<ToUserName> < ![CDATA[gh_3fcea188bf78] ]></ToUserName>
<FromUserName>< ![CDATA[obLatjlaNQKb8FqOvt1M1x1lIBFE] ]></FromUserName>
<CreateTime>1432668700</CreateTime>
<MsgType>< ![CDATA[event] ]></MsgType>
<Event>< ![CDATA[submit_membercard_user_info] ]></Event>
<CardId>< ![CDATA[pbLatjtZ7v1BG_ZnTjbW85GYc_E8] ]></CardId>
<UserCardCode>< ![CDATA[018255396048] ]></UserCardCode>
</xml>
微信公众号开发文档-接收普通消息
上面这一段是接收到的明文消息,ToUserName指向你的服务,FromUserName则是用户的OpenId。
MsgType有几种类型,我这里只处理了“event”和“text”,因为实际需求里只针对菜单点击动作和文本消息自动回复这两个。回复的时候把接收到的xml消息处理后,将ToUserName和FromUserName换一下位置即可。
官方文档里面提到这个,所以一开始你不能确定是因为你的服务器地址写错了还是回复格式出错了,就回复空白HttpContext.Current.Response.Write("");如果你在公众号上发消息没有收到任何消息,也没有出现提示“该公众号提供的服务出现故障,请稍后再试”则证明你的地址是没有问题的。
下面的内容是我在官方文档下的demo里面的,因为我自己处理的忘记截图了。加密后接收的消息是这样的。
<xml>
<ToUserName><![CDATA[wx5823bf96d3bd56c7]]></ToUserName>
<Encrypt><![CDATA[RypEvHKD8QQKFhvQ6QleEB4J58tiPdvo+rtK1I9qca6aM/wvqnLSV5zEPeusUiX5L5X/0lWfrf0QADHHhGd3QczcdCUpj911L3vg3W/sYYvuJTs3TUUkSUXxaccAS0qhxchrRYt66wiSpGLYL42aM6A8dTT+6k4aSknmPj48kzJs8qLjvd4Xgpue06DOdnLxAUHzM6+kDZ+HMZfJYuR+LtwGc2hgf5gsijff0ekUNXZiqATP7PF5mZxZ3Izoun1s4zG4LUMnvw2r+KqCKIw+3IQH03v+BCA9nMELNqbSf6tiWSrXJB3LAVGUcallcrw8V2t9EL4EhzJWrQUax5wLVMNS0+rUPA3k22Ncx4XXZS9o0MBH27Bo6BpNelZpS+/uh9KsNlY6bHCmJU9p8g7m3fVKn28H3KDYA5Pl/T8Z1ptDAVe0lXdQ2YoyyH2uyPIGHBZZIs2pDBS8R07+qN+E7Q==]]></Encrypt>
</xml>
解密出来之后就类似下面这样(形式相同,我复制的可能是内容上毫不相干的)
<xml>
<ToUserName><![CDATA[mycreate]]></ToUserName>
<FromUserName><![CDATA[wx5823bf96d3bd56c7]]></FromUserName>
<CreateTime>1348831860</CreateTime>
<MsgType><![CDATA[text]]></MsgType>
<Content><![CDATA[this is a test]]></Content>
<MsgId>1234567890123456</MsgId>
</xml>
然后就需要解析xml的内容了,我看其他人的处理方式大多数是处理后以Map的形式来,但是C#里面明明有XmlDocument可以用,很奇怪。
回复文本、视频、语音等消息都是以xml的形式,其中视频一类的媒体消息需要你在公众号素材库里有相应的素材,你可以使用微信上的接口https://api.weixin.qq.com/cgi-bin/material/batchget_material?access_token= 你的access_token 获取到素材的msgId。
private void EventInfoAnalysis(string xmlStr){
//从xml消息中获取解码用的值
XmlDocument doc = new XmlDocument();
doc.LoadXml(xmlStr);
XmlNode root = doc.FirstChild;
string msgType = root["MsgType"].InnerText;
switch(msgType.ToLower()){
case "event":
//会员卡激活
string eventType = root["Event"].InnerText;
string userName = root["FromUserName"].InnerText;
string fromName = root["ToUserName"].InnerText;
switch(eventType){
case "submit_membercard_user_info":
string cardCode = root["UserCardCode"].InnerText;
string cardId = root["CardId"].InnerText;
string jsonResult = new MyHttpRequest().MExcute("GetMemberInfo", string.Format("cardId={0}&code={1}", cardId, cardCode));
if (!string.IsNullOrEmpty(jsonResult))
{
MemCardAliveInfo info = TokenTicketUtil.JsonParseBeanData<MemCardAliveInfo>(jsonResult);
if (info != null)
{
memCardActive(info, "", "");
}
}
break;
case "VIEW":
//介绍等连接 暂时不需要做什么
break;
case "CLICK": //菜单点击事件
string eventKey = root["EventKey"].InnerText;
switch(eventKey){
case "V1001_PHONE":
ResponseToWeChat(userName,fromName, @"预约热线:******");
break;
case "V1001_VIDEO": //视频
ResponseVideoToWeChat(userName, fromName, "video", "********", "企业视频",
@"同修仁德 济世养生
中华老字号 南京同仁堂 乐家药铺");
break;
case "V1001_MEM_CARD": //尊享会员卡
string xml0 = new MyHttpRequest().MExcute("sendMemCoups", string.Format("cardId={0}&userName={1}", "*****", userName));
HttpContext.Current.Response.Write("");
break;
case "V1001_NEW_COUP": //10元新会员优惠券
string xml1 = new MyHttpRequest().MExcute("sendMemCoups", string.Format("cardId={0}&userName={1}", "****", userName));
HttpContext.Current.Response.Write("");
break;
default:
break;
}
break;
}
break;
case "text":
string content = root["Content"].InnerText;
string name = root["FromUserName"].InnerText;
string fName = root["ToUserName"].InnerText;
ResponseToWeChat(name, fName, "感谢您的来访,自动回复功能正在完善中,暂无法为您提供此服务。");
break;
default:
break;
}
}
回复的内容也是要以xml的形式来,按照文档上的要求来就行了,我把文本消息和视频消息回复的内容放上来。
/// <summary>
/// 根据微信消息回应文本消息
/// </summary>
/// <param name="toUser"></param>
/// <param name="replyText"></param>
public string ResponseToWeChat(string toUser, string fromUser, string replyText)
{
string result = "";
Tencent.WXBizMsgCrypt wxcpt = new Tencent.WXBizMsgCrypt(m_sToken, m_sEncodingAESKey, m_sAppID);
string messageTime = MessageCreateTime();
string sRespData = string.Format(@"<xml>
<ToUserName><![CDATA[{0}]]></ToUserName>
<FromUserName><![CDATA[{1}]]></FromUserName>
<CreateTime>{2}</CreateTime>
<MsgType><![CDATA[text]]></MsgType>
<Content><![CDATA[{3}]]></Content>
</xml>", toUser, fromUser, messageTime, replyText);
string sEncryptMsg = ""; //xml格式的密文
int ret = wxcpt.EncryptMsg(sRespData, messageTime, GetRandomString(10, true, false, false, false, ""), ref sEncryptMsg);
//System.Console.WriteLine("sEncryptMsg");
//System.Console.WriteLine(sEncryptMsg);
if (ret != 0)
{
result = "{\"ERR\": Decrypt fail, ret: " + ret + "}";
System.Console.WriteLine("ERR: Decrypt fail, ret: " + ret);
}
else {
result = "{\"MSG\":" + sEncryptMsg + "}";
}
HttpContext.Current.Response.ContentEncoding = Encoding.UTF8;
HttpContext.Current.Response.Write(sEncryptMsg);
return result;
}
/// <summary>
/// 根据微信消息回应视频信息
/// </summary>
/// <param name="toUser"></param>
/// <param name="msgType"></param>
/// <param name="mediaID"></param>
private void ResponseVideoToWeChat(string toUser, string fromUser, string msgType, string mediaID, string title, string description)
{
Tencent.WXBizMsgCrypt wxcpt = new Tencent.WXBizMsgCrypt(m_sToken, m_sEncodingAESKey, m_sAppID);
string messageTime = MessageCreateTime();
string sRespData = string.Format(@"<xml>
<ToUserName><![CDATA[{0}]]></ToUserName>
<FromUserName><![CDATA[{1}]]></FromUserName>
<CreateTime>{2}</CreateTime>
<MsgType><![CDATA[{3}]]></MsgType>
<Video>
<MediaId><![CDATA[{4}]]></MediaId>
<Title><![CDATA[{5}]]></Title>
<Description><![CDATA[{6}]]></Description>
</Video>
</xml>", toUser, fromUser, messageTime, msgType, mediaID, title, description);
string sEncryptMsg = ""; //xml格式的密文
int ret = wxcpt.EncryptMsg(sRespData, messageTime, GetRandomString(10, true, false, false, false, ""), ref sEncryptMsg);
//System.Console.WriteLine("sEncryptMsg");
//System.Console.WriteLine(sEncryptMsg);
if (ret != 0)
{
System.Console.WriteLine("ERR: Decrypt fail, ret: " + ret);
}
HttpContext.Current.Response.ContentEncoding = Encoding.UTF8;
HttpContext.Current.Response.Write(sEncryptMsg);
}
其中保险起见,加密用的timesmap要与写在xml回复消息里的CreateTime一致,不一致的话应该会出问题吧,我猜。。。wxcpt.EnctyptMsg()这个方法里面的第三个参数是一个随机字符串,由你自己设置,你可以自己编一套规则生成。到这里基本上就完成了,是不是很简单?