本意是在注销账号前保留之前的一些数据。决定用python 爬取收藏。可是未登录无法爬取。想要登录有两种办法,伪造浏览器登录。第二就是注册新浪开发者账号,通过Oauth认证调用其API。
Oauth 的原理搞了一天才明白。很多网站都提供多语言的Oauth。而 1.0 和 2.0 的最大差别就是多了一个 callback 回调页面。关于这方面的说明很少,搞得我一头雾水折腾了好久。总算明白了。
Oauth的原理
现在多用 oauth2.0. 具体可以看其官方文档。用下面的图简单说明:
Consumer指应用程序,User是用户,Service是服务器。大致过程为,User使用程序Consumer。C 向 Service请求一个未授权的令牌(Request_token),这个时候 S 验证 C的合法性(通过开发者的 APP_KEY 和 APP_SECREST)。然后重定向到一个授权页面(通常由服务方提供)。此时用户进行授权,即输入用户名和密码,用户会自动向服务器发送 authorize request_token。得到授权的 request_token。再然后,授权通过后,跳转到一个 callback 页面。通过request_token用来向服务器请求,换取 acess_token。至此,认证结束。接下来要请求数据,只需要根据文档 把 acess_token 当参数传给服务器。
这样说比较抽象,举个简单的例子:
有一个人 U 想去银行 S 取钱。U 没事情去柜台,就委托朋友 C 去柜台。当C 去了银行S,给S说:我要帮 U 取钱。S 先验证 C 的身份,是合法公民。然后 S 打电话给 U。说:C 要帮你取钱,你确定么,确定的话就输入用户名和密码。 U 确定了。此时,S 就给了一个 钥匙给 C。说:我们不提供自动服务,给你钥匙,自己去库房取。然后 C 拿着钥匙,就去取钱了。取完之后给力 U。U 很感激。
通过上面的解释,应该可以知道整个过程中有三个URL请求地址,如下图:
这写地址由服务方提供,上面那个就是 qq 公共平台的url。新浪有新浪自己的。关于第一步,请求 request_token的时候,服务器也要认证开发者帐号,也就是银行认证 C 的合法身份。
每个 开发者都有app_key 和 app_secret. app_key称为密钥, app_secret为密匙。开发者和服务都知道。然后 ,开发者通过 app_key 和 app_secret 通过 一种加密(sha1)得到一个字符串secretcode 公钥。再把 app_key 和 secretcode 发送给服务器。服务器接收之后,将发送到 app_key和服务器存储的 app_secret 通过同样的加密手段得到一个 secretcode2 和发送过来的secretcode进行对比,如果一样,则可以证明是通过。这一点好处是安全。不怕被抓包。
新浪 Oauth的运用
简单知道原理,就可以进行认证。
准备工作
需要向新浪申请开发者帐号,然后创建一个应用。之后会得到一个 app_key 和 app_secret。需要注意是一定要填写下面的回调地址:
新浪的是可以设置 本地的,例如 http://127.0.0.1/oauth/callback.php
豆瓣的需要公网可以访问的回调地址,不同api不一样。
布署
可以下载 官方提供的 SDK ,根据语言选择。这里选择 php。理由是 php的web环境可以一键安装。需要注意到是,新浪的 SDK需要 php 开启 curl。不然会报错。
文档结构如下:
config.php 开发者配置文件
index.php 登录主入口
callback.php 回调处理
saetv2.ex.class.php SDK 主文件,提供认证和api调用
weibolist.php 认证成功之后的数据请求和展示
引用官方的 SDK ,根据demo使用就行。这里重写一下 SDK 的请求过程。
index.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
<?php
session_start();
include_once
(
'config.php'
);
include_once
(
'saetv2.ex.class.php'
);
$o
=
new
SaeTOAuthV2( WB_AKEY , WB_SKEY );
//获取请求 request_token 的 url
$code_url
=
$o
->getAuthorizeURL( WB_CALLBACK_URL );
?>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv=
"Content-Type"
content=
"text/html; charset=utf-8"
/>
<title>新浪微博登录</title>
</head>
<body>
<!-- 授权按钮 -->
<p><a href=
"<?php echo $code_url?>"
><img src=
"weibo_login.png"
title=
"点击进入授权页面"
alt=
"点击进入授权页面"
border=
"0"
/></a></p>
</body>
</html>
|
主要做的事情和I获取 请求 request_token 的url。然后跳转到官方提供的授权页面:
授权之后会重定向到回调 callback.pho.
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
|
<?php
session_start();
include_once
(
'config.php'
);
include_once
(
'saetv2.ex.class.php'
);
$o
=
new
SaeTOAuthV2( WB_AKEY , WB_SKEY );
if
(isset(
$_REQUEST
[
'code'
])) {
$keys
=
array
();
$keys
[
'code'
] =
$_REQUEST
[
'code'
];
$keys
[
'redirect_uri'
] = WB_CALLBACK_URL;
try
{
$token
=
$o
->getAccessToken(
'code'
,
$keys
) ;
}
catch
(OAuthException
$e
) {
}
}
if
(
$token
) {
$_SESSION
[
'token'
] =
$token
;
setcookie(
'weibojs_'
.
$o
->client_id, http_build_query(
$token
) );
?>
授权完成,<a href=
"weibolist.php"
>进入你的微博列表页面</a><br />
<?php
}
else
{
?>
授权失败。
<?php
}
?>
|
这一步主要是根据授权之后返回的 code 进行调用,请求 acess_token。
前面两部的方法 ,都在
saetv2.ex.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
|
<?php
/**
* @ignore
*/
class
OAuthException
extends
Exception {
// pass
}
/**
* 新浪微博 OAuth 认证类(OAuth2)
*
* 授权机制说明请大家参考微博开放平台文档:{@link http://open.weibo.com/wiki/Oauth2}
*
* @package sae
* @author Elmer Zhang
* @version 1.0
*/
class
SaeTOAuthV2 {
public
$client_id
;
public
$client_secret
;
public
$access_token
;
public
$http_code
;
public
$url
;
public
$timeout
= 30;
public
$connecttimeout
= 30;
public
$ssl_verifypeer
= FALSE;
public
$format
=
'json'
;
public
$decode_json
= TRUE;
public
$http_info
;
public
$useragent
=
'Sae T OAuth2 v0.1'
;
public
$debug
= FALSE;
public
static
$boundary
=
''
;
/**
* Set API URLS
*/
/**
* @ignore
*/
/**
* @ignore
*/
/**
* construct WeiboOAuth object
*/
function
__construct(
$client_id
,
$client_secret
,
$access_token
= NULL) {
$this
->client_id =
$client_id
;
$this
->client_secret =
$client_secret
;
$this
->access_token =
$access_token
;
}
/**
* authorize接口
*
* 对应API:{@link http://open.weibo.com/wiki/Oauth2/authorize Oauth2/authorize}
*
* @param string $url 授权后的回调地址,站外应用需与回调地址一致,站内应用需要填写canvas page的地址
* @param string $response_type 支持的值包括 code 和token 默认值为code
* @param string $state 用于保持请求和回调的状态。在回调时,会在Query Parameter中回传该参数
* @param string $display 授权页面类型 可选范围:
* - default 默认授权页面
* - mobile 支持html5的手机
* - popup 弹窗授权页
* - wap1.2 wap1.2页面
* - wap2.0 wap2.0页面
* - js js-sdk 专用 授权页面是弹窗,返回结果为js-sdk回掉函数
* - apponweibo 站内应用专用,站内应用不传display参数,并且response_type为token时,默认使用改display.授权后不会返回access_token,只是输出js刷新站内应用父框架
* @return array
*/
function
getAuthorizeURL(
$url
,
$response_type
=
'code'
,
$state
= NULL,
$display
= NULL ) {
$params
=
array
();
$params
[
'client_id'
] =
$this
->client_id;
$params
[
'redirect_uri'
] =
$url
;
$params
[
'response_type'
] =
$response_type
;
$params
[
'state'
] =
$state
;
$params
[
'display'
] =
$display
;
return
$this
->authorizeURL() .
"?"
. http_build_query(
$params
);
}
/**
* access_token接口
*
* 对应API:{@link http://open.weibo.com/wiki/OAuth2/access_token OAuth2/access_token}
*
* @param string $type 请求的类型,可以为:code, password, token
* @param array $keys 其他参数:
* - 当$type为code时: array('code'=>..., 'redirect_uri'=>...)
* - 当$type为password时: array('username'=>..., 'password'=>...)
* - 当$type为token时: array('refresh_token'=>...)
* @return array
*/
function
getAccessToken(
$type
=
'code'
,
$keys
) {
$params
=
array
();
$params
[
'client_id'
] =
$this
->client_id;
$params
[
'client_secret'
] =
$this
->client_secret;
if
(
$type
===
'code'
) {
$params
[
'grant_type'
] =
'authorization_code'
;
$params
[
'code'
] =
$keys
[
'code'
];
$params
[
'redirect_uri'
] =
$keys
[
'redirect_uri'
];
}
else
{
throw
new
OAuthException(
"wrong auth type"
);
}
$response
=
$this
->oAuthRequest(
$this
->accessTokenURL(),
'POST'
,
$params
);
$token
= json_decode(
$response
, true);
if
(
is_array
(
$token
) && !isset(
$token
[
'error'
]) ) {
$this
->access_token =
$token
[
'access_token'
];
}
else
{
throw
new
OAuthException(
"get access token failed."
.
$token
[
'error'
]);
}
return
$token
;
}
/**
* Format and sign an OAuth / API request
*
* @return string
* @ignore
*/
function
oAuthRequest(
$url
,
$method
,
$parameters
,
$multi
= false) {
$url
=
"{$this->host}{$url}.{$this->format}"
;
}
switch
(
$method
) {
case
'GET'
:
$url
=
$url
.
'?'
. http_build_query(
$parameters
);
return
$this
->http(
$url
,
'GET'
);
default
:
$headers
=
array
();
if
(!
$multi
&& (
is_array
(
$parameters
) ||
is_object
(
$parameters
)) ) {
$body
= http_build_query(
$parameters
);
}
else
{
$body
= self::build_http_query_multi(
$parameters
);
$headers
[] =
"Content-Type: multipart/form-data; boundary="
. self::
$boundary
;
}
return
$this
->http(
$url
,
$method
,
$body
,
$headers
);
}
}
/**
* Make an HTTP request
*
* @return string API results
* @ignore
*/
function
http(
$url
,
$method
,
$postfields
= NULL,
$headers
=
array
()) {
$this
->http_info =
array
();
$ci
= curl_init();
/* Curl settings */
curl_setopt(
$ci
, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0);
curl_setopt(
$ci
, CURLOPT_USERAGENT,
$this
->useragent);
curl_setopt(
$ci
, CURLOPT_CONNECTTIMEOUT,
$this
->connecttimeout);
curl_setopt(
$ci
, CURLOPT_TIMEOUT,
$this
->timeout);
curl_setopt(
$ci
, CURLOPT_RETURNTRANSFER, TRUE);
curl_setopt(
$ci
, CURLOPT_ENCODING,
""
);
curl_setopt(
$ci
, CURLOPT_SSL_VERIFYPEER,
$this
->ssl_verifypeer);
curl_setopt(
$ci
, CURLOPT_HEADERFUNCTION,
array
(
$this
,
'getHeader'
));
curl_setopt(
$ci
, CURLOPT_HEADER, FALSE);
switch
(
$method
) {
case
'POST'
:
curl_setopt(
$ci
, CURLOPT_POST, TRUE);
if
(!
empty
(
$postfields
)) {
curl_setopt(
$ci
, CURLOPT_POSTFIELDS,
$postfields
);
$this
->postdata =
$postfields
;
}
break
;
case
'DELETE'
:
curl_setopt(
$ci
, CURLOPT_CUSTOMREQUEST,
'DELETE'
);
if
(!
empty
(
$postfields
)) {
$url
=
"{$url}?{$postfields}"
;
}
}
if
( isset(
$this
->access_token) &&
$this
->access_token )
$headers
[] =
"Authorization: OAuth2 "
.
$this
->access_token;
$headers
[] =
"API-RemoteIP: "
.
$_SERVER
[
'REMOTE_ADDR'
];
curl_setopt(
$ci
, CURLOPT_URL,
$url
);
curl_setopt(
$ci
, CURLOPT_HTTPHEADER,
$headers
);
curl_setopt(
$ci
, CURLINFO_HEADER_OUT, TRUE );
$response
= curl_exec(
$ci
);
$this
->http_code = curl_getinfo(
$ci
, CURLINFO_HTTP_CODE);
$this
->http_info =
array_merge
(
$this
->http_info, curl_getinfo(
$ci
));
$this
->url =
$url
;
if
(
$this
->debug) {
echo
"=====post data======\r\n"
;
var_dump(
$postfields
);
echo
'=====info====='
.
"\r\n"
;
print_r( curl_getinfo(
$ci
) );
echo
'=====$response====='
.
"\r\n"
;
print_r(
$response
);
}
curl_close (
$ci
);
return
$response
;
}
/**
* Get the header info to store.
*
* @return int
* @ignore
*/
function
getHeader(
$ch
,
$header
) {
$i
=
strpos
(
$header
,
':'
);
if
(!
empty
(
$i
)) {
$key
=
str_replace
(
'-'
,
'_'
,
strtolower
(
substr
(
$header
, 0,
$i
)));
$value
= trim(
substr
(
$header
,
$i
+ 2));
$this
->http_header[
$key
] =
$value
;
}
return
strlen
(
$header
);
}
/**
* @ignore
*/
public
static
function
build_http_query_multi(
$params
) {
if
(!
$params
)
return
''
;
uksort(
$params
,
'strcmp'
);
$pairs
=
array
();
self::
$boundary
=
$boundary
= uniqid(
'------------------'
);
$MPboundary
=
'--'
.
$boundary
;
$endMPboundary
=
$MPboundary
.
'--'
;
$multipartbody
=
''
;
foreach
(
$params
as
$parameter
=>
$value
) {
if
( in_array(
$parameter
,
array
(
'pic'
,
'image'
)) &&
$value
{0} ==
'@'
) {
$url
= ltrim(
$value
,
'@'
);
$content
=
file_get_contents
(
$url
);
$array
=
explode
(
'?'
,
basename
(
$url
) );
$filename
=
$array
[0];
$multipartbody
.=
$MPboundary
.
"\r\n"
;
$multipartbody
.=
'Content-Disposition: form-data; name="'
.
$parameter
.
'"; filename="'
.
$filename
.
'"'
.
"\r\n"
;
$multipartbody
.=
"Content-Type: image/unknown\r\n\r\n"
;
$multipartbody
.=
$content
.
"\r\n"
;
}
else
{
$multipartbody
.=
$MPboundary
.
"\r\n"
;
$multipartbody
.=
'content-disposition: form-data; name="'
.
$parameter
. "\
"\r\n\r\n"
;
$multipartbody
.=
$value
.
"\r\n"
;
}
}
$multipartbody
.=
$endMPboundary
;
return
$multipartbody
;
}
}
?>
|
getAuthorizeURL 方法是用来获取请求 request_token的地址。
getAccessToken 方法是获取 access_token
oAuthRequest 方法是用来发送请求
getHeader 我没发现有地方调用,但是没有他有不行,暂时不知道为什么。
至此Oauth认证结束。
认证就是为了得倒 access_token。本来我是要爬数据。才为了登录。后来发现直接使用 新浪的 api 测试接口,自动生成 access_token。爬虫就直接用。当然,早没有发现,才促使我去研究了 Oauth认证,额外的收获吧。总而言之,人总是在逼迫中才能进步。