Frenlee

QQ登录PHP SDK源码阅读

QQ登录的源码阅读,一般API的请求在PHP中都是通过curl请求来完成的。同时一般的API请求可能会对应一系列不同的请求地址和参数,以及必要验证参数。通过阅读QQ登录源码后,以后类似的API可以通过类似的方法来完成。

文件组成

QQ登录PHP SDK文件主要分为三个大部分:

  • 入口文件 qqConnectAPI
  • 配置文件 inc.php config.php
  • 相关类文件

当然,核心主要存在相关的类文件中,其中包括:

  • 异常错误处理类 ErrorCase
  • 配置值记录类 Recorder
  • URL请求类 URL
  • 授权认证基类 OAuth
  • API信息获取类 QC

入口文件主要负责资源的加载和引入到我们需要的调用QQ登录API的地方.

配置文件中主要存放了一些认证信息以及API调用权限信息。

好了先从入口文件开始入手看:

入口文件

qqConnectAPI.php

1
2
3
4
5
6
7
8
9
10
<?php
session_start();
/* PHP SDK
* @version 2.0.0
* @author connect@qq.com
* @copyright © 2013, Tencent Corporation. All rights reserved.
*/

require_once(dirname(__FILE__)."/comm/config.php");
require_once(CLASS_PATH."QC.class.php");

这里主要负责引入配置文件和主类,配置文件就config.php只是定义了两个常量,一个ROOT只想当前API路径。比较有意思的是它使用的是相对路径进行定位的,实现手段是使用的dirname()手段向前递进目录的。

config.php

1
2
define("ROOT",dirname(dirname(__FILE__))."/");
define("CLASS_PATH",ROOT."class/");

在inc.php文件中是以json字符串格式保存的appid,appkey,callback等信息。

错误异常处理类

ErrorCase.class.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<?php
/* PHP SDK
* @version 2.0.0
* @author connect@qq.com
* @copyright © 2013, Tencent Corporation. All rights reserved.
*/

require_once(CLASS_PATH."Recorder.class.php");

/*
* @brief ErrorCase类,封闭异常
* */
class ErrorCase{
private $errorMsg;

public function __construct(){
$this->errorMsg = array(
"20001" => "<h2>配置文件损坏或无法读取,请重新执行intall</h2>",
"30001" => "<h2>The state does not match. You may be a victim of CSRF.</h2>",
"50001" => "<h2>可能是服务器无法请求https协议</h2>可能未开启curl支持,请尝试开启curl支持,重启web服务器,如果问题仍未解决,请联系我们"
);
}

/**
* showError
* 显示错误信息
* @param int $code 错误代码
* @param string $description 描述信息(可选)
*/
public function showError($code, $description = '$'){
$recorder = new Recorder();
if(! $recorder->readInc("errorReport")){
die();//die quietly
}


echo "<meta charset=\"UTF-8\">";
if($description == "$"){
die($this->errorMsg[$code]);
}else{
echo "<h3>error:</h3>$code";
echo "<h3>msg :</h3>$description";
exit();
}
}
public function showTips($code, $description = '$'){
}
}

这个类中主要就是实现了一个函数,showError来显示错误信息,通过Recorder类获取配置信息中是否开启了错误报告,若没有开启的话就直接die(),不进行错误信息显示。

配置值记录类

Recorder.class.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
<?php
/* PHP SDK
* @version 2.0.0
* @author connect@qq.com
* @copyright © 2013, Tencent Corporation. All rights reserved.
*/

require_once(CLASS_PATH."ErrorCase.class.php");
class Recorder{
private static $data;
private $inc;
private $error;

public function __construct(){
$this->error = new ErrorCase();

//-------读取配置文件
$incFileContents = file(ROOT."comm/inc.php");
$incFileContents = $incFileContents[1];
$this->inc = json_decode($incFileContents);
if(empty($this->inc)){
$this->error->showError("20001");
}

if(empty($_SESSION['QC_userData'])){
self::$data = array();
}else{
self::$data = $_SESSION['QC_userData'];
}
}

public function write($name,$value){
self::$data[$name] = $value;
}

public function read($name){
if(empty(self::$data[$name])){
return null;
}else{
return self::$data[$name];
}
}

public function readInc($name){
if(empty($this->inc->$name)){
return null;
}else{
return $this->inc->$name;
}
}

public function delete($name){
unset(self::$data[$name]);
}

function __destruct(){
$_SESSION['QC_userData'] = self::$data;
}
}

