├── .gitignore ├── composer.json ├── ErrorCase.php ├── Recorder.php ├── README.md ├── URL.php ├── Oauth.php └── QC.php /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | .idea 3 | /vendor/ 4 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "loveteemo/qqconnect", 3 | "description": "thinkphp 5 & qqconnect", 4 | "authors": [ 5 | { 6 | "name": "long", 7 | "email": "admin@loveteemo.com" 8 | } 9 | ], 10 | "require": { 11 | "php": ">=5.4.0" 12 | }, 13 | "autoload": { 14 | "psr-4": { 15 | "loveteemo\\qqconnect\\": "/" 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /ErrorCase.php: -------------------------------------------------------------------------------- 1 | 10 | // +---------------------------------------------------------------------- 11 | namespace loveteemo\qqconnect; 12 | 13 | class ErrorCase{ 14 | 15 | //定义错误 16 | private $errorMsg; 17 | 18 | //错误代码 19 | public function __construct(){ 20 | $this->errorMsg = array( 21 | "20001" => "

配置文件无法读取,独立配置文件请修改Recorder第22行 [配置文件名.qqconnect]

", 22 | "30001" => "

域名不匹配,这个请求可能是CSRF攻击.

", 23 | "50001" => "

可能是服务器无法请求https协议

可能未开启curl支持,请尝试开启curl支持,重启web服务器,如果问题仍未解决,请联系我们" 24 | ); 25 | } 26 | 27 | //显示错误 28 | public function showError($code, $description = '$'){ 29 | echo ""; 30 | if(isset($this->errorMsg[$code])){ 31 | echo $this->errorMsg[$code]; 32 | }else{ 33 | echo "

错误代码:{$code}手册点我

"; 34 | } 35 | die; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Recorder.php: -------------------------------------------------------------------------------- 1 | 10 | // +---------------------------------------------------------------------- 11 | namespace loveteemo\qqconnect; 12 | 13 | class Recorder{ 14 | private static $data; 15 | private $inc; 16 | private $error; 17 | 18 | public function __construct(){ 19 | $this->error = new ErrorCase(); 20 | 21 | $this->inc = config('auth.qqconnect'); 22 | if(empty($this->inc)){ 23 | $this->error->showError("20001"); 24 | } 25 | 26 | if(empty(session('QC_userData'))){ 27 | self::$data = array(); 28 | }else{ 29 | self::$data = session('QC_userData'); 30 | } 31 | } 32 | 33 | public function write($name,$value){ 34 | self::$data[$name] = $value; 35 | } 36 | 37 | public function read($name){ 38 | if(empty(self::$data[$name])){ 39 | return null; 40 | }else{ 41 | return self::$data[$name]; 42 | } 43 | } 44 | 45 | public function readInc($name){ 46 | if(empty($this->inc[$name])){ 47 | return null; 48 | }else{ 49 | return $this->inc[$name]; 50 | } 51 | } 52 | 53 | public function delete($name){ 54 | unset(self::$data[$name]); 55 | } 56 | 57 | function __destruct(){ 58 | session('QC_userData', self::$data); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ThinkPHP 5 & Oauth2.0 QQ登录 2 | 3 | 类库是基于Oauth2.0结合ThinkPHP 5 修改部分内容,仅适配 ThinkPhP 5 4 | 5 | 个人博客主页: http://www.loveteemo.com 6 | 7 | ## 安装方法 8 | 9 | composer安装: 10 | 11 | ``` bash 12 | composer require loveteemo/qqconnect 13 | ``` 14 | 15 | 添加公共配置: 16 | ``` php 17 | // QQ 互联配置 在config.php中添加 18 | 'qqconnect' => [ 19 | 'appid' => '', 20 | 'appkey' => '', 21 | 'callback' => '', 22 | 'scope' => 'get_user_info', 23 | 'errorReport' => true 24 | ] 25 | ``` 26 | 27 | ## 示例 28 | 29 | ### 页面编写: 30 | 31 | ``` 32 | QQ登录 33 | ``` 34 | 35 | ### 控制器编写: 36 | 37 | 登录 38 | ``` php 39 | namespace app\index\controller; 40 | use loveteemo\qqconnect\QC; 41 | class Base 42 | { 43 | public function qqlogin() 44 | { 45 | $qc = new QC(); 46 | return redirect($qc->qq_login()); 47 | } 48 | } 49 | ``` 50 | 51 | 回调 52 | ``` php 53 | namespace app\index\controller; 54 | use loveteemo\qqconnect\QC; 55 | class Base 56 | { 57 | public function callback() 58 | { 59 | $Qc = new QC(); 60 | $access_token = $Qc->qq_callback(); 61 | $openid = $Qc->get_openid(); 62 | $Qc = new QC($access_token,$openid); 63 | $qq_user_info = $Qc->get_user_info(); 64 | //打印数据 65 | //dump($qq_user_info);die; 66 | // ... 67 | // 用户逻辑 68 | return redirect(url('Index/index')); 69 | } 70 | } 71 | ``` 72 | 73 | 获取到的QQ数据 74 | ``` html 75 | 76 | array(18) { 77 | ["ret"] => int(0) 78 | ["msg"] => string(0) "" 79 | ["is_lost"] => int(0) 80 | ["nickname"] => string(21) "那年,烟雨重楼" 81 | ["gender"] => string(3) "男" 82 | ["province"] => string(6) "广东" 83 | ["city"] => string(6) "深圳" 84 | ["year"] => string(4) "1993" 85 | ["figureurl"] => string(73) "http://qzapp.qlogo.cn/qzapp/101232670/7C8F797F30B08554A6E39A537F9A324B/30" 86 | ["figureurl_1"] => string(73) "http://qzapp.qlogo.cn/qzapp/101232670/7C8F797F30B08554A6E39A537F9A324B/50" 87 | ["figureurl_2"] => string(74) "http://qzapp.qlogo.cn/qzapp/101232670/7C8F797F30B08554A6E39A537F9A324B/100" 88 | ["figureurl_qq_1"] => string(69) "http://q.qlogo.cn/qqapp/101232670/7C8F797F30B08554A6E39A537F9A324B/40" 89 | ["figureurl_qq_2"] => string(70) "http://q.qlogo.cn/qqapp/101232670/7C8F797F30B08554A6E39A537F9A324B/100" 90 | ["is_yellow_vip"] => string(1) "0" 91 | ["vip"] => string(1) "0" 92 | ["yellow_vip_level"] => string(1) "0" 93 | ["level"] => string(1) "0" 94 | ["is_yellow_year_vip"] => string(1) "0" 95 | } 96 | 97 | ``` 98 | 99 | 100 | -------------------------------------------------------------------------------- /URL.php: -------------------------------------------------------------------------------- 1 | 10 | // +---------------------------------------------------------------------- 11 | namespace loveteemo\qqconnect; 12 | 13 | class URL{ 14 | private $error; 15 | 16 | public function __construct(){ 17 | $this->error = new ErrorCase(); 18 | } 19 | 20 | /** 21 | * combineURL 22 | * 拼接url 23 | * @param string $baseURL 基于的url 24 | * @param array $keysArr 参数列表数组 25 | * @return string 返回拼接的url 26 | */ 27 | public function combineURL($baseURL,$keysArr){ 28 | $combined = $baseURL."?"; 29 | $valueArr = array(); 30 | 31 | foreach($keysArr as $key => $val){ 32 | $valueArr[] = "$key=$val"; 33 | } 34 | 35 | $keyStr = implode("&",$valueArr); 36 | $combined .= ($keyStr); 37 | 38 | return $combined; 39 | } 40 | 41 | /** 42 | * get_contents 43 | * 打开Https需要开启 php.ini 的ssl extension=php_openssl.dll 44 | * 服务器通过get请求获得内容 45 | * @param string $url 请求的url,拼接后的 46 | * @return string 请求返回的内容 47 | */ 48 | public function get_contents($url){ 49 | if (ini_get("allow_url_fopen") == "1") { 50 | // 如果这里报错,请找到对应目录新建证书文件 http://curl.haxx.se/docs/caextract.html 51 | $response = file_get_contents($url); 52 | }else{ 53 | $ch = curl_init(); 54 | curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE); 55 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE); 56 | curl_setopt($ch, CURLOPT_URL, $url); 57 | $response = curl_exec($ch); 58 | curl_close($ch); 59 | } 60 | 61 | //-------请求为空 62 | if(empty($response)){ 63 | $this->error->showError("50001"); 64 | } 65 | 66 | return $response; 67 | } 68 | 69 | /** 70 | * get 71 | * get方式请求资源 72 | * @param string $url 基于的baseUrl 73 | * @param array $keysArr 参数列表数组 74 | * @return string 返回的资源内容 75 | */ 76 | public function get($url, $keysArr){ 77 | $combined = $this->combineURL($url, $keysArr); 78 | return $this->get_contents($combined); 79 | } 80 | 81 | /** 82 | * post 83 | * post方式请求资源 84 | * @param string $url 基于的baseUrl 85 | * @param array $keysArr 请求的参数列表 86 | * @param int $flag 标志位 87 | * @return string 返回的资源内容 88 | */ 89 | public function post($url, $keysArr, $flag = 0){ 90 | 91 | $ch = curl_init(); 92 | if(! $flag) curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE); 93 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE); 94 | curl_setopt($ch, CURLOPT_POST, TRUE); 95 | curl_setopt($ch, CURLOPT_POSTFIELDS, $keysArr); 96 | curl_setopt($ch, CURLOPT_URL, $url); 97 | $ret = curl_exec($ch); 98 | 99 | curl_close($ch); 100 | return $ret; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /Oauth.php: -------------------------------------------------------------------------------- 1 | 10 | // +---------------------------------------------------------------------- 11 | namespace loveteemo\qqconnect; 12 | 13 | class Oauth{ 14 | 15 | //QQ互联版本 16 | const VERSION = "2.0"; 17 | //授权地址 18 | const GET_AUTH_CODE_URL = "https://graph.qq.com/oauth2.0/authorize"; 19 | //token地址 20 | const GET_ACCESS_TOKEN_URL = "https://graph.qq.com/oauth2.0/token"; 21 | //openid地址 22 | const GET_OPENID_URL = "https://graph.qq.com/oauth2.0/me"; 23 | 24 | protected $recorder; 25 | 26 | public $urlUtils; 27 | 28 | protected $error; 29 | 30 | 31 | function __construct(){ 32 | $this->recorder = new Recorder(); 33 | $this->urlUtils = new URL(); 34 | $this->error = new ErrorCase(); 35 | } 36 | 37 | //登陆 38 | public function qq_login($callbakc_url = ''){ 39 | $appid = $this->recorder->readInc("appid"); 40 | $callback = $callbakc_url ? : $this->recorder->readInc("callback"); 41 | $scope = $this->recorder->readInc("scope"); 42 | 43 | $state = md5(uniqid(rand(), TRUE)); 44 | $this->recorder->write('state',$state); 45 | 46 | $keysArr = array( 47 | "response_type" => "code", 48 | "client_id" => $appid, 49 | "redirect_uri" => $callback, 50 | "state" => $state, 51 | "scope" => $scope 52 | ); 53 | 54 | return $this->urlUtils->combineURL(self::GET_AUTH_CODE_URL, $keysArr); 55 | } 56 | 57 | //回调 58 | public function qq_callback(){ 59 | $state = $this->recorder->read("state"); 60 | 61 | if($_GET['state'] != $state){ 62 | $this->error->showError("30001"); 63 | } 64 | 65 | $keysArr = array( 66 | "grant_type" => "authorization_code", 67 | "client_id" => $this->recorder->readInc("appid"), 68 | "redirect_uri" => urlencode($this->recorder->readInc("callback")), 69 | "client_secret" => $this->recorder->readInc("appkey"), 70 | "code" => $_GET['code'] 71 | ); 72 | 73 | //------构造请求access_token的url 74 | $token_url = $this->urlUtils->combineURL(self::GET_ACCESS_TOKEN_URL, $keysArr); 75 | $response = $this->urlUtils->get_contents($token_url); 76 | 77 | if(strpos($response, "callback") !== false){ 78 | 79 | $lpos = strpos($response, "("); 80 | $rpos = strrpos($response, ")"); 81 | $response = substr($response, $lpos + 1, $rpos - $lpos -1); 82 | $msg = json_decode($response); 83 | 84 | if(isset($msg->error)){ 85 | $this->error->showError($msg->error, $msg->error_description); 86 | } 87 | } 88 | 89 | $params = array(); 90 | parse_str($response, $params); 91 | 92 | $this->recorder->write("access_token", $params["access_token"]); 93 | return $params["access_token"]; 94 | 95 | } 96 | 97 | //获取OPENID 98 | public function get_openid(){ 99 | 100 | //-------请求参数列表 101 | $keysArr = array( 102 | "access_token" => $this->recorder->read("access_token") 103 | ); 104 | 105 | $graph_url = $this->urlUtils->combineURL(self::GET_OPENID_URL, $keysArr); 106 | $response = $this->urlUtils->get_contents($graph_url); 107 | 108 | //--------检测错误是否发生 109 | if(strpos($response, "callback") !== false){ 110 | 111 | $lpos = strpos($response, "("); 112 | $rpos = strrpos($response, ")"); 113 | $response = substr($response, $lpos + 1, $rpos - $lpos -1); 114 | } 115 | 116 | $user = json_decode($response); 117 | if(isset($user->error)){ 118 | $this->error->showError($user->error, $user->error_description); 119 | } 120 | 121 | //------记录openid 122 | $this->recorder->write("openid", $user->openid); 123 | return $user->openid; 124 | 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /QC.php: -------------------------------------------------------------------------------- 1 | 10 | // +---------------------------------------------------------------------- 11 | namespace loveteemo\qqconnect; 12 | 13 | class QC extends Oauth{ 14 | private $kesArr; 15 | 16 | private $APIMap; 17 | 18 | public function __construct($access_token = "", $openid = ""){ 19 | parent::__construct(); 20 | 21 | //如果access_token和openid为空,则从session里去取 22 | if($access_token === "" || $openid === ""){ 23 | $this->keysArr = array( 24 | "oauth_consumer_key" => (int)$this->recorder->readInc("appid"), 25 | "access_token" => $this->recorder->read("access_token"), 26 | "openid" => $this->recorder->read("openid") 27 | ); 28 | }else{ 29 | $this->keysArr = array( 30 | "oauth_consumer_key" => (int)$this->recorder->readInc("appid"), 31 | "access_token" => $access_token, 32 | "openid" => $openid 33 | ); 34 | } 35 | 36 | //初始化APIMap add_share和add_one_blog接口已于2014.3.31正式下线 37 | $this->APIMap = array( 38 | 39 | "add_blog" => array( 40 | "https://graph.qq.com/blog/add_one_blog", 41 | array("title", "format" => "json", "content" => null), 42 | "POST" 43 | ), 44 | "add_topic" => array( 45 | "https://graph.qq.com/shuoshuo/add_topic", 46 | array("richtype","richval","con","#lbs_nm","#lbs_x","#lbs_y","format" => "json", "#third_source"), 47 | "POST" 48 | ), 49 | "get_user_info" => array( 50 | "https://graph.qq.com/user/get_user_info", 51 | array("format" => "json"), 52 | "GET" 53 | ), 54 | "add_album" => array( 55 | "https://graph.qq.com/photo/add_album", 56 | array("albumname", "#albumdesc", "#priv", "format" => "json"), 57 | "POST" 58 | ), 59 | "upload_pic" => array( 60 | "https://graph.qq.com/photo/upload_pic", 61 | array("picture", "#photodesc", "#title", "#albumid", "#mobile", "#x", "#y", "#needfeed", "#successnum", "#picnum", "format" => "json"), 62 | "POST" 63 | ), 64 | "list_album" => array( 65 | "https://graph.qq.com/photo/list_album", 66 | array("format" => "json") 67 | ), 68 | "check_page_fans" => array( 69 | "https://graph.qq.com/user/check_page_fans", 70 | array("page_id" => "314416946","format" => "json") 71 | ), 72 | 73 | "add_t" => array( 74 | "https://graph.qq.com/t/add_t", 75 | array("format" => "json", "content","#clientip","#longitude","#compatibleflag"), 76 | "POST" 77 | ), 78 | "add_pic_t" => array( 79 | "https://graph.qq.com/t/add_pic_t", 80 | array("content", "pic", "format" => "json", "#clientip", "#longitude", "#latitude", "#syncflag", "#compatiblefalg"), 81 | "POST" 82 | ), 83 | "del_t" => array( 84 | "https://graph.qq.com/t/del_t", 85 | array("id", "format" => "json"), 86 | "POST" 87 | ), 88 | "get_repost_list" => array( 89 | "https://graph.qq.com/t/get_repost_list", 90 | array("flag", "rootid", "pageflag", "pagetime", "reqnum", "twitterid", "format" => "json") 91 | ), 92 | "get_info" => array( 93 | "https://graph.qq.com/user/get_info", 94 | array("format" => "json") 95 | ), 96 | "get_other_info" => array( 97 | "https://graph.qq.com/user/get_other_info", 98 | array("format" => "json", "#name", "fopenid") 99 | ), 100 | "get_fanslist" => array( 101 | "https://graph.qq.com/relation/get_fanslist", 102 | array("format" => "json", "reqnum", "startindex", "#mode", "#install", "#sex") 103 | ), 104 | "get_idollist" => array( 105 | "https://graph.qq.com/relation/get_idollist", 106 | array("format" => "json", "reqnum", "startindex", "#mode", "#install") 107 | ), 108 | "add_idol" => array( 109 | "https://graph.qq.com/relation/add_idol", 110 | array("format" => "json", "#name-1", "#fopenids-1"), 111 | "POST" 112 | ), 113 | "del_idol" => array( 114 | "https://graph.qq.com/relation/del_idol", 115 | array("format" => "json", "#name-1", "#fopenid-1"), 116 | "POST" 117 | ), 118 | 119 | "get_tenpay_addr" => array( 120 | "https://graph.qq.com/cft_info/get_tenpay_addr", 121 | array("ver" => 1,"limit" => 5,"offset" => 0,"format" => "json") 122 | ) 123 | ); 124 | } 125 | 126 | //调用相应api 127 | private function _applyAPI($arr, $argsList, $baseUrl, $method){ 128 | $pre = "#"; 129 | $keysArr = $this->keysArr; 130 | 131 | $optionArgList = array();//一些多项选填参数必选一的情形 132 | foreach($argsList as $key => $val){ 133 | $tmpKey = $key; 134 | $tmpVal = $val; 135 | 136 | if(!is_string($key)){ 137 | $tmpKey = $val; 138 | 139 | if(strpos($val,$pre) === 0){ 140 | $tmpVal = $pre; 141 | $tmpKey = substr($tmpKey,1); 142 | if(preg_match("/-(\d$)/", $tmpKey, $res)){ 143 | $tmpKey = str_replace($res[0], "", $tmpKey); 144 | $optionArgList[$res[1]][] = $tmpKey; 145 | } 146 | }else{ 147 | $tmpVal = null; 148 | } 149 | } 150 | 151 | //-----如果没有设置相应的参数 152 | if(!isset($arr[$tmpKey]) || $arr[$tmpKey] === ""){ 153 | 154 | if($tmpVal == $pre){//则使用默认的值 155 | continue; 156 | }else if($tmpVal){ 157 | $arr[$tmpKey] = $tmpVal; 158 | }else{ 159 | if($v = $_FILES[$tmpKey]){ 160 | 161 | $filename = dirname($v['tmp_name'])."/".$v['name']; 162 | move_uploaded_file($v['tmp_name'], $filename); 163 | $arr[$tmpKey] = "@$filename"; 164 | 165 | }else{ 166 | $this->error->showError("api调用参数错误","未传入参数$tmpKey"); 167 | } 168 | } 169 | } 170 | 171 | $keysArr[$tmpKey] = $arr[$tmpKey]; 172 | } 173 | //检查选填参数必填一的情形 174 | foreach($optionArgList as $val){ 175 | $n = 0; 176 | foreach($val as $v){ 177 | if(in_array($v, array_keys($keysArr))){ 178 | $n ++; 179 | } 180 | } 181 | 182 | if(! $n){ 183 | $str = implode(",",$val); 184 | $this->error->showError("api调用参数错误",$str."必填一个"); 185 | } 186 | } 187 | 188 | if($method == "POST"){ 189 | if($baseUrl == "https://graph.qq.com/blog/add_one_blog") $response = $this->urlUtils->post($baseUrl, $keysArr, 1); 190 | else $response = $this->urlUtils->post($baseUrl, $keysArr, 0); 191 | }else if($method == "GET"){ 192 | $response = $this->urlUtils->get($baseUrl, $keysArr); 193 | } 194 | 195 | return $response; 196 | 197 | } 198 | 199 | /** 200 | * _call 201 | * 魔术方法,做api调用转发 202 | * @param string $name 调用的方法名称 203 | * @param array $arg 参数列表数组 204 | * @since 5.0 205 | * @return array 返加调用结果数组 206 | */ 207 | public function __call($name,$arg){ 208 | //如果APIMap不存在相应的api 209 | if(empty($this->APIMap[$name])){ 210 | $this->error->showError("api调用名称错误","不存在的API: $name"); 211 | } 212 | 213 | //从APIMap获取api相应参数 214 | $baseUrl = $this->APIMap[$name][0]; 215 | $argsList = $this->APIMap[$name][1]; 216 | $method = isset($this->APIMap[$name][2]) ? $this->APIMap[$name][2] : "GET"; 217 | 218 | if(empty($arg)){ 219 | $arg[0] = null; 220 | } 221 | 222 | //对于get_tenpay_addr,特殊处理,php json_decode对\xA312此类字符支持不好 223 | if($name != "get_tenpay_addr"){ 224 | $response = json_decode($this->_applyAPI($arg[0], $argsList, $baseUrl, $method)); 225 | $responseArr = $this->objToArr($response); 226 | }else{ 227 | $responseArr = $this->simple_json_parser($this->_applyAPI($arg[0], $argsList, $baseUrl, $method)); 228 | } 229 | 230 | 231 | //检查返回ret判断api是否成功调用 232 | if($responseArr['ret'] == 0){ 233 | return $responseArr; 234 | }else{ 235 | $this->error->showError($response->ret, $response->msg); 236 | } 237 | 238 | } 239 | 240 | //php 对象到数组转换 241 | private function objToArr($obj){ 242 | if(!is_object($obj) && !is_array($obj)) { 243 | return $obj; 244 | } 245 | $arr = array(); 246 | foreach($obj as $k => $v){ 247 | $arr[$k] = $this->objToArr($v); 248 | } 249 | return $arr; 250 | } 251 | 252 | 253 | /** 254 | * get_access_token 255 | * 获得access_token 256 | * @param void 257 | * @since 5.0 258 | * @return string 返加access_token 259 | */ 260 | public function get_access_token(){ 261 | return $this->recorder->read("access_token"); 262 | } 263 | 264 | //简单实现json到php数组转换功能 265 | private function simple_json_parser($json){ 266 | $json = str_replace("{","",str_replace("}","", $json)); 267 | $jsonValue = explode(",", $json); 268 | $arr = array(); 269 | foreach($jsonValue as $v){ 270 | $jValue = explode(":", $v); 271 | $arr[str_replace('"',"", $jValue[0])] = (str_replace('"', "", $jValue[1])); 272 | } 273 | return $arr; 274 | } 275 | } 276 | --------------------------------------------------------------------------------