WechatAuth.class.php 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702
  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. class WechatAuth {
  13. /* 消息类型常量 */
  14. const MSG_TYPE_TEXT = 'text';
  15. const MSG_TYPE_IMAGE = 'image';
  16. const MSG_TYPE_VOICE = 'voice';
  17. const MSG_TYPE_VIDEO = 'video';
  18. const MSG_TYPE_SHORTVIDEO = 'shortvideo';
  19. const MSG_TYPE_LOCATION = 'location';
  20. const MSG_TYPE_LINK = 'link';
  21. const MSG_TYPE_MUSIC = 'music';
  22. const MSG_TYPE_NEWS = 'news';
  23. const MSG_TYPE_EVENT = 'event';
  24. /* 二维码类型常量 */
  25. const QR_SCENE = 'QR_SCENE';
  26. const QR_LIMIT_SCENE = 'QR_LIMIT_SCENE';
  27. /**
  28. * 微信开发者申请的appID
  29. * @var string
  30. */
  31. private $appId = '';
  32. /**
  33. * 微信开发者申请的appSecret
  34. * @var string
  35. */
  36. private $appSecret = '';
  37. /**
  38. * 获取到的access_token
  39. * @var string
  40. */
  41. private $accessToken = '';
  42. /**
  43. * 微信api根路径
  44. * @var string
  45. */
  46. private $apiURL = 'https://api.weixin.qq.com/cgi-bin';
  47. /**
  48. * 微信二维码根路径
  49. * @var string
  50. */
  51. private $qrcodeURL = 'https://mp.weixin.qq.com/cgi-bin';
  52. private $requestCodeURL = 'https://open.weixin.qq.com/connect/oauth2/authorize';
  53. private $oauthApiURL = 'https://api.weixin.qq.com/sns';
  54. private $userinfo = 'https://api.weixin.qq.com/cgi-bin/user/info';
  55. /**
  56. * 构造方法,调用微信高级接口时实例化SDK
  57. * @param string $appid 微信appid
  58. * @param string $secret 微信appsecret
  59. * @param string $token 获取到的access_token
  60. */
  61. public function __construct($appid, $secret, $token = null){
  62. if($appid && $secret){
  63. $this->appId = $appid;
  64. $this->appSecret = $secret;
  65. if(!empty($token)){
  66. $this->accessToken = $token;
  67. }
  68. } else {
  69. throw new \Think\Exception('缺少参数 APP_ID 和 APP_SECRET!');
  70. }
  71. }
  72. public function getRequestCodeURL($redirect_uri, $state = null,$scope = 'snsapi_userinfo'){
  73. $query = array(
  74. 'appid' => $this->appId,
  75. 'redirect_uri' => $redirect_uri,
  76. 'response_type' => 'code',
  77. 'scope' => $scope,
  78. );
  79. if(!is_null($state) && preg_match('/[a-zA-Z0-9]+/', $state)){
  80. $query['state'] = $state;
  81. }
  82. $query = http_build_query($query);
  83. return "{$this->requestCodeURL}?{$query}#wechat_redirect";
  84. }
  85. /**
  86. * 获取access_token,用于后续接口访问
  87. * @return array access_token信息,包含 token 和有效期
  88. */
  89. public function getAccessToken($type = 'client', $code = null){
  90. $param = array(
  91. 'appid' => $this->appId,
  92. 'secret' => $this->appSecret
  93. );
  94. switch ($type) {
  95. case 'client':
  96. $param['grant_type'] = 'client_credential';
  97. $url = "{$this->apiURL}/token";
  98. break;
  99. case 'code':
  100. $param['code'] = $code;
  101. $param['grant_type'] = 'authorization_code';
  102. $url = "{$this->oauthApiURL}/oauth2/access_token";
  103. break;
  104. default:
  105. throw new \Think\Exception('不支持的grant_type类型!');
  106. break;
  107. }
  108. $token = self::http($url, $param);
  109. $token = json_decode($token, true);
  110. file_put_contents('./data.json',json_encode($token)."\r\n",FILE_APPEND);
  111. if(is_array($token)){
  112. if(isset($token['errcode'])){
  113. throw new \Think\Exception($token['errmsg']);
  114. } else {
  115. $this->accessToken = $token['access_token'];
  116. return $token;
  117. }
  118. } else {
  119. throw new \Think\Exception('获取微信access_token失败!');
  120. }
  121. }
  122. /**
  123. * 获取关注用户信息
  124. * @param string $openid 用户的OpenID
  125. * @param string $lang 指定的语言
  126. * @return array 用户信息数据,具体参见微信文档
  127. */
  128. public function getUserInfo($openid, $lang = 'zh_CN'){
  129. $query = array(
  130. 'access_token' => $this->accessToken,
  131. 'openid' => $openid,
  132. 'lang' => $lang,
  133. );
  134. //$info = self::http("{$this->oauthApiURL}/userinfo", $query);
  135. $info = self::http("{$this->userinfo}", $query);
  136. return json_decode($info, true);
  137. }
  138. /**
  139. * 获取授权用户信息
  140. * @param string $openid 用户的OpenID
  141. * @param string $lang 指定的语言
  142. * @return array 用户信息数据,具体参见微信文档
  143. */
  144. public function getOauthUserinfo($access_token, $openid, $lang = 'zh_CN'){
  145. $query = array(
  146. 'access_token' => $access_token,
  147. 'openid' => $openid,
  148. 'lang' => $lang,
  149. );
  150. //echo $this->oauthApiURL."/userinfo?" . http_build_query($query);exit;
  151. $info = self::http("{$this->oauthApiURL}/userinfo", $query);
  152. return json_decode($info, true);
  153. }
  154. /**
  155. * 上传零时媒体资源
  156. * @param string $filename 媒体资源本地路径
  157. * @param string $type 媒体资源类型,具体请参考微信开发手册
  158. */
  159. public function mediaUpload($filename, $type){
  160. $filename = realpath($filename);
  161. if(!$filename) throw new \Think\Exception('资源路径错误!');
  162. $data = array(
  163. 'type' => $type,
  164. 'media' => "@{$filename}"
  165. );
  166. return $this->api('media/upload', $data, 'POST', '', false);
  167. }
  168. /**
  169. * 上传永久媒体资源
  170. * @param string $filename 媒体资源本地路径
  171. * @param string $type 媒体资源类型,具体请参考微信开发手册
  172. * @param string $description 资源描述,仅资源类型为 video 时有效
  173. */
  174. public function materialAddMaterial($filename, $type, $description = ''){
  175. $filename = realpath($filename);
  176. if(!$filename) throw new \Think\Exception('资源路径错误!');
  177. $data = array(
  178. 'type' => $type,
  179. 'media' => "@{$filename}",
  180. );
  181. if($type == 'video'){
  182. if(is_array($description)){
  183. //保护中文,微信api不支持中文转义的json结构
  184. array_walk_recursive($description, function(&$value){
  185. $value = urlencode($value);
  186. });
  187. $description = urldecode(json_encode($description));
  188. }
  189. $data['description'] = $description;
  190. }
  191. return $this->api('material/add_material', $data, 'POST', '', false);
  192. }
  193. /**
  194. * 获取媒体资源下载地址
  195. * 注意:视频资源不允许下载
  196. * @param string $media_id 媒体资源id
  197. * @return string 媒体资源下载地址
  198. */
  199. public function mediaGet($media_id){
  200. $param = array(
  201. 'access_token' => $this->accessToken,
  202. 'media_id' => $media_id
  203. );
  204. $url = "{$this->apiURL}/media/get?";
  205. return $url . http_build_query($param);
  206. }
  207. /**
  208. * 给指定用户推送信息
  209. * 注意:微信规则只允许给在48小时内给公众平台发送过消息的用户推送信息
  210. * @param string $openid 用户的openid
  211. * @param array $content 发送的数据,不同类型的数据结构可能不同
  212. * @param string $type 推送消息类型
  213. */
  214. public function messageCustomSend($openid, $content, $type = self::MSG_TYPE_TEXT){
  215. //基础数据
  216. $data = array(
  217. 'touser'=>$openid,
  218. 'msgtype'=>$type,
  219. );
  220. //根据类型附加额外数据
  221. $data[$type] = call_user_func(array(self, $type), $content);
  222. return $this->api('message/custom/send', $data);
  223. }
  224. /**
  225. * 发送文本消息
  226. * @param string $openid 用户的openid
  227. * @param string $text 发送的文字
  228. */
  229. public function sendText($openid, $text){
  230. return $this->messageCustomSend($openid, $text, self::MSG_TYPE_TEXT);
  231. }
  232. /**
  233. * 发送图片消息
  234. * @param string $openid 用户的openid
  235. * @param string $media 图片ID
  236. */
  237. public function sendImage($openid, $media){
  238. return $this->messageCustomSend($openid, $media, self::MSG_TYPE_IMAGE);
  239. }
  240. /**
  241. * 发送语音消息
  242. * @param string $openid 用户的openid
  243. * @param string $media 音频ID
  244. */
  245. public function sendVoice($openid, $media){
  246. return $this->messageCustomSend($openid, $media, self::MSG_TYPE_VOICE);
  247. }
  248. /**
  249. * 发送视频消息
  250. * @param string $openid 用户的openid
  251. * @param string $media_id 视频ID
  252. * @param string $title 视频标题
  253. * @param string $discription 视频描述
  254. */
  255. public function sendVideo(){
  256. $video = func_get_args();
  257. $openid = array_shift($video);
  258. return $this->messageCustomSend($openid, $video, self::MSG_TYPE_VIDEO);
  259. }
  260. /**
  261. * 发送音乐消息
  262. * @param string $openid 用户的openid
  263. * @param string $title 音乐标题
  264. * @param string $discription 音乐描述
  265. * @param string $musicurl 音乐链接
  266. * @param string $hqmusicurl 高品质音乐链接
  267. * @param string $thumb_media_id 缩略图ID
  268. */
  269. public function sendMusic(){
  270. $music = func_get_args();
  271. $openid = array_shift($music);
  272. return $this->messageCustomSend($openid, $music, self::MSG_TYPE_MUSIC);
  273. }
  274. /**
  275. * 发送图文消息
  276. * @param string $openid 用户的openid
  277. * @param array $news 图文内容 [标题,描述,URL,缩略图]
  278. * @param array $news1 图文内容 [标题,描述,URL,缩略图]
  279. * @param array $news2 图文内容 [标题,描述,URL,缩略图]
  280. * ... ...
  281. * @param array $news9 图文内容 [标题,描述,URL,缩略图]
  282. */
  283. public function sendNews(){
  284. $news = func_get_args();
  285. $openid = array_shift($news);
  286. return $this->messageCustomSend($openid, $news, self::MSG_TYPE_NEWS);
  287. }
  288. /**
  289. * 发送一条图文消息
  290. * @param string $openid 用户的openid
  291. * @param string $title 文章标题
  292. * @param string $discription 文章简介
  293. * @param string $url 文章连接
  294. * @param string $picurl 文章缩略图
  295. */
  296. public function sendNewsOnce(){
  297. $news = func_get_args();
  298. $openid = array_shift($news);
  299. $news = array($news);
  300. return $this->messageCustomSend($openid, $news, self::MSG_TYPE_NEWS);
  301. }
  302. /**
  303. * 创建用户组
  304. * @param string $name 组名称
  305. */
  306. public function groupsCreate($name){
  307. $data = array('group' => array('name' => $name));
  308. return $this->api('groups/create', $data);
  309. }
  310. /**
  311. * 查询所有分组
  312. * @return array 分组列表
  313. */
  314. public function groupsGet(){
  315. return $this->api('groups/get', '', 'GET');
  316. }
  317. /**
  318. * 查询用户所在的分组
  319. * @param string $openid 用户的OpenID
  320. * @return number 分组ID
  321. */
  322. public function groupsGetid($openid){
  323. $data = array('openid' => $openid);
  324. return $this->api('groups/getid', $data);
  325. }
  326. /**
  327. * 修改分组
  328. * @param number $id 分组ID
  329. * @param string $name 分组名称
  330. * @return array 修改成功或失败信息
  331. */
  332. public function groupsUpdate($id, $name){
  333. $data = array('id' => $id, 'name' => $name);
  334. return $this->api('groups/update', $data);
  335. }
  336. /**
  337. * 移动用户分组
  338. * @param string $openid 用户的OpenID
  339. * @param number $to_groupid 要移动到的分组ID
  340. * @return array 移动成功或失败信息
  341. */
  342. public function groupsMemberUpdate($openid, $to_groupid){
  343. $data = array('openid' => $openid, 'to_groupid' => $to_groupid);
  344. return $this->api('groups/member/update', $data);
  345. }
  346. /**
  347. * 用户设备注名
  348. * @param string $openid 用户的OpenID
  349. * @param string $remark 设备注名
  350. * @return array 执行成功失败信息
  351. */
  352. public function userInfoUpdateremark($openid, $remark){
  353. $data = array('openid' => $openid, 'remark' => $remark);
  354. return $this->api('user/info/updateremark', $data);
  355. }
  356. /**
  357. * 获取指定用户的详细信息
  358. * @param string $openid 用户的openid
  359. * @param string $lang 需要获取数据的语言
  360. */
  361. public function userInfo($openid, $lang = 'zh_CN'){
  362. $param = array('openid' => $openid, 'lang' => $lang);
  363. return $this->api('user/info', '', 'GET', $param);
  364. }
  365. /**
  366. * 获取关注者列表
  367. * @param string $next_openid 下一个openid,在用户数大于10000时有效
  368. * @return array 用户列表
  369. */
  370. public function userGet($next_openid = ''){
  371. $param = array('next_openid' => $next_openid);
  372. return $this->api('user/get', '', 'GET', $param);
  373. }
  374. /**
  375. * 创建自定义菜单
  376. * @param array $button 符合规则的菜单数组,规则参见微信手册
  377. */
  378. public function menuCreate($button){
  379. $data = array('button' => $button);
  380. return $this->api('menu/create', $data);
  381. }
  382. /**
  383. * 获取所有的自定义菜单
  384. * @return array 自定义菜单数组
  385. */
  386. public function menuGet(){
  387. return $this->api('menu/get', '', 'GET');
  388. }
  389. /**
  390. * 删除自定义菜单
  391. */
  392. public function menuDelete(){
  393. return $this->api('menu/delete', '', 'GET');
  394. }
  395. /**
  396. * 创建二维码,可创建指定有效期的二维码和永久二维码
  397. * @param integer $scene_id 二维码参数
  398. * @param integer $expire_seconds 二维码有效期,0-永久有效
  399. */
  400. public function qrcodeCreate($scene_id, $expire_seconds = 0){
  401. $data = array();
  402. if(is_numeric($expire_seconds) && $expire_seconds > 0){
  403. $data['expire_seconds'] = $expire_seconds;
  404. $data['action_name'] = self::QR_SCENE;
  405. } else {
  406. $data['action_name'] = self::QR_LIMIT_SCENE;
  407. }
  408. $data['action_info']['scene']['scene_id'] = $scene_id;
  409. return $this->api('qrcode/create', $data);
  410. }
  411. /**
  412. * 根据ticket获取二维码URL
  413. * @param string $ticket 通过 qrcodeCreate接口获取到的ticket
  414. * @return string 二维码URL
  415. */
  416. public function showqrcode($ticket){
  417. return "{$this->qrcodeURL}/showqrcode?ticket={$ticket}";
  418. }
  419. /**
  420. * 长链接转短链接
  421. * @param string $long_url 长链接
  422. * @return string 短链接
  423. */
  424. public function shorturl($long_url){
  425. $data = array(
  426. 'action' => 'long2short',
  427. 'long_url' => $long_url
  428. );
  429. return $this->api('shorturl', $data);
  430. }
  431. /**
  432. * 调用微信api获取响应数据
  433. * @param string $name API名称
  434. * @param string $data POST请求数据
  435. * @param string $method 请求方式
  436. * @param string $param GET请求参数
  437. * @return array api返回结果
  438. */
  439. protected function api($name, $data = '', $method = 'POST', $param = '', $json = true){
  440. $params = array('access_token' => $this->accessToken);
  441. if(!empty($param) && is_array($param)){
  442. $params = array_merge($params, $param);
  443. }
  444. $url = "{$this->apiURL}/{$name}";
  445. if($json && !empty($data)){
  446. //保护中文,微信api不支持中文转义的json结构
  447. array_walk_recursive($data, function(&$value){
  448. $value = urlencode($value);
  449. });
  450. $data = urldecode(json_encode($data));
  451. }
  452. $data = self::http($url, $params, $data, $method);
  453. return json_decode($data, true);
  454. }
  455. /**
  456. * 发送HTTP请求方法,目前只支持CURL发送请求
  457. * @param string $url 请求URL
  458. * @param array $param GET参数数组
  459. * @param array $data POST的数据,GET请求时该参数无效
  460. * @param string $method 请求方法GET/POST
  461. * @return array 响应数据
  462. */
  463. protected static function http($url, $param, $data = '', $method = 'GET'){
  464. $opts = array(
  465. CURLOPT_TIMEOUT => 30,
  466. CURLOPT_RETURNTRANSFER => 1,
  467. CURLOPT_SSL_VERIFYPEER => false,
  468. CURLOPT_SSL_VERIFYHOST => false,
  469. );
  470. /* 根据请求类型设置特定参数 */
  471. $opts[CURLOPT_URL] = $url . '?' . http_build_query($param);
  472. //file_put_contents('./data.json',$opts[CURLOPT_URL]."\r\n",FILE_APPEND);
  473. if(strtoupper($method) == 'POST'){
  474. $opts[CURLOPT_POST] = 1;
  475. $opts[CURLOPT_POSTFIELDS] = $data;
  476. if(is_string($data)){ //发送JSON数据
  477. $opts[CURLOPT_HTTPHEADER] = array(
  478. 'Content-Type: application/json; charset=utf-8',
  479. 'Content-Length: ' . strlen($data),
  480. );
  481. }
  482. }
  483. /* 初始化并执行curl请求 */
  484. $ch = curl_init();
  485. curl_setopt_array($ch, $opts);
  486. $data = curl_exec($ch);
  487. $error = curl_error($ch);
  488. curl_close($ch);
  489. //发生错误,抛出异常
  490. if($error) throw new \Think\Exception('请求发生错误:' . $error);
  491. return $data;
  492. }
  493. /**
  494. * 构造文本信息
  495. * @param string $content 要回复的文本
  496. */
  497. private static function text($content){
  498. $data['content'] = $content;
  499. return $data;
  500. }
  501. /**
  502. * 构造图片信息
  503. * @param integer $media 图片ID
  504. */
  505. private static function image($media){
  506. $data['media_id'] = $media;
  507. return $data;
  508. }
  509. /**
  510. * 构造音频信息
  511. * @param integer $media 语音ID
  512. */
  513. private static function voice($media){
  514. $data['media_id'] = $media;
  515. return $data;
  516. }
  517. /**
  518. * 构造视频信息
  519. * @param array $video 要回复的视频 [视频ID,标题,说明]
  520. */
  521. private static function video($video){
  522. $data = array();
  523. list(
  524. $data['media_id'],
  525. $data['title'],
  526. $data['description'],
  527. ) = $video;
  528. return $data;
  529. }
  530. /**
  531. * 构造音乐信息
  532. * @param array $music 要回复的音乐[标题,说明,链接,高品质链接,缩略图ID]
  533. */
  534. private static function music($music){
  535. $data = array();
  536. list(
  537. $data['title'],
  538. $data['description'],
  539. $data['musicurl'],
  540. $data['hqmusicurl'],
  541. $data['thumb_media_id'],
  542. ) = $music;
  543. return $data;
  544. }
  545. /**
  546. * 构造图文信息
  547. * @param array $news 要回复的图文内容
  548. * [
  549. * 0 => 第一条图文信息[标题,说明,图片链接,全文连接],
  550. * 1 => 第二条图文信息[标题,说明,图片链接,全文连接],
  551. * 2 => 第三条图文信息[标题,说明,图片链接,全文连接],
  552. * ]
  553. */
  554. private static function news($news){
  555. $articles = array();
  556. foreach ($news as $key => $value) {
  557. list(
  558. $articles[$key]['title'],
  559. $articles[$key]['description'],
  560. $articles[$key]['url'],
  561. $articles[$key]['picurl']
  562. ) = $value;
  563. if($key >= 9) break; //最多只允许10条图文信息
  564. }
  565. $data['articles'] = $articles;
  566. return $data;
  567. }
  568. public function getSignPackage(){
  569. $jsapiTicket = $this->_getJsApiTicket();
  570. /* if (!$jsapiTicket){
  571. return false;
  572. } */
  573. $url = "http://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]";
  574. $timestamp = time();
  575. $nonceStr = $this->_createNonceStr();
  576. // 这里参数的顺序要按照 key 值 ASCII 码升序排序
  577. $string = "jsapi_ticket=$jsapiTicket&noncestr=$nonceStr&timestamp=$timestamp&url=$url";
  578. $signature = sha1($string);
  579. // t($string."\n\n\n\n".$signature);
  580. return $signPackage = array(
  581. "appId" => C('WX_APPID'),
  582. "nonceStr" => $nonceStr,
  583. "timestamp" => $timestamp,
  584. "url" => $url,
  585. "signature" => $signature,
  586. "rawString" => $string
  587. );
  588. }
  589. private function _createNonceStr($length = 16){
  590. $chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
  591. $str = "";
  592. for ($i = 0; $i < $length; $i ++) {
  593. $str .= substr($chars, mt_rand(0, strlen($chars) - 1), 1);
  594. }
  595. return $str;
  596. }
  597. private function _getJsApiTicket(){
  598. $ticket = S('jsSDK_ticket');
  599. if (!$ticket) {
  600. $accessToken = $this->accessToken;
  601. //file_put_contents('aa.txt', $accessToken."\r\n",FILE_APPEND);
  602. if (empty($this->accessToken)){
  603. return false;
  604. }
  605. $url = "https://api.weixin.qq.com/cgi-bin/ticket/getticket";
  606. $param = array('type'=>'jsapi','access_token'=>$accessToken);
  607. $res = json_decode(self::http($url,$param),true);
  608. $ticket = $res['ticket'];
  609. S('jsSDK_ticket',$ticket,$res['expires_in']);
  610. }
  611. return $ticket;
  612. }
  613. }