本文共 15420 字,大约阅读时间需要 51 分钟。
本节书摘来自华章出版社《微信公众平台开发最佳实践》一 书中的第2章,第2.2节,作者:方倍工作室,更多章节内容可以访问云栖社区“华章计算机”公众号查看。
登录微信公众平台后台,微信公众平台地址:mp.weixin.qq.com,在左侧列表中最下方,找到“开发者中心”,如图2-19所示。
单击进入开发者中心,可以看到当前有个服务器配置的信息,状态为未启用,如图2-20所示。
单击“修改配置”按钮,进入配置页面,如图2-21所示。此处的URL为上一节中介绍的云应用的域名,即为cctv15.sinaapp.com,而Token在index.php中定义为weixin,EncodingAESKey不需要填写,单击“随机生成”按钮,让系统自动生成一个即可,消息加解密方式选择“明文模式”,然后单击“提交”按钮。弹出确认框,如图2-22所示。在弹出的提示框中,单击“确定”按钮,相关参数填写成功,如图2-23所示。
再单击右上角的“启用”按钮来启用服务器的配置。系统弹出提示框,询问是否确定开启服务器配置,如图2-24所示。
单击“确定”按钮将启用服务器配置。
如果单击按钮后,上方提示“token验证失败”,可以重试几次,有时候微信服务器不稳定也会造成这样的情况,并不是程序本身有问题。?启用成功后界面如图2-25所示。这样就成功配置并启用了服务器。
在图2-21中,微信公众平台在配置服务器时,提供了3种加解密的模式供开发者选择,即明文模式、兼容模式、安全模式,选择兼容模式和安全模式前,需在开发者中心填写AES对称加密算法的消息加解密密钥EncodingAESKey。公众账号用此密钥对收到的密文消息体进行解密,回复消息体也用此密钥加密。
明文模式:维持现有模式,没有适配加解密新特性,消息体明文收发,默认设置为明文模式。兼容模式:公众平台发送消息内容将同时包括明文和密文,消息包长度增加到原来的3倍左右;公众账号回复明文或密文均可,不影响现有消息收发;开发者可在此模式下进行调试。安全模式(推荐):公众平台发送消息体的内容只含有密文,公众账号回复的消息体也为密文,建议开发者在调试成功后使用此模式收发消息。消息体加解密的实现过程如下:假设本次的开发配置中URL为www.fangbei.org/index.php接口程序中需要配置以下三项参数:/* 方倍工作室 http://www.cnblogs.com/txw1958/ CopyRight 2014 All Rights Reserved*/define("TOKEN", "weixin");define("AppID", "wxbad0b45542aa0b5e");define("EncodingAESKey", "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG");require_once('wxBizMsgCrypt.php’);```当用户向公众账号发送消息时,微信公众账号将会在URL中带上signature、timestamp、nonce、encrypt_type、msg_signature等参数,如下所示:
`
```这时,程序需要从URL中获得以下参数:
$timestamp = $_GET['timestamp’];
$nonce = $_GET["nonce"];$msg_signature = $_GET['msg_signature'];$encrypt_type = $_GET['encrypt_type’];`
这些参数将用于加解密过程。接口程序收到消息后,先进行解密,解密部分代码如下: $postStr = $GLOBALS["HTTP_RAW_POST_DATA"];if ($encrypt_type == 'aes'){ $pc = new WXBizMsgCrypt(TOKEN, EncodingAESKey, AppID); $this->logger(" D \r\n".$postStr); $decryptMsg = ""; //解密后的明文 $errCode = $pc->DecryptMsg($msg_signature, $timestamp, $nonce, $postStr, $decryptMsg); $postStr = $decryptMsg;}```解密完成后,把解密内容又返回给$postStr,这是为了保证将消息中解密后的内容和明文模式时的消息统一,方便后续处理,解密后的XML如下:
1414243737 6074130599188426998
`
```把上述消息进行加密,返回给微信公众账号,加密过程如下: 1414243733
//加密
if ($encrypt_type == 'aes'){$encryptMsg = ''; //加密后的密文$errCode = $pc->encryptMsg($result, $timeStamp, $nonce, $encryptMsg);$result = $encryptMsg;$this->logger(" E \r\n".$result);
}`
```这样,一个安全模式下的加解密消息就完成了。完整的代码清单如下: 1414243733
1 <?php
2 /* 3 方倍工作室 4 CopyRight 2014 All Rights Reserved 5 */ 6 define("TOKEN", "weixin"); 7 define("AppID", "wxbad0b45542aa0b5e"); 8 define("EncodingAESKey", "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG"); 9 require_once('wxBizMsgCrypt.php'); 10 11 $wechatObj = new wechatCallbackapiTest(); 12 if (!isset($_GET['echostr'])) { 13 $wechatObj->responseMsg(); 14 }else{ 15 $wechatObj->valid(); 16 } 17 18 class wechatCallbackapiTest 19 { 20 //验证签名 21 public function valid() 22 { 23 $echoStr = $_GET["echostr"]; 24 $signature = $_GET["signature"]; 25 $timestamp = $_GET["timestamp"]; 26 $nonce = $_GET["nonce"]; 27 $tmpArr = array(TOKEN, $timestamp, $nonce); 28 sort($tmpArr); 29 $tmpStr = implode($tmpArr); 30 $tmpStr = sha1($tmpStr); 31 if($tmpStr == $signature){ 32 echo $echoStr; 33 exit; 34 } 35 } 36 37 //响应消息 38 public function responseMsg() 39 { 40 $timestamp = $_GET['timestamp']; 41 $nonce = $_GET["nonce"]; 42 $msg_signature = $_GET['msg_signature']; 43 $encrypt_type = (isset($_GET['encrypt_type']) && ($_GET['encrypt_type'] == 'aes')) ? "aes" : "raw"; 44 45 $postStr = $GLOBALS["HTTP_RAW_POST_DATA"]; 46 if (!empty($postStr)){ 47 //解密 48 if ($encrypt_type == 'aes'){ 49 $pc = new WXBizMsgCrypt(TOKEN, EncodingAESKey, AppID); 50 $this->logger(" D \r\n".$postStr); 51 $decryptMsg = ""; //解密后的明文 52 $errCode = $pc->DecryptMsg($msg_signature, $timestamp, $nonce, $postStr, $decryptMsg); 53 $postStr = $decryptMsg; 54 } 55 $this->logger(" R \r\n".$postStr); 56 $postObj = simplexml_load_string($postStr, 'SimpleXMLElement', LIBXML_NOCDATA); 57 $RX_TYPE = trim($postObj->MsgType); 58 59 //消息类型分离 60 switch ($RX_TYPE) 61 { 62 case "event": 63 $result = $this->receiveEvent($postObj); 64 break; 65 case "text": 66 $result = $this->receiveText($postObj); 67 break; 68 } 69 $this->logger(" R \r\n".$result); 70 //加密 71 if ($encrypt_type == 'aes'){ 72 $encryptMsg = ''; //加密后的密文 73 $errCode = $pc->encryptMsg($result, $timeStamp, $nonce, $encryptMsg); 74 $result = $encryptMsg; 75 $this->logger(" E \r\n".$result); 76 } 77 echo $result; 78 }else { 79 echo ""; 80 exit; 81 } 82 } 83 84 //接收事件消息 85 private function receiveEvent($object) 86 { 87 $content = ""; 88 switch ($object->Event) 89 { 90 case "subscribe": 91 $content = "欢迎关注方倍工作室 "; 92 break; 93 } 94 95 $result = $this->transmitText($object, $content); 96 return $result; 97 } 98 99 //接收文本消息100 private function receiveText($object)101 { 102 $keyword = trim($object->Content);103 if (strstr($keyword, "文本")){ 104 $content = "这是个文本消息";105 }else if (strstr($keyword, "单图文")){ 106 $content = array();107 $content[] = array("Title"=>"单图文标题", "Description"=>"单图文内容", "PicUrl"=>"http://discuz.comli.com/weixin/weather/icon/cartoon.jpg", "Url" =>"http://m.cnblogs.com/?u=txw1958");108 }else if (strstr($keyword, "图文") || strstr($keyword, "多图文")){ 109 $content = array();110 $content[] = array("Title"=>"多图文1标题", "Description"=>"", "PicUrl" =>"http://discuz.comli.com/weixin/weather/icon/cartoon.jpg", "Url" =>"http://m.cnblogs.com/?u=txw1958");111 $content[] = array("Title"=>"多图文2标题", "Description"=>"", "PicUrl" =>" g", "Url" =>"http://m.cnblogs.com/?u=txw1958");112 $content[] = array("Title"=>"多图文3标题", "Description"=>"", "PicUrl"=>"http://g.hiphotos.bdimg.com/wisegame/pic/item/18cb0a46f21fbe090d338acc6a600c338644adfd.jpg", "Url" =>"http://m.cnblogs.com/?u=txw1958");113 }else if (strstr($keyword, "音乐")){ 114 $content = array();115 $content = array("Title"=>"最炫民族风", "Description"=>"歌手:凤凰传奇", "MusicUrl"=>"http://121.199.4.61/music/zxmzf.mp3", "HQMusicUrl"=>"http://121.199.4.61/music/zxmzf.mp3");116 }else{ 117 $content = date("Y-m-d H:i:s",time())."\n".$object->FromUserName."n技术支持 方倍工作室";118 }119120 if(is_array($content)){ 121 if (isset($content[0])){ 122 $result = $this->transmitNews($object, $content);123 }else if (isset($content['MusicUrl'])){ 124 $result = $this->transmitMusic($object, $content);125 }126 }else{ 127 $result = $this->transmitText($object, $content);128 }129 return $result;130 }131132 //回复文本消息133 private function transmitText($object, $content)134 { 135 $xmlTpl = "136 %s137 %s138 %s139 text140 %s141 ";142 $result = sprintf($xmlTpl, $object->FromUserName, $object->ToUserName, time(), $content);143 return $result;144 }145146 //回复图文消息147 private function transmitNews($object, $newsArray)148 { 149 if(!is_array($newsArray)){ 150 return;151 }152 $itemTpl = " 153 %s154 %s155 %s156 %s157 158 ";159 $item_str = "";160 foreach ($newsArray as $item){ 161 $item_str .= sprintf($itemTpl, $item['Title'], $item['Description'], $item['PicUrl'], $item['Url']);162 }163 $xmlTpl = "164 %s165 %s166 %s167 news168 %s169 170 $item_str 171 ";172173 $result = sprintf($xmlTpl, $object->FromUserName, $object->ToUserName, time(), count($newsArray));174 return $result;175 }176177 //回复音乐消息178 private function transmitMusic($object, $musicArray)179 { 180 $itemTpl = "181 %s182 %s183 %s184 %s185 ";186187 $item_str = sprintf($itemTpl, $musicArray['Title'], $musicArray['Description'], $musicArray['MusicUrl'], $musicArray['HQMusicUrl']);188189 $xmlTpl = "190 %s191 %s192 %s193 music194 $item_str195 ";196197 $result = sprintf($xmlTpl, $object->FromUserName, $object->ToUserName, time());198 return $result;199 }200201 //日志记录202 public function logger($log_content)203 { 204 if(isset($_SERVER['HTTP_APPNAME'])){ //SAE205 sae_set_display_errors(false);206 sae_debug($log_content);207 sae_set_display_errors(true);208 }else if($_SERVER['REMOTE_ADDR'] != "127.0.0.1"){ //LOCAL209 $max_size = 500000;210 $log_filename = "log.xml";211 if(file_exists($log_filename) and (abs(filesize($log_filename)) > $max_size)){unlink($log_filename);}212 file_put_contents($log_filename, date('Y-m-d H:i:s').$log_content."rn", FILE_APPEND);213 }214 }215 }216 ?>`
当我们在提交URL和Token的时候,有时候会碰到提交不成功的情况,具体有以下几种:
1)请求URL超时。这种情况一般是由于服务器网速或响应速度太慢。可以先重试几次或者等一段时间再来试,如果还是这样,则需要考虑更换速度更快、性能更好的服务器。2)系统发生错误,请稍后重试。这种情况一般是由于微信服务器短时间内异常引起的,重试或者过一段时间尝试即可。3)Token验证失败。这种情况需要具体分析验证过程被卡在哪一个环节了。我们可以通过调用变量$_SERVER来获取服务器和执行环境信息来分析。$_SERVER是一个包含了诸如头信息(header)、路径(path),以及脚本位置(script locations)等信息的数组。这个数组中的项目由Web服务器创建。这里我们需要使用以下两个元素:$_SERVER[‘REMOTE_ADDR’](来访者的IP地址,此处为微信服务器的IP)$_SERVER[‘QUERY_STRING’](查询请求字符串,此处为微信服务器发过来的GET请求字符串)将以上两个变量记录到日志中。函数定义如下:function traceHttp(){ $content = date('Y-m-d H:i:s')."\nREMOTE_ADDR:".$_SERVER["REMOTE_ADDR"]."\nQUERY_STRING:".$_SERVER["QUERY_STRING"]."\n\n"; if (isset($_SERVER['HTTP_APPNAME'])){ //SAE sae_set_display_errors(false); sae_debug(trim($content)); sae_set_display_errors(true); }else { $max_size = 100000; $log_filename = "log.xml"; if(file_exists($log_filename) and (abs(filesize($log_filename)) > $max_size)){unlink($log_filename);} file_put_contents($log_filename, $content, FILE_APPEND); }}```上面代码中,当环境为SAE时,使用SAE的调试函数sae_debug()将内容记录到日志中。而在具有读写权限的空间下,使用file_put_contents()函数把字符串写入文件。然后在程序的数据处理之前调用该函数,记录信息,代码如下所示:
define("TOKEN", "weixin");
traceHttp();$wechatObj = new wechatCallbackapiTest();if (isset($_GET['echostr'])) {$wechatObj->valid();
}else{
$wechatObj->responseMsg();
}`
2014-01-10 11:03:21REMOTE_ADDR:101.226.61.144QUERY_STRING:signature=6e35c6f3d3279338781047dbffd09426b9ecdee3&echostr=5979420653038092664×tamp=1392001400&nonce=1392192345```下面可以得出初步结论。如果没有生成日志文件:微信服务器没有访问到服务器,你需要先检查一下你的服务器是否可以通过公网访问,以及URL路径是否正确并且可以访问,如果公网访问而微信服务器不能访问,那可能是防火墙拦截了80端口或微信服务器IP,也可能服务器所在区域与微信服务器通信不畅,需要更换服务器。如果已经生成日志文件:查看REMOTE_ADDR和QUERY_STRING内容是否和上述类似。确认signature、timestamp、nonce、echostr四个参数是否都有值。如果这些都没有问题,则检查程序中定义的Token值是否和提交的一致,再检查程序流程及数据处理是否和官方文档描述的一致。如果确定以上均没有问题,可以使用下面章节中的微信调试器进行测试,它提供了更为宽松的校验方式,并且可以实时输出当前的XML数据供调试时参考。####2.2.4 自动回复当前时间在上面的例子中,我们已经嵌入了一个简单的时间查询功能,发送一个问号“?”就能回复当前的时间,如图2-26所示。这个功能是基于下面的代码实现的。
if($keyword == "?" || $keyword == "?")
{$msgType = "text";$content = date("Y-m-d H:i:s",time());$result = sprintf($textTpl, $fromUsername, $toUsername, $time, $msgType, $content);echo $result;
}`
现在我们结合上一节的代码来分析一下微信公众平台的消息交互原理。下面的代码是基于微信公众平台官方示例代码修改完善而成的。
1 valid();11 }else{12 $wechatObj->responseMsg();13 }14 15 class wechatCallbackapiTest16 {17 public function valid()18 {19 $echoStr = $_GET["echostr"];20 if($this->checkSignature()){21 echo $echoStr;22 exit;23 }24 }25 26 private function checkSignature()27 {28 $signature = $_GET["signature"];29 $timestamp = $_GET["timestamp"];30 $nonce = $_GET["nonce"];31 32 $token = TOKEN;33 $tmpArr = array($token, $timestamp, $nonce);34 sort($tmpArr);35 $tmpStr = implode( $tmpArr );36 $tmpStr = sha1( $tmpStr );37 38 if( $tmpStr == $signature ){39 return true;40 }else{41 return false;42 }43 }44 45 public function responseMsg()46 {47 $postStr = $GLOBALS["HTTP_RAW_POST_DATA"];48 49 if (!empty($postStr)){50 $postObj = simplexml_load_string($postStr, 'SimpleXMLElement', LIBXML_NOCDATA);51 $fromUsername = $postObj->FromUserName;52 $toUsername = $postObj->ToUserName;53 $keyword = trim($postObj->Content);54 $time = time();55 $textTpl = "56 ";63 if($keyword == "?" || $keyword == "?")64 {65 $msgType = "text";66 $content = date("Y-m-d H:i:s",time());67 $result = sprintf($textTpl, $fromUsername, $toUsername, $time, $msgType, $content);68 echo $result;69 }70 }else{71 echo "";72 exit;73 }74 }75 }76 ?>```首先我们来看一下代码的结构。第2行~第5行是注释部分。第7行使用define()函数定义常量,常量名称为TOKEN,常量的值为weixin,这个值就是在启用开发模式时填写的Token。第15行~第75行定义了一个类wechatCallbackapiTest,并在类中定义了3个方法valid()、checkSignature()、responseMsg()。第8行~第13行为程序执行语句。第8行实例化了一个类对象。在第9行中,判断GET请求是否设置了echostr变量,如果有则执行valid()方法,否则执行responseMsg()方法。然后分析微信消息交互流程,具体如下。在提交URL和Token申请验证的时候,微信服务器将发送GET请求到填写的URL上,并且带上四个参数(signature、timestamp、nonce、echostr),GET请求类似如下:57 58 %s 5960 61 0 62
signature=6e35c6f3d3279338781047dbffd09426b9ecdee3&echostr=5979420653038092664&
timestamp=1392001400&nonce=1392192345`
上述请求参数说明如表2-1所示。 这个GET请求是包含echostr变量的,所以执行valid()方法,在该方法中,又调用了校验签名方法checkSignature()方法。如果签名校验为真,则原样输出变量$echoStr的值。
加密/校验流程如下:1)第33行~第34行将token、timestamp、nonce三个参数进行字典序排序。2)第35行~第36行将三个参数字符串拼接成一个字符串进行sha1加密。3)第38行~第42行表示开发者获得加密后的字符串可与signature对比,标识该请求来源于微信。在发送问号的时候,微信服务器也将会带上前面三个参数(signature、timestamp、nonce)访问开发者设置的URL,同时还会将消息的XML数据包POST到URL上。XML格式类似如下:```而消息请求不包含echostr变量,所以将执行响应消息responseMsg()方法。第47行表示响应消息方法首先接收上述原始POST数据。第50行实现的功能是将数据载入对象中,对象名为SimpleXMLElement,LIBXML_NOCDATA表示将CDATA合并为文本节点。第51行~第54行取得XML类对象的值,并赋予新的变量,注意发送方变为接收方,接收方变为发送方。第55行~第62行构造要回复的XML数据包。第63行判断发送过来的关键字是不是问号。第64行~第65行设置回复的消息类型为text,内容为当前年、月、日、时、分、秒。第66行~第67行封装回复的XML数据包,并且向微信服务器输出。XML格式如下所示: 1392043637 5978781895719912033
1392043638
`