OAuth1.0实践之foursquare客户端同步到饭否

上回说完了《OAuth2.0实践之foursquare客户端登录》这回还是要回来说OAuth1.0。

还 是我写的那个foursquare的web客户端程序。其中加上了同步到饭否的功能,原来用的是BasicAuth,不过饭否宣布从今年元旦开始关闭 BasicAuth,全面改用OAuth1.0。于是我又不得不赶在截止前几天把这部分程序改写了——还好不像上次那样完全重写,改起来还是很快的。不过 其中因为OAuth1.0的签名部分影响到了几个基础函数,改了一些地方,弄出了些坏味道。因为反正用php就是为了quick and dirty,也就懒得去重构什么的了,能用就行。

同样,这里也不讨论程序的其它部分,只谈一下同步到饭否的部分。

按照标准的OAuth1.0登录流程应该是这样的:

1、在服务提供方注册一个客户端应用,取得一对consumer_key和consumer_secret;

2、通过调用提供方的oauth request_token取得一个临时token;

3、凭这个临时token和回调地址转向到提供方的authorize页面,用户在这个页面上决定是否授权;

4、用户授权后,提供方转向到回调地址,并提供一个verifier码;

5、凭这个verifier码和临时token调用提供方的oauth access_token取得正式的access_token;

6、使用access_token调用提供方的API。

以上全部调用请求中都必须附上consumer_key/secret,并且加以数字签名。

很 明显,比OAuth2.0要复杂很多,还好这个技术已经比较成熟,有很多现成的库可以用。因为我的虚拟主机目前还是只支持PHP4,所以用的是一个可以支 持PHP4的库:lib_oauth.php。那个流行的OAuth.php库其实稍微改改也可以,我原来也试过,不过还是觉得用现成的方便些,就换成这 个lib_oauth了。

顺便吐槽一句,国内这些网站号称提供API的,个个实现都有妖蛾子,并不像国外网站做得那么规范。尤其以新浪和腾 迅为甚,网易和搜狐略好,不过搜狐也有问题,OAuth1.0不支持Head方式,而且对参数也有一些特别的要求。至于饭否,它最妖的地方就是:居然没有 verifier 码!估计是通过时间和IP之类的来鉴定的吧。

好吧,具体到这个应用上来。我需要实现的功能包括两块:一是登录饭否,二是通过饭否API发消息。

首先是按饭否官方方式申请一对key/secret。

然后用类似foursquare登录部分的方式实现一个饭否登录:

用户点一个链接,程序调用request_token并重定向到饭否的authorize,用户确认授权以后回调回来,程序再调用access_token取得正式的access_token(注意:这里没有verifier)并回以保存。

最后,在调用饭否API前用OAuth1.0规范进行请求签名。

具体的登录代码如下:

function fanfou_login() {
    session_start();
    $GLOBALS['user']['fantype'] = 'oauth';

    if ($oauth_token = $_GET['oauth_token']) { // 来自饭否的回调         $response = fanfou_process(FANFOU_OAUTH.'access_token'); // 直接申请access_token,与标准不同的是没有verifier         parse_str($response, $token);         $GLOBALS['user']['fanpass'] = $token['oauth_token'] .'|'.$token['oauth_token_secret'];         unset($_SESSION['oauth_request_token_secret']);         $user = fanfou_process(FANFOU_API.'account/verify_credentials.json'); // 调用一个API功能以验证登录是否成功         $GLOBALS['user']['fanuser'] = $user->id;         _user_save_cookie(1);         header('Location: '.BASE_URL.SF_CALLBACK.'?'.http_build_query(array('redir' => BASE_URL)));         exit();     } else { // 用户选择登录饭否         $response = fanfou_process(FANFOU_OAUTH.'request_token'); // 取得临时token         parse_str($response, $token);         $_SESSION['oauth_request_token_secret'] = $token['oauth_token_secret'];         $authorise_url = FANFOU_OAUTH.'authorize?oauth_token='.$token['oauth_token']."&oauth_callback=".BASE_URL.FANFOU_CALLBACK;         header("Location: $authorise_url"); // 转向饭否的authorize页面,请求用户授权     } }

取得access_token以后以及取得之前的请求,都需要按规范加入consumer_key/consumer_secret,并加以签名(签名内容包括规范要求的所有字段,不过这部分工作已经由oauth库处理了)。

function fan_oauth_sign(&$url, &$args) {
    require_once 'lib_oauth.php';  //  引用 lib_oauth 库
    $method = $args !== false ? 'POST' : 'GET';
    if (preg_match_all('#[?&]([^=]+)=([^&]+)#', $url, $matches, PREG_SET_ORDER)) {
        foreach ($matches as $match) {
            $args[$match[1]] = $match[2];
        }
        $url = substr($url, 0, strpos($url, '?'));
    }
    if (($oauth_token = $_GET['oauth_token']) && $_SESSION['oauth_request_token_secret']) {
        $oauth_token_secret = $_SESSION['oauth_request_token_secret'];
    } else {
        list($oauth_token, $oauth_token_secret) = explode('|', $GLOBALS['user']['fanpass']);
    }
        $keys = array(
            'oauth_key'        => OAUTH_CONSUMER_KEY,
            'oauth_secret'    => OAUTH_CONSUMER_SECRET,
            'user_key'      => $oauth_token,
            'user_secret'   => $oauth_token_secret,
        );
    $url = oauth_sign_get($keys, $url, $args, $method);  // OAuth1.0签名

    if ($method == 'POST'){         list($url, $args) = explode('?', $url, 2);     }else{         $args = false;     } }

function fanfou_process($url, $post_data = false) {     fan_oauth_sign($url, $post_data);     $result = api_process($url, $post_data);     $response = $result['response'];     $response_info = $result['response_info']; // 错误处理及json解码之类,从略…… }

这部分代码看起来比OAuth2.0并没有复杂太多,但这是在使用了oauth库的情况下才有这样的简化。用OAuth2.0的话则不需要依赖别的库就可以实现,而且还要简单。

整个客户端代码下载在Google Code

推送到[go4pro.org]