上回说完了《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。