{ yeah : 必须哒 } No place to place should record our youth?

27Jan/110

[转]基于PECL OAuth打造微博应用

最近,国内主要门户网站相继开放了微博平台,对开发者而言这无疑是个利好消息,不过在实际使用中却发现平台质量良莠不齐,有很多不完善的地方,就拿PHP版SDK来说吧,多半都是用TwitterOAuth改的,一旦多平台集成,很容易出现命名冲突之类的问题。

既然官方SDK不给力,那我们只能发扬自力更生的革命精神了!好消息是PHP本身已经有了一个标准的OAuth实现:PECL OAuth!下面以此为例来讲解一下如何实现微博应用:

说明:首先需要对OAuth概念有一定的了解,如不清楚可以参考我以前写的文章:OAuth那些事儿,其次需要注册成为各个微博平台(新浪腾讯搜狐网易)的开发者,拿到属于你自己的CONSUMER_KEY和CONSUMER_SECRET(有时也被称作APP_*)。

下面开始!假定我们要开发一个类似Follow5微博通的应用,简单点说就是把消息同时发送到多个微博平台,出于安全性的考虑,不会使用HTTP Basic,而会使用OAuth,这就需要我们先拿到Access Token和Access Token Secret。

以新浪微博为例,大致的代码如下:

<?php

 

session_start();

 

$request_token_url = 'http://api.t.sina.com.cn/oauth/request_token';

$authorize_url = 'http://api.t.sina.com.cn/oauth/authorize';

$access_token_url = 'http://api.t.sina.com.cn/oauth/access_token';

 

$oauth = new OAuth(

'YOUR_CONSUMER_KEY',

'YOUR_CONSUMER_SECRET',

OAUTH_SIG_METHOD_HMACSHA1,

OAUTH_AUTH_TYPE_FORM

);

 

if (empty($_GET['oauth_verifier'])) {

$callback_url = "http://{$_SERVER['HTTP_HOST']}{$_SERVER['REQUEST_URI']}";

 

$request_token = $oauth->getRequestToken($request_token_url);

 

$_SESSION['oauth_token_secret'] = $request_token['oauth_token_secret'];

 

$param = array(

'oauth_token' => $request_token['oauth_token'],

'oauth_callback' => $callback_url

);

 

header("Location: {$authorize_url}?" . http_build_query($param));

exit;

}

 

$oauth->setToken($_GET['oauth_token'], $_SESSION['oauth_token_secret']);

 

$access_token = $oauth->getAccessToken(

$access_token_url, null, $_GET['oauth_verifier']

);

 

var_dump($access_token);

 

?>

腾讯微博相比较而言有点特殊,大致代码如下:

<?php

 

session_start();

 

$request_token_url = 'https://open.t.qq.com/cgi-bin/request_token';

$authorize_url = 'https://open.t.qq.com/cgi-bin/authorize';

$access_token_url = 'https://open.t.qq.com/cgi-bin/access_token';

 

$oauth = new OAuth(

'YOUR_CONSUMER_KEY',

'YOUR_CONSUMER_SECRET',

OAUTH_SIG_METHOD_HMACSHA1,

OAUTH_AUTH_TYPE_FORM

);

 

$oauth->setNonce(md5(mt_rand()));

 

if (empty($_GET['oauth_verifier'])) {

$callback_url = "http://{$_SERVER['HTTP_HOST']}{$_SERVER['REQUEST_URI']}";

 

$request_token = $oauth->getRequestToken($request_token_url, $callback_url);

 

$_SESSION['oauth_token_secret'] = $request_token['oauth_token_secret'];

 

$param = array(

'oauth_token' => $request_token['oauth_token']

);

 

header("Location: {$authorize_url}?" . http_build_query($param));

exit;

}

 

$oauth->setToken($_GET['oauth_token'], $_SESSION['oauth_token_secret']);

 

$access_token = $oauth->getAccessToken(

$access_token_url, null, $_GET['oauth_verifier']

);

 

var_dump($access_token);

 

?>

注意:参数nonce和callback的设置,详见:使用 PECL 的 OAuth 库访问 QQ 微博 API

照猫画虎就能得到搜狐和网易的Access Token和Access Token Secret了,我就不罗嗦了。

下面继续做我们的微博应用,发消息一般都是文本形式的,不过有中国特色的微博开放平台支持文本加图片的方式:图片上传到服务器,但本身并不参与签名。这和标准OAuth是冲突的,所以要扩展一下PECL OAuth,并且尽可能兼容原类的使用方法和习惯:

<?php

 

