IT技术博客大学习 共学习 共进步

基于PECL OAuth打造微博应用

火丁笔记 2011-01-16 22:36:09 浏览 5,102 次

    最近,国内主要门户网站相继开放了微博平台,对开发者而言这无疑是个利好消息,不过在实际使用中却发现平台质量良莠不齐,有很多不完善的地方,就拿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) {
            $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;
    }
}

?>

    说明:如果使用PHP Streams方式发送请求的话,需要注意HTTP版本的问题,否则某些情况下请求会被防火墙拦截,详见:由于 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,搞定新浪,腾讯,搜狐,网易!

建议继续学习

  1. Twitter/微博客的学习摘要 (阅读 12,082)
  2. 微博架构与平台安全演讲稿 (阅读 5,623)
  3. 微博进入肉搏时代 (阅读 5,102)
  4. 深入理解OAuth与豆瓣OAuth test (阅读 4,945)
  5. 新浪微博OAuth认证流程分析 (阅读 4,921)
  6. 给微博打上标签 (阅读 4,762)
  7. PHP for Twitter OAuth 教学演示 (阅读 4,642)
  8. 在sae中利用SaeFetchurl进行豆瓣的OAuth授权 (阅读 4,524)
  9. 新浪微博开放平台初探 (阅读 4,424)
  10. 构建可扩展的微博架构(qcon beijing 2010演讲) (阅读 4,160)