这个类主要是用来记录配置信息以及其他需要保存的数据的(比如AccessToken)配置信息从inc.php文件中获取,而其他数据$data是保存在session中的。函数就是读写删除data数据的函数以及读取配置信息的函数,这里使用了析构函数来保存$data信息到session中。这个地方值得学习。

网路请求工具类

URL.class.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
<?php
/* PHP SDK
* @version 2.0.0
* @author connect@qq.com
* @copyright © 2013, Tencent Corporation. All rights reserved.
*/

require_once(CLASS_PATH."ErrorCase.class.php");

/*
* @brief url封装类,将常用的url请求操作封装在一起
* */
class URL{
private $error;

public function __construct(){
$this->error = new ErrorCase();
}

/**
* combineURL
* 拼接url
* @param string $baseURL 基于的url
* @param array $keysArr 参数列表数组
* @return string 返回拼接的url
*/
public function combineURL($baseURL,$keysArr){
$combined = $baseURL."?";
$valueArr = array();

foreach($keysArr as $key => $val){
$valueArr[] = "$key=$val";
}

$keyStr = implode("&",$valueArr);
$combined .= ($keyStr);

return $combined;
}

/**
* get_contents
* 服务器通过get请求获得内容
* @param string $url 请求的url,拼接后的
* @return string 请求返回的内容
*/
public function get_contents($url){
if (ini_get("allow_url_fopen") == "1") {
$response = file_get_contents($url);
}else{
$ch = curl_init();
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
curl_setopt($ch, CURLOPT_URL, $url);
$response = curl_exec($ch);
curl_close($ch);
}

//-------请求为空
if(empty($response)){
$this->error->showError("50001");
}

return $response;
}

/**
* get
* get方式请求资源
* @param string $url 基于的baseUrl
* @param array $keysArr 参数列表数组
* @return string 返回的资源内容
*/
public function get($url, $keysArr){
$combined = $this->combineURL($url, $keysArr);
return $this->get_contents($combined);
}

/**
* post
* post方式请求资源
* @param string $url 基于的baseUrl
* @param array $keysArr 请求的参数列表
* @param int $flag 标志位
* @return string 返回的资源内容
*/
public function post($url, $keysArr, $flag = 0){

$ch = curl_init();
if(! $flag) curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
curl_setopt($ch, CURLOPT_POST, TRUE);
curl_setopt($ch, CURLOPT_POSTFIELDS, $keysArr);
curl_setopt($ch, CURLOPT_URL, $url);
$ret = curl_exec($ch);

curl_close($ch);
return $ret;
}
}

这个类主要是实现服务器端与远程服务器(QQ API服务器)之间的通信请求,简单的实现了get请求和post请求,还有一个就是将基地址和请求参数数组拼合成url的函数了,简单明了。

OAuth认证交互基类

Oauth.class.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
<?php
/* PHP SDK
* @version 2.0.0
* @author connect@qq.com
* @copyright © 2013, Tencent Corporation. All rights reserved.
*/

require_once(CLASS_PATH."Recorder.class.php");
require_once(CLASS_PATH."URL.class.php");
require_once(CLASS_PATH."ErrorCase.class.php");

