Wechat.class.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462
  1. <?php
  2. // +----------------------------------------------------------------------
  3. // | ThinkPHP [ WE CAN DO IT JUST THINK IT ]
  4. // +----------------------------------------------------------------------
  5. // | Copyright (c) 2006-2015 http://thinkphp.cn All rights reserved.
  6. // +----------------------------------------------------------------------
  7. // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
  8. // +----------------------------------------------------------------------
  9. // | Author: 麦当苗儿 <zuojiazi.cn@gmail.com> <http://www.zjzit.cn>
  10. // +----------------------------------------------------------------------
  11. namespace Com;
  12. use Com\WechatCrypt;
  13. //支持在非ThinkPHP环境下使用
  14. defined('NOW_TIME') || define('NOW_TIME', $_SERVER['REQUEST_TIME']);
  15. defined('IS_GET') || define('IS_GET', $_SERVER['REQUEST_METHOD'] == 'GET');
  16. class Wechat {
  17. /**
  18. * 消息类型常量
  19. */
  20. const MSG_TYPE_TEXT = 'text';
  21. const MSG_TYPE_IMAGE = 'image';
  22. const MSG_TYPE_VOICE = 'voice';
  23. const MSG_TYPE_VIDEO = 'video';
  24. const MSG_TYPE_SHORTVIDEO = 'shortvideo';
  25. const MSG_TYPE_LOCATION = 'location';
  26. const MSG_TYPE_LINK = 'link';
  27. const MSG_TYPE_MUSIC = 'music';
  28. const MSG_TYPE_NEWS = 'news';
  29. const MSG_TYPE_EVENT = 'event';
  30. /**
  31. * 事件类型常量
  32. */
  33. const MSG_EVENT_SUBSCRIBE = 'subscribe';
  34. const MSG_EVENT_UNSUBSCRIBE = 'unsubscribe';
  35. const MSG_EVENT_SCAN = 'SCAN';
  36. const MSG_EVENT_LOCATION = 'LOCATION';
  37. const MSG_EVENT_CLICK = 'CLICK';
  38. const MSG_EVENT_VIEW = 'VIEW';
  39. /**
  40. * 微信推送过来的数据
  41. * @var array
  42. */
  43. private $data = array();
  44. /**
  45. * 微信TOKEN
  46. * @var string
  47. */
  48. private static $token = '';
  49. /**
  50. * 微信APP_ID
  51. * @var string
  52. */
  53. private static $appId = '';
  54. /**
  55. * 消息加密KEY
  56. * @var string
  57. */
  58. private static $encodingAESKey = '';
  59. /**
  60. * 是否使用安全模式
  61. * @var boolean
  62. */
  63. private static $msgSafeMode = false;
  64. /**
  65. * 构造方法,用于实例化微信SDK
  66. * 自动回复消息时实例化该SDK
  67. * @param string $token 微信后台填写的TOKEN
  68. * @param string $appid 微信APPID (安全模式和兼容模式有效)
  69. * @param string $key 消息加密KEY (EncodingAESKey)
  70. */
  71. public function __construct($token, $appid = '', $key = ''){
  72. //设置安全模式
  73. if(isset($_GET['encrypt_type']) && $_GET['encrypt_type'] == 'aes'){
  74. self::$msgSafeMode = true;
  75. }
  76. //参数验证
  77. if(self::$msgSafeMode){
  78. if(empty($key) || empty($appid)){
  79. throw new \Think\Exception('缺少参数EncodingAESKey或APP_ID!');
  80. }
  81. self::$appId = $appid;
  82. self::$encodingAESKey = $key;
  83. }
  84. //TOKEN验证
  85. if($token){
  86. self::auth($token) || exit;
  87. if(IS_GET){
  88. exit($_GET['echostr']);
  89. } else {
  90. self::$token = $token;
  91. $this->init();
  92. }
  93. } else {
  94. throw new \Think\Exception('缺少参数TOKEN!');
  95. }
  96. }
  97. /**
  98. * 初始化微信推送的数据
  99. */
  100. private function init(){
  101. $xml = file_get_contents("php://input");
  102. $data = self::xml2data($xml);
  103. //安全模式 或兼容模式
  104. if(self::$msgSafeMode){
  105. if(isset($data['MsgType'])){
  106. //兼容模式追加解密后的消息内容
  107. $data['Decrypt'] = self::extract($data['Encrypt']);
  108. } else {
  109. //安全模式
  110. $data = self::extract($data['Encrypt']);
  111. }
  112. }
  113. $this->data = $data;
  114. }
  115. /**
  116. * 获取微信推送的数据
  117. * @return array 转换为数组后的数据
  118. */
  119. public function request(){
  120. return $this->data;
  121. }
  122. /**
  123. * * 响应微信发送的信息(自动回复)
  124. * @param array $content 回复信息,文本信息为string类型
  125. * @param string $type 消息类型
  126. */
  127. public function response($content, $type = self::MSG_TYPE_TEXT){
  128. /* 基础数据 */
  129. $data = array(
  130. 'ToUserName' => $this->data['FromUserName'],
  131. 'FromUserName' => $this->data['ToUserName'],
  132. 'CreateTime' => NOW_TIME,
  133. 'MsgType' => $type,
  134. );
  135. /* 按类型添加额外数据 */
  136. $content = call_user_func(array(self, $type), $content);
  137. if($type == self::MSG_TYPE_TEXT || $type == self::MSG_TYPE_NEWS){
  138. $data = array_merge($data, $content);
  139. } else {
  140. $data[ucfirst($type)] = $content;
  141. }
  142. //安全模式,加密消息内容
  143. if(self::$msgSafeMode){
  144. $data = self::generate($data);
  145. }
  146. /* 转换数据为XML */
  147. $xml = new \SimpleXMLElement('<xml></xml>');
  148. self::data2xml($xml, $data);
  149. exit($xml->asXML());
  150. }
  151. /**
  152. * 回复文本消息
  153. * @param string $text 回复的文字
  154. */
  155. public function replyText($text){
  156. return $this->response($text, self::MSG_TYPE_TEXT);
  157. }
  158. /**
  159. * 回复图片消息
  160. * @param string $media_id 图片ID
  161. */
  162. public function replyImage($media_id){
  163. return $this->response($media_id, self::MSG_TYPE_IMAGE);
  164. }
  165. /**
  166. * 回复语音消息
  167. * @param string $media_id 音频ID
  168. */
  169. public function replyVoice($media_id){
  170. return $this->response($media_id, self::MSG_TYPE_VOICE);
  171. }
  172. /**
  173. * 回复视频消息
  174. * @param string $media_id 视频ID
  175. * @param string $title 视频标题
  176. * @param string $discription 视频描述
  177. */
  178. public function replyVideo($media_id, $title, $discription){
  179. return $this->response(func_get_args(), self::MSG_TYPE_VIDEO);
  180. }
  181. /**
  182. * 回复音乐消息
  183. * @param string $title 音乐标题
  184. * @param string $discription 音乐描述
  185. * @param string $musicurl 音乐链接
  186. * @param string $hqmusicurl 高品质音乐链接
  187. * @param string $thumb_media_id 缩略图ID
  188. */
  189. public function replyMusic($title, $discription, $musicurl, $hqmusicurl, $thumb_media_id){
  190. return $this->response(func_get_args(), self::MSG_TYPE_MUSIC);
  191. }
  192. /**
  193. * 回复图文消息,一个参数代表一条信息
  194. * @param array $news 图文内容 [标题,描述,URL,缩略图]
  195. * @param array $news1 图文内容 [标题,描述,URL,缩略图]
  196. * @param array $news2 图文内容 [标题,描述,URL,缩略图]
  197. * ... ...
  198. * @param array $news9 图文内容 [标题,描述,URL,缩略图]
  199. */
  200. public function replyNews($news, $news1, $news2, $news3){
  201. return $this->response(func_get_args(), self::MSG_TYPE_NEWS);
  202. }
  203. /**
  204. * 回复一条图文消息
  205. * @param string $title 文章标题
  206. * @param string $discription 文章简介
  207. * @param string $url 文章连接
  208. * @param string $picurl 文章缩略图
  209. */
  210. public function replyNewsOnce($title, $discription, $url, $picurl){
  211. return $this->response(array(func_get_args()), self::MSG_TYPE_NEWS);
  212. }
  213. /**
  214. * 数据XML编码
  215. * @param object $xml XML对象
  216. * @param mixed $data 数据
  217. * @param string $item 数字索引时的节点名称
  218. * @return string
  219. */
  220. protected static function data2xml($xml, $data, $item = 'item') {
  221. foreach ($data as $key => $value) {
  222. /* 指定默认的数字key */
  223. is_numeric($key) && $key = $item;
  224. /* 添加子元素 */
  225. if(is_array($value) || is_object($value)){
  226. $child = $xml->addChild($key);
  227. self::data2xml($child, $value, $item);
  228. } else {
  229. if(is_numeric($value)){
  230. $child = $xml->addChild($key, $value);
  231. } else {
  232. $child = $xml->addChild($key);
  233. $node = dom_import_simplexml($child);
  234. $cdata = $node->ownerDocument->createCDATASection($value);
  235. $node->appendChild($cdata);
  236. }
  237. }
  238. }
  239. }
  240. /**
  241. * XML数据解码
  242. * @param string $xml 原始XML字符串
  243. * @return array 解码后的数组
  244. */
  245. protected static function xml2data($xml){
  246. $xml = new \SimpleXMLElement($xml);
  247. if(!$xml){
  248. throw new \Think\Exception('非法XXML');
  249. }
  250. $data = array();
  251. foreach ($xml as $key => $value) {
  252. $data[$key] = strval($value);
  253. }
  254. return $data;
  255. }
  256. /**
  257. * 对数据进行签名认证,确保是微信发送的数据
  258. * @param string $token 微信开放平台设置的TOKEN
  259. * @return boolean true-签名正确,false-签名错误
  260. */
  261. protected static function auth($token){
  262. /* 获取数据 */
  263. $data = array($_GET['timestamp'], $_GET['nonce'], $token);
  264. $sign = $_GET['signature'];
  265. /* 对数据进行字典排序 */
  266. sort($data, SORT_STRING);
  267. /* 生成签名 */
  268. $signature = sha1(implode($data));
  269. return $signature === $sign;
  270. }
  271. /**
  272. * 构造文本信息
  273. * @param string $content 要回复的文本
  274. */
  275. private static function text($content){
  276. $data['Content'] = $content;
  277. return $data;
  278. }
  279. /**
  280. * 构造图片信息
  281. * @param integer $media 图片ID
  282. */
  283. private static function image($media){
  284. $data['MediaId'] = $media;
  285. return $data;
  286. }
  287. /**
  288. * 构造音频信息
  289. * @param integer $media 语音ID
  290. */
  291. private static function voice($media){
  292. $data['MediaId'] = $media;
  293. return $data;
  294. }
  295. /**
  296. * 构造视频信息
  297. * @param array $video 要回复的视频 [视频ID,标题,说明]
  298. */
  299. private static function video($video){
  300. $data = array();
  301. list(
  302. $data['MediaId'],
  303. $data['Title'],
  304. $data['Description'],
  305. ) = $video;
  306. return $data;
  307. }
  308. /**
  309. * 构造音乐信息
  310. * @param array $music 要回复的音乐[标题,说明,链接,高品质链接,缩略图ID]
  311. */
  312. private static function music($music){
  313. $data = array();
  314. list(
  315. $data['Title'],
  316. $data['Description'],
  317. $data['MusicUrl'],
  318. $data['HQMusicUrl'],
  319. $data['ThumbMediaId'],
  320. ) = $music;
  321. return $data;
  322. }
  323. /**
  324. * 构造图文信息
  325. * @param array $news 要回复的图文内容
  326. * [
  327. * 0 => 第一条图文信息[标题,说明,图片链接,全文连接],
  328. * 1 => 第二条图文信息[标题,说明,图片链接,全文连接],
  329. * 2 => 第三条图文信息[标题,说明,图片链接,全文连接],
  330. * ]
  331. */
  332. private static function news($news){
  333. $articles = array();
  334. foreach ($news as $key => $value) {
  335. list(
  336. $articles[$key]['Title'],
  337. $articles[$key]['Description'],
  338. $articles[$key]['Url'],
  339. $articles[$key]['PicUrl']
  340. ) = $value;
  341. if($key >= 9) break; //最多只允许10条图文信息
  342. }
  343. $data['ArticleCount'] = count($articles);
  344. $data['Articles'] = $articles;
  345. return $data;
  346. }
  347. /**
  348. * 验证并解密密文数据
  349. * @param string $encrypt 密文
  350. * @return array 解密后的数据
  351. */
  352. private static function extract($encrypt){
  353. //验证数据签名
  354. $signature = self::sign($_GET['timestamp'], $_GET['nonce'], $encrypt);
  355. if($signature != $_GET['msg_signature']){
  356. throw new \Think\Exception('数据签名错误!');
  357. }
  358. //消息解密对象
  359. $WechatCrypt = new WechatCrypt(self::$encodingAESKey, self::$appId);
  360. //解密得到回明文消息
  361. $decrypt = $WechatCrypt->decrypt($encrypt);
  362. //返回解密的数据
  363. return self::xml2data($decrypt);
  364. }
  365. /**
  366. * 加密并生成密文消息数据
  367. * @param array $data 获取到的加密的消息数据
  368. * @return array 生成的加密消息结构
  369. */
  370. private static function generate($data){
  371. /* 转换数据为XML */
  372. $xml = new \SimpleXMLElement('<xml></xml>');
  373. self::data2xml($xml, $data);
  374. $xml = $xml->asXML();
  375. //消息加密对象
  376. $WechatCrypt = new WechatCrypt(self::$encodingAESKey, self::$appId);
  377. //加密得到密文消息
  378. $encrypt = $WechatCrypt->encrypt($xml);
  379. //签名
  380. $nonce = mt_rand(0, 9999999999);
  381. $signature = self::sign(NOW_TIME, $nonce, $encrypt);
  382. /* 加密消息基础数据 */
  383. $data = array(
  384. 'Encrypt' => $encrypt,
  385. 'MsgSignature' => $signature,
  386. 'TimeStamp' => NOW_TIME,
  387. 'Nonce' => $nonce,
  388. );
  389. return $data;
  390. }
  391. /**
  392. * 生成数据签名
  393. * @param string $timestamp 时间戳
  394. * @param string $nonce 随机数
  395. * @param string $encrypt 被签名的数据
  396. * @return string SHA1签名
  397. */
  398. private static function sign($timestamp, $nonce, $encrypt){
  399. $sign = array(self::$token, $timestamp, $nonce, $encrypt);
  400. sort($sign, SORT_STRING);
  401. return sha1(implode($sign));
  402. }
  403. }