class MicroblogOAuth extends OAuth

{

public $consumer_key;

 

public $signature_method;

 

public $auth_type;

 

public $nonce;

 

public $timestamp;

 

public $token;

 

public $version;

 

public $request_engine;

 

public $last_response;

 

public function setAuthType($auth_type)

{

if (parent::setAuthType($auth_type)) {

$this->auth_type = $auth_type;

 

return true;

}

 

return false;

}

 

public function setNonce($nonce)

{

if (parent::setNonce($nonce)) {

$this->nonce = $nonce;

 

return true;

}

 

return false;

}

 

public function setTimestamp($timestamp)

{

if (parent::setTimestamp($timestamp)) {

$this->timestamp = $timestamp;

 

return true;

}

 

return false;

}

 

public function setToken($token, $token_secret)

{

if (parent::setToken($token, $token_secret)) {

$this->token = $token;

 

return true;

}

 

return false;

}

 

public function setVersion($version)

{

if (parent::setVersion($version)) {

$this->version = $version;

 

return true;

}

 

return false;

}

 

public function setRequestEngine($request_engine)

{

try {

parent::setRequestEngine($request_engine);

 

$this->request_engine = $request_engine;

} catch(OAuthException $e) {

echo $e->getMessage();

}

}

 

public function getLastResponse()

{

return parent::getLastResponse() ?: $this->last_response;

}

 

public function upload($url, $file, $param = array(), $header = array())

{

$boundary = sprintf('%010d', mt_rand());

 

$header[] = "Content-Type: multipart/form-data; boundary={$boundary}";

 

$oauth = array(

'oauth_consumer_key' => $this->consumer_key,

'oauth_nonce' => $this->nonce,

'oauth_signature_method' => $this->signature_method,

'oauth_timestamp' => $this->timestamp,

'oauth_token' => $this->token,

'oauth_version' => $this->version,

);

 

if ($this->auth_type == OAUTH_AUTH_TYPE_FORM) {

$param += $oauth;

 

$param['oauth_signature'] = $this->generateSignature(

OAUTH_HTTP_METHOD_POST, $url, $param

);

} else {

$oauth_header = array();

 

$oauth['oauth_signature'] = $this->generateSignature(

OAUTH_HTTP_METHOD_POST, $url, $param

);

 

foreach ($oauth AS $name => $value) {

$oauth_header[] = $name . '="' . $value . '"';

}

 

$header[] = 'Authorization: OAuth ' . implode(', ', $oauth_header);

}

 

$content_disposition = function($name, $filename = null) {

$result = 'Content-Disposition: form-data; name="' . $name . '"';

 

if ($filename !== null) {

$result .= '; filename="' . $filename . '"';

}

 

return $result;

};

 

$content = array();

 

foreach ($file as $name => $value) {

$filename = pathinfo($value, PATHINFO_BASENAME);

 

switch(strtolower(pathinfo($filename, PATHINFO_EXTENSION))) {

case 'gif';

$mime = 'image/gif';

break;

case 'jpeg':

case 'jpg':

$mime = 'image/jpg';

break;

case 'png';

$mime = 'image/png';

break;

default:

$mime = 'application/octet-stream';

}

 

$content_type = "Content-Type: {$mime}";

 

$content[] = "--{$boundary}";

$content[] = $content_disposition($name, $filename);

$content[] = $content_type;

$content[] = '';

 

$content[] = file_get_contents($value);

}

 

ksort($param);

 

foreach ($param as $name => $value) {

$content[] = "--{$boundary}";

$content[] = $content_disposition($name);

$content[] = '';

 

$content[] = $value;

}

 

$content[] = "--{$boundary}--";

$content[] = '';

 

$content = implode("\r\n", $content);

 

if ($this->request_engine == OAUTH_REQENGINE_CURL) {

$header[] = 'Expect:';

 

$curl = curl_init();

 

curl_setopt($curl, CURLOPT_POST, true);

curl_setopt($curl, CURLOPT_POSTFIELDS, $content);

curl_setopt($curl, CURLOPT_HTTPHEADER, $header);

curl_setopt($curl, CURLOPT_URL, $url);

 

curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);

 

$response = curl_exec($curl);

 

curl_close($curl);

} else {

$header[] = 'Connection: close';

 

$context = stream_context_create(array(

'http' => array(

'protocol_version' => '1.1',

'method' => 'POST',

'content' => $content,

'header' => implode("\r\n", $header),

)

));

 

$response = file_get_contents($url, false, $context);

}

 

if ($response) {

$this->last_response = $response;

 

return true;

}

 

return false;

}

}

 