class Oauth{

const VERSION = "2.0";
const GET_AUTH_CODE_URL = "https://graph.qq.com/oauth2.0/authorize";
const GET_ACCESS_TOKEN_URL = "https://graph.qq.com/oauth2.0/token";
const GET_OPENID_URL = "https://graph.qq.com/oauth2.0/me";

protected $recorder;
public $urlUtils;
protected $error;


function __construct(){
$this->recorder = new Recorder();
$this->urlUtils = new URL();
$this->error = new ErrorCase();
}
//QQ登录
public function qq_login(){
$appid = $this->recorder->readInc("appid");
$callback = $this->recorder->readInc("callback");
$scope = $this->recorder->readInc("scope");

//-------生成唯一随机串防CSRF攻击
$state = md5(uniqid(rand(), TRUE));
$this->recorder->write('state',$state);

//-------构造请求参数列表
$keysArr = array(
"response_type" => "code",
"client_id" => $appid,
"redirect_uri" => $callback,
"state" => $state,
"scope" => $scope
);

$login_url = $this->urlUtils->combineURL(self::GET_AUTH_CODE_URL, $keysArr);

header("Location:$login_url");
}

//登录回调
public function qq_callback(){
$state = $this->recorder->read("state");

//--------验证state防止CSRF攻击
if($_GET['state'] != $state){
$this->error->showError("30001");
}

//-------请求参数列表
$keysArr = array(
"grant_type" => "authorization_code",
"client_id" => $this->recorder->readInc("appid"),
"redirect_uri" => urlencode($this->recorder->readInc("callback")),
"client_secret" => $this->recorder->readInc("appkey"),
"code" => $_GET['code']
);

//------构造请求access_token的url
$token_url = $this->urlUtils->combineURL(self::GET_ACCESS_TOKEN_URL, $keysArr);
$response = $this->urlUtils->get_contents($token_url);

if(strpos($response, "callback") !== false){

$lpos = strpos($response, "(");
$rpos = strrpos($response, ")");
$response = substr($response, $lpos + 1, $rpos - $lpos -1);
$msg = json_decode($response);

if(isset($msg->error)){
$this->error->showError($msg->error, $msg->error_description);
}
}

$params = array();
parse_str($response, $params);

$this->recorder->write("access_token", $params["access_token"]);
return $params["access_token"];

}

public function get_openid(){

//-------请求参数列表
$keysArr = array(
"access_token" => $this->recorder->read("access_token")
);

$graph_url = $this->urlUtils->combineURL(self::GET_OPENID_URL, $keysArr);
$response = $this->urlUtils->get_contents($graph_url);

//--------检测错误是否发生
if(strpos($response, "callback") !== false){

$lpos = strpos($response, "(");
$rpos = strrpos($response, ")");
$response = substr($response, $lpos + 1, $rpos - $lpos -1);
}

$user = json_decode($response);
if(isset($user->error)){
$this->error->showError($user->error, $user->error_description);
}

//------记录openid
$this->recorder->write("openid", $user->openid);
return $user->openid;

}
}

这个类是一个基类,只完成了QQ登录API请求核心的部分,即QQ登录地址跳转,qq回调函数处理,以及获取用户openid的函数。
QQ登录跳转主要就是拼接跳转地址.在这个地址中使用了一个放跨站请求的state,使用的是 md5(uniqid(rand()),TRUE)实现的。存放于session中,在回调的时候用于验证是否是从本服务器发起的请求,从而来防止跨站请求。
qq回调函数主要就是从QQ服务器获取access_token用于之后的请求。
获取用户openid就是通过access_token获取信息。

API信息获取类

QC.class.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
<?php
/* PHP SDK
* @version 2.0.0
* @author connect@qq.com
* @copyright © 2013, Tencent Corporation. All rights reserved.
*/
require_once(CLASS_PATH."Oauth.class.php");

