基于PECL OAuth打造微博应用

标签: Technical OAuth PHP | 发表时间:2011-01-16 16:26 | 作者:老王 lostsnow
出处:http://huoding.com

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

相关 [pecl oauth 微博] 推荐:

基于PECL OAuth打造微博应用

- lostsnow - 火丁笔记
最近,国内主要门户网站相继开放了微博平台,对开发者而言这无疑是个利好消息,不过在实际使用中却发现平台质量良莠不齐,有很多不完善的地方,就拿PHP版SDK来说吧,多半都是用TwitterOAuth改的,一旦多平台集成,很容易出现命名冲突之类的问题. 既然官方SDK不给力,那我们只能发扬自力更生的革命精神了.

OAuth的改变

- lyxint - 火丁笔记
去年我写过一篇《OAuth那些事儿》,对OAuth做了一些简单扼要的介绍,今天我打算写一些细节,以阐明OAuth如何从1.0改变成1.0a,继而改变成2.0的. 在OAuth诞生前,Web安全方面的标准协议只有OpenID,不过它关注的是验证,即WHO的问题,而不是授权,即WHAT的问题. 好在FlickrAuth和GoogleAuthSub等私有协议在授权方面做了不少有益的尝试,从而为OAuth的诞生奠定了基础.

理解OAuth 2.0

- - 阮一峰的网络日志
OAuth是一个关于授权(authorization)的开放网络标准,在全世界得到广泛应用,目前的版本是2.0版. 本文对OAuth 2.0的设计思路和运行流程,做一个简明通俗的解释,主要参考材料为 RFC 6749. 为了理解OAuth的适用场合,让我举一个假设的例子. 有一个"云冲印"的网站,可以将用户储存在Google的照片,冲印出来.

oauth 认证心得

- 非狐外传 - python.cn(jobs, news)
OAUTH协议为用户资源的授权提供了一个安全的、开放而又简易的标准. 简易来说,就是我们可以在某一个第三方服务器,如:新浪,豆瓣,在用户授权,并且不透漏密码等信息给我们的条件下,访问和修改用户的资源. oauth的项目主页为:http://oauth.net/ ,现在国内很多网站的开放平台都采用了Oauth方式来进行授权.

OAuth学习笔记

- 宋大妈 - FeedzShare
来自: 标点符 - FeedzShare  . 发布时间:2011年08月29日,  已有 2 人推荐. OAuth(开放授权)是一个开放标准,允许用户让第三方应用访问该用户在某一网站上存储的私密的资源(如照片,视频,联系人列表),而无需将用户名和密码提供给第三方应用. OAuth允许用户提供一个令牌,而不是用户名和密码来访问他们存放在特定服务提供者的数据.

OAuth 2.0 工作流程

- - 企业架构 - ITeye博客
原文链接:http://www-01.ibm.com/support/knowledgecenter/SSELE6_8.0.0.3/com.ibm.ammob.doc_8.0.0.3/config/concept/con_oauth20_workflow.html%23con_oauth20_workflow?lang=zh.

GitHub - casinthecloud/cas-pac4j-oauth-demo: Demo webapps to test CAS/OAuth/OpenID/SAML client support and OAuth server support in CAS version >= 4.0.0

- -

纯 JavaScript 实现的 OAuth 认证

- - 博客 - 伯乐在线
英文原文: JavaScript oAuth 编译: oschina. 现在,很多的应用程序都在使用HTML和JavaScript, 这是一个非常明智的选择,让你跟上目前的趋势. 一些主要实体工具因为客户端验证和授权等原因提供了API. 当前网站对于验证的一个广受欢迎的功能是”单点登录”. 这让用户可以通过其它一些社交媒体网站上的身份认证直接登录你的网站.

PHP实现Google Oauth的登录系统

- - 极客521 | 极客521
本文讲述的是如何为你的PHP项目实现Google的Oauth系统. 这个示例PHP脚本非常快,对增加你的PHP项目注册当然是很有帮助的. 在这之前,我们已经覆盖了包含Facebook、Twitter、Google plus以及Instagram的Oauth登录系统示例. 很遗憾之前我遗漏掉了Google的Oauth登录系统.