?>

注意:为了让代码潮一点,用了一些PHP5.3以上版本才有的特性,你可以改写成低版本。

如果使用CURL方式发送请求的话,最好发送一个空的Expect头,否则如果POST数据大于1K,CURL会自作主张发送 Expect:100-continue头,对多数Web服务器来说这没问题,但低版本Lighttpd(如1.4)则会出现HTTP 417错误。

详见:'Expect' header gives HTTP error 417

如果使用PHP Streams方式发送请求的话,缺省使用的是HTTP1.0版本,可以通过tcpdump命令来检测这种现象,大致方法如下:

shell> tcpdump -A host foo.com

此时,某些防火墙会过滤掉非标准HTTP1.0的请求头,如Host请求头,从而造成错误。

详见:由于 HTTP request 不规范导致的被防火墙拦截

新类MicroblogOAuth直接扩展自PECL的OAuth类!随着PHP内核API的逐渐类化,这样的扩展方式将会越来越常见,值得开发人员重视。

为了让调用方式更统一,使用工厂方法包装MicroblogOAuth的实例化过程:

<?php

 

function OAuth($consumer_key, $consumer_secret, $signature_method, $auth_type)

{

$instance = new MicroblogOAuth(

$consumer_key,

$consumer_secret,

$signature_method,

$auth_type

);

 

$instance->consumer_key = $consumer_key;

$instance->signature_method = $signature_method;

 

$instance->setAuthType($auth_type);

$instance->setNonce(md5(mt_rand()));

$instance->setTimestamp(time());

$instance->setVersion('1.0');

 

if (extension_loaded('curl')) {

$instance->setRequestEngine(OAUTH_REQENGINE_CURL);

} else {

$instance->setRequestEngine(OAUTH_REQENGINE_STREAMS);

}

 

$instance->last_response = null;

 

return $instance;

}

 

?>

先看看搜狐是如何发送文本加图片消息的:

<?php

 

$text = 'hello, world.';

$image = 'http://www.foo.com/bar.gif';

 

$oauth = OAuth(

'YOUR_CONSUMER_KEY',

'YOUR_CONSUMER_SECRET',

OAUTH_SIG_METHOD_HMACSHA1,

OAUTH_AUTH_TYPE_AUTHORIZATION

);

 

$oauth->setToken(

'YOUR_ACCESS_TOKEN',

'YOUR_ACCESS_TOKEN_SECRET'

);

 

$oauth->upload(

'http://api.t.sohu.com/statuses/upload.json',

array('pic' => $image),

array('status' => oauth_urlencode($text))

);

 

$result = json_decode($oauth->getLastResponse(), true);

 

var_dump($result);

 

?>

说明:搜狐要求文本要先编码,然后和图片一起发送,这点不同于其它微博开放平台。

再看看网易是如何发送文本加图片消息的:

<?php

 

$text = 'hello, world.';

$image = 'http://www.foo.com/bar.gif';

 

$oauth = OAuth(

'YOUR_CONSUMER_KEY',

'YOUR_CONSUMER_SECRET',

OAUTH_SIG_METHOD_HMACSHA1,

OAUTH_AUTH_TYPE_AUTHORIZATION

);

 

$oauth->setToken(

'YOUR_ACCESS_TOKEN',

'YOUR_ACCESS_TOKEN_SECRET'

);

 

$oauth->upload(

'http://api.t.163.com/statuses/upload.json',

array('pic' => $image)

);

 

$result = json_decode($oauth->getLastResponse(), true);

 

if (isset($result['upload_image_url'])) {

$text .= " {$result['upload_image_url']}";

}

 

$oauth->fetch(

'http://api.t.163.com/statuses/update.json',

array('status' => $text),

OAUTH_HTTP_METHOD_POST

);

 

$result = json_decode($oauth->getLastResponse(), true);

 

var_dump($result);

 

?>

说明:网易发送文本加图片消息是分两步实现的,先上传图片,然后把图片的URL附加在文本信息的后面再发送到服务器,这点不同于其它微博开放平台。

收工!微博开放平台的使用并没有太多复杂的地方,仔细看文档调试,一般的问题都很容易解决。有了上面的基础代码,只要再使用适配器模式分别包装一下各个微博平台,很容易就能实现一套通用SDK,搞定新浪,腾讯,搜狐,网易!

【转自:http://huoding.com/2011/01/16/42】

Posted by alacner

Comments (0) Trackbacks (0)

No comments yet.


Leave a comment

(required)

No trackbacks yet.