/*
* @brief QC类,api外部对象,调用接口全部依赖于此对象
* */
class QC extends Oauth{
private $kesArr, $APIMap;

/**
* _construct
*
* 构造方法
* @access public
* @since 5
* @param string $access_token access_token value
* @param string $openid openid value
* @return Object QC
*/
public function __construct($access_token = "", $openid = ""){
parent::__construct();

//如果access_token和openid为空,则从session里去取,适用于demo展示情形
if($access_token === "" || $openid === ""){
$this->keysArr = array(
"oauth_consumer_key" => (int)$this->recorder->readInc("appid"),
"access_token" => $this->recorder->read("access_token"),
"openid" => $this->recorder->read("openid")
);
}else{
$this->keysArr = array(
"oauth_consumer_key" => (int)$this->recorder->readInc("appid"),
"access_token" => $access_token,
"openid" => $openid
);
}

//初始化APIMap
/*
* 加#表示非必须,无则不传入url(url中不会出现该参数), "key" => "val" 表示key如果没有定义则使用默认值val
* 规则 array( baseUrl, argListArr, method)
*
*/
$this->APIMap = array(


/* qzone */
"add_blog" => array(
"https://graph.qq.com/blog/add_one_blog",
array("title", "format" => "json", "content" => null),
"POST"
),
"add_topic" => array(
"https://graph.qq.com/shuoshuo/add_topic",
array("richtype","richval","con","#lbs_nm","#lbs_x","#lbs_y","format" => "json", "#third_source"),
"POST"
),
"get_user_info" => array(
"https://graph.qq.com/user/get_user_info",
array("format" => "json"),
"GET"
),
"add_one_blog" => array(
"https://graph.qq.com/blog/add_one_blog",
array("title", "content", "format" => "json"),
"GET"
),
"add_album" => array(
"https://graph.qq.com/photo/add_album",
array("albumname", "#albumdesc", "#priv", "format" => "json"),
"POST"
),
"upload_pic" => array(
"https://graph.qq.com/photo/upload_pic",
array("picture", "#photodesc", "#title", "#albumid", "#mobile", "#x", "#y", "#needfeed", "#successnum", "#picnum", "format" => "json"),
"POST"
),
"list_album" => array(
"https://graph.qq.com/photo/list_album",
array("format" => "json")
),
"add_share" => array(
"https://graph.qq.com/share/add_share",
array("title", "url", "#comment","#summary","#images","format" => "json","#type","#playurl","#nswb","site","fromurl"),
"POST"
),
"check_page_fans" => array(
"https://graph.qq.com/user/check_page_fans",
array("page_id" => "314416946","format" => "json")
),
/* wblog */

"add_t" => array(
"https://graph.qq.com/t/add_t",
array("format" => "json", "content","#clientip","#longitude","#compatibleflag"),
"POST"
),
"add_pic_t" => array(
"https://graph.qq.com/t/add_pic_t",
array("content", "pic", "format" => "json", "#clientip", "#longitude", "#latitude", "#syncflag", "#compatiblefalg"),
"POST"
),
"del_t" => array(
"https://graph.qq.com/t/del_t",
array("id", "format" => "json"),
"POST"
),
"get_repost_list" => array(
"https://graph.qq.com/t/get_repost_list",
array("flag", "rootid", "pageflag", "pagetime", "reqnum", "twitterid", "format" => "json")
),
"get_info" => array(
"https://graph.qq.com/user/get_info",
array("format" => "json")
),
"get_other_info" => array(
"https://graph.qq.com/user/get_other_info",
array("format" => "json", "#name", "fopenid")
),
"get_fanslist" => array(
"https://graph.qq.com/relation/get_fanslist",
array("format" => "json", "reqnum", "startindex", "#mode", "#install", "#sex")
),
"get_idollist" => array(
"https://graph.qq.com/relation/get_idollist",
array("format" => "json", "reqnum", "startindex", "#mode", "#install")
),
"add_idol" => array(
"https://graph.qq.com/relation/add_idol",
array("format" => "json", "#name-1", "#fopenids-1"),
"POST"
),
"del_idol" => array(
"https://graph.qq.com/relation/del_idol",
array("format" => "json", "#name-1", "#fopenid-1"),
"POST"
),
/* pay */

"get_tenpay_addr" => array(
"https://graph.qq.com/cft_info/get_tenpay_addr",
array("ver" => 1,"limit" => 5,"offset" => 0,"format" => "json")
)
);
}

/*
* 请求API
* @param $arr 传入参数数组
* @param $argsList 参数列表
* @param $baseUrl 基础地址
* @param $method 请求方法
*/
private function _applyAPI($arr, $argsList, $baseUrl, $method){
$pre = "#";//可省略的参数的头部
$keysArr = $this->keysArr;//获取到接口配置信息

$optionArgList = array();//一些多项选填参数必选一的情形

foreach($argsList as $key => $val){

$tmpKey = $key;//参数名
$tmpVal = $val;//参数值
//如果没有设置key,则将value作为参数名使用
if(!is_string($key)){
$tmpKey = $val;
//参数是否必须
if(strpos($val,$pre) === 0){//否
$tmpVal = $pre;
$tmpKey = substr($tmpKey,1);
if(preg_match("/-(\d$)/", $tmpKey, $res)){
$tmpKey = str_replace($res[0], "", $tmpKey);
$optionArgList[$res[1]][] = $tmpKey;
}
}else{//是
$tmpVal = null;
}
}

//-----如果没有设置相应的参数
if(!isset($arr[$tmpKey]) || $arr[$tmpKey] === ""){

if($tmpVal == $pre){//则使用默认的值
continue;
}else if($tmpVal){
$arr[$tmpKey] = $tmpVal;
}else{//文件上传处理
if($v = $_FILES[$tmpKey]){

$filename = dirname($v['tmp_name'])."/".$v['name'];
move_uploaded_file($v['tmp_name'], $filename);
$arr[$tmpKey] = "@$filename";

}else{
$this->error->showError("api调用参数错误","未传入参数$tmpKey");
}
}
}
//将值拼接值keyArr中
$keysArr[$tmpKey] = $arr[$tmpKey];
}
//检查选填参数必填一的情形
foreach($optionArgList as $val){
$n = 0;
foreach($val as $v){
if(in_array($v, array_keys($keysArr))){
$n ++;
}
}
//多项必选其一中不存在一个
if(! $n){
$str = implode(",",$val);
$this->error->showError("api调用参数错误",$str."必填一个");
}
}
//发起请求
if($method == "POST"){
if($baseUrl == "https://graph.qq.com/blog/add_one_blog") $response = $this->urlUtils->post($baseUrl, $keysArr, 1);
else $response = $this->urlUtils->post($baseUrl, $keysArr, 0);
}else if($method == "GET"){
$response = $this->urlUtils->get($baseUrl, $keysArr);
}

return $response;

}

/**
* _call
* 魔术方法,做api调用转发
* @param string $name 调用的方法名称
* @param array $arg 参数列表数组
* @since 5.0
* @return array 返加调用结果数组
*/
public function __call($name,$arg){
//如果APIMap不存在相应的api
if(empty($this->APIMap[$name])){
$this->error->showError("api调用名称错误","不存在的API: <span style='color:red;'>$name</span>");
}

//从APIMap获取api相应参数
$baseUrl = $this->APIMap[$name][0];
$argsList = $this->APIMap[$name][1];
$method = isset($this->APIMap[$name][2]) ? $this->APIMap[$name][2] : "GET";

if(empty($arg)){
$arg[0] = null;
}

//对于get_tenpay_addr,特殊处理,php json_decode对\xA312此类字符支持不好
if($name != "get_tenpay_addr"){
$response = json_decode($this->_applyAPI($arg[0], $argsList, $baseUrl, $method));
$responseArr = $this->objToArr($response);
}else{
$responseArr = $this->simple_json_parser($this->_applyAPI($arg[0], $argsList, $baseUrl, $method));
}


//检查返回ret判断api是否成功调用
if($responseArr['ret'] == 0){
return $responseArr;
}else{
$this->error->showError($response->ret, $response->msg);
}

}

//php 对象到数组转换
private function objToArr($obj){
if(!is_object($obj) && !is_array($obj)) {
return $obj;
}
$arr = array();
foreach($obj as $k => $v){
$arr[$k] = $this->objToArr($v);
}
return $arr;
}


/**
* get_access_token
* 获得access_token
* @param void
* @since 5.0
* @return string 返加access_token
*/
public function get_access_token(){
return $this->recorder->read("access_token");
}

//简单实现json到php数组转换功能
private function simple_json_parser($json){
$json = str_replace("{","",str_replace("}","", $json));
$jsonValue = explode(",", $json);
$arr = array();
foreach($jsonValue as $v){
$jValue = explode(":", $v);
$arr[str_replace('"',"", $jValue[0])] = (str_replace('"', "", $jValue[1]));
}
return $arr;
}
}

最喜欢的就是这个类了,喜欢它这种简洁明了的思想。将请求基地址,请求参数以及请求方式定义为数组,然后通过 __call 魔术方法来调用器API函数,返回值。最核心的部分应该就是 _applyAPI 这个函数了,它的主要任务就是处理请求参数的关系,参数分为 必须参数 , 非必须参数,多项必选其一参数等这些参数的处理。还有就是参数默认值,这些情况都有考虑到处理。 使用 #区分了非必须参数 ,在非必须参数后加 【-数字】处理了多项参数必选其一的参数。然后就是文件的处理使用了 @filename 来处理的。将处理好的参数统一放到keyArr数组中,最后通过url工具进行处理。

使用部分

接下来简单看下QQ登录的使用吧,在示例中,认证部分(example/oauth/):

index.php

1
2
3
4
5
<?php

require_once("../../API/qqConnectAPI.php");
$qc = new QC();
$qc->qq_login();

简单的调用,包含乳沟文件,实例化QC对象,调用QQ登录。

回调文件:

callback.php

1
2
3
4
5
<?php
require_once("../../API/qqConnectAPI.php");
$qc = new QC();
echo $qc->qq_callback();
echo $qc->get_openid();

和登录一样,只是在回调后获取了用户openid信息。

QQ登录的这种代码设计思想可以用在其他很多API请求方面,一般的都可以满足,而且需要修改的地方不会太多,然后可以加入自己的相关存储操作类。