[筆記] Twitter API v1.1 踩坑記
最近在重構遠古時期寫的社群整合登入功能,主要是想把各平台引用的庫都拿掉,
畢竟只是要做簡單的 Token 授權和存取用戶個人資料,用到整個庫還是太肥了,
其中 Facebook、Google、Discord 甚至是 QQ、微博都已經採用 OAuth 2.0 標準,
唯獨 Twitter 主要還是採用相當古老的 OAuth 1.0a,真的是想要搞死人阿。
目前 Twitter 已經有部分 API 支援 OAuth 2.0 驗證,可以 [在此查看] 比較表格
驗證方式差異
OAuth 1.0a 最麻煩之處就是每次請求都要使用 Consumer Key / Secret 加上 Token / Token Secret 和請求參數來產生一個雜湊簽名,再將所有驗證參數帶入請求的 Headers 中。
POST /oauth/access_token?oauth_token=AAAAA&oauth_verifier=BBBBB HTTP/1.1
Host: api.twitter.com
Authorization: OAuth oauth_callback="https%3A%2F%2Fexample.com%2F", oauth_consumer_key="CCCCC", oauth_nonce="DDDDD", oauth_signature="EEEEE", oauth_signature_method="HMAC-SHA1", oauth_timestamp="1600111186", oauth_version="1.0"
相比之下,OAuth 2.0 只需要在每次請求中帶上 Bearer Token 驗證就能打天下,實在是方便了許多。
GET /v1/people/me?personFields=emailAddresses%2Cgenders%2Cnames%2Cphotos HTTP/1.1
Host: people.googleapis.com
Authorization: Bearer XXXXXXXXXX
深坑一:麻煩的編碼過程
先來說一下雜湊簽名的產生方法,這裡以第一步要取得 Request Token 為例,
首先要將包含 oauth_*
開頭的必要參數以 Key / Value 配對的方式收集好,
將 Key、Value 分別以 RFC 3986 標準編碼(Percent encoding)後,以 key 的字母順序來排序,
再將整個列表以 key=value
格式,並使用 &
符號來拼接各項參數,成為 Param String:
oauth_callback=https%3A%2F%2Fexample.com%2F&oauth_consumer_key=TWITTER_CONSUMER_KEY&oauth_nonce=NONCE&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1600111186&oauth_version=1.0
這種格式其實就是 XHR 中常會用到的 Query 參數格式,上面的步驟在 PHP 中等同:
$params = array(
'oauth_consumer_key' => TWITTER_CONSUMER_KEY,
'oauth_signature_method' => 'HMAC-SHA1',
'oauth_timestamp' => time(),
'oauth_version' => '1.0',
'oauth_callback' => 'https://example.com/',
'oauth_nonce' => NONCE,
);
ksort($params);
$paramString = http_build_query($params);
接著將 HTTP 請求方法(大寫)、請求 API 路徑以及上面獲得的 Param String 都以 Percent encode 編碼,並使用 &
相連結合成 Base String。
POST&https%3A%2F%2Fapi.twitter.com%2Foauth%2Frequest_token&oauth_callback%3Dhttps%253A%252F%252Fexample.com%252F%26oauth_consumer_key%3DTWITTER_CONSUMER_KEY%26oauth_nonce%3DNONCE%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1600111186%26oauth_version%3D1.0
接著準備簽名要用的 Key,是將 Consumer secret 以及 Token Secret(若無,則用空字串)以 Percent encode 編碼,並使用 &
相連成 Signing key。
TWITTER_CONSUMER_SECRET&
最後則是利用 HMAC-SHA1 算法,以 Signing key 為金鑰來計算 Base String 的雜湊值,以二進制格式輸出並用 base64 格式編碼。
以上的步驟在 PHP 中的實作方法為(以下函式使用 Class 中的寫法):
function getSignature() {
$paramString = ...;
$signature = self::calculateSignature(
'POST',
'https://api.twitter.com/oauth/request_token',
$paramString
);
}
private function calculateSignature($method, $endpoint, $paramString, $tokenSecret = '') {
$baseStrings = self::urlencodeRfc3986(array(
strtoupper($method),
$endpoint,
$paramString
));
$baseString = implode('&', $baseStrings);
$keys = self::urlencodeRfc3986(array(
TWITTER_CONSUMER_SECRET,
$tokenSecret
));
$key = implode('&', $keys);
return base64_encode(hash_hmac('sha1', $baseString, $key, true));
}
private function urlencodeRfc3986($input) {
$output = '';
if (is_array($input)) {
$output = array_map([$this, 'urlencodeRfc3986'], $input);
}
else if (is_scalar($input)) {
$output = rawurlencode($input);
}
return $output;
}
終於,走了這麼長的一段路才成功拿到了 oauth_signature
參數所需要的雜湊簽名。
深坑二:Header 帶的驗證字串格式不同
在上一個步驟拿到 oauth_signature
之後,要再把這個參數塞回去原本的參數列表 $params
中,
並和上面的步驟一樣先將 Key、Value 以 Percent encoding 編碼,但不需要再另外排序,
接著再依序拼接組合成 Header String,但問題就來了,這裡的拼接格式和上面的格式並不相同!
於是我就在這邊鬼打牆好一段時間,不斷得到 215: Bad Authentication data.
的回應。
後來熊熊一看才發現,原來 Header 驗證的格式是以 key="value"
整理,再以 ,
連接的方式拼接而成,而不是單純的做成 Query String 的格式就行,
所以只好將字串拼接分成兩種情況,給 Header 用的跟不是給 Header 用的(簽名用的):
private function makePayload($params, $forHeader = false) {
if ($forHeader) {
$payload = [];
foreach ($params as $key => $value) {
$payload[] = self::urlencodeRfc3986($key).'="'.self::urlencodeRfc3986($value).'"';
}
return implode(', ', $payload);
}
else {
ksort($params);
return http_build_query($params);
}
}
產完 Header String 之後,在字串前加上 OAuth
前綴,放到 Header 裡的 Authorization
欄位就能完成請求驗證啦!
順帶一提,此 API 回傳的內容使用 PHP 中的 parse_str 就能將結果解析成 Key / Value 配對的陣列。
深坑三:一個參數,各自表述
在前面所有驗證步驟都完事之後,接著就要進入最終正式環節,就是竊取存取使用者已授權的資料。
因為只記錄踩坑過程,這裡就不贅述將使用者導向授權頁面和用 Request Token 交換 Access Token 的細節,
由於我的需求是獲取使用者 ID、名稱、性別(Twitter 沒開放)、大頭貼和信箱,所以選擇 account/verify_credentials 這支 API,
這支 API 的驗證的方法和上面的步驟都一樣,整理參數列表、計算雜湊簽名後再把參數放到 Header 中驗證,
要注意的是,這支 API 是用 GET 請求,有使用到文件中 Parameters 裡的欄位都要一並收集到參數列表中。
GET /1.1/account/verify_credentials.json?skip_status=true&include_email=true HTTP/1.1
Host: api.twitter.com
Authorization: OAuth include_email="true", oauth_consumer_key="AAAAAAAAAAAAAAAAAAAAAAAAA", oauth_nonce="XXXXXXXXXX", oauth_signature="bBHPX5CFknxirhTLpTJU4gy0mkk%3D", oauth_signature_method="HMAC-SHA1", skip_status="true", oauth_timestamp="1600372433", oauth_token="000000000-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", oauth_version="1.0"
但問題來了,不管我怎麼驗證,永遠都會收到 32: Could not authenticate you.
的錯誤:
這問題真的卡了我滿長一段時間,本來一直朝著參數帶錯或是少帶的方向去找原因,但掃完文檔還是毫無頭緒,
索性就先把額外加的 GET 參數拿掉嘗試,沒想到還真的請求成功了,所以問題點一定就是在簽名或 Header 驗證上,
後來又仔細重看了 Authorizing a request 裡的每一字每一句後才驚覺完全漏掉了這段:
You should be able to see that the header contains 7 key/value pairs, where the keys all begin with the string “oauth_”.
原來是 GET 參數只有要在計算雜湊簽名的時候才需要帶入參數列表中,Header 驗證的欄位只會有不多不少剛剛好 7 個 oauth_
開頭的參數(為了方便辨識有修改格式):
Authorization: OAuth
oauth_consumer_key="AAAAAAAAAAAAAAAAAAAAAAAAA",
oauth_nonce="XXXXXXXXXX",
oauth_signature_method="HMAC-SHA1",
oauth_signature="bBHPX5CFknxirhTLpTJU4gy0mkk%3D",
oauth_timestamp="1600358031",
oauth_token="000000000-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
oauth_version="1.0"
Header 驗證的問題算是水落石出了,但到目前為止,Code 32 的錯誤還是存在,所以接下來就要改朝簽名問題的方向走...
好的,一個下午過去了,結果其實在 Creating a signature 的文檔中就有提到:
The base URL is the URL to which the request is directed, minus any query string or hash parameters.
也就是說,計算雜湊簽名的時候帶入的 $endpoint
參數必須是最基本的、不帶任何參數或後綴的 Base URL,
例如這支 API 的 Base URL 就是 https://api.twitter.com/1.1/account/verify_credentials.json
,
但我當初在 StackOverflow 上找到的 ✨最佳解答✨ 卻是這樣說:
If you want to get email from Twitter OAuth api, the url in signature should have ?include_email=true.
總之就是遇到問題除了 Google 之外,官方文檔還是要重讀一遍,才不會被來路不明的解答誤導了,
終於,在掉入一連串的深坑之後,成功從坑裡面爬出來了,這次的踩坑記也在這裡畫下完美(?)的句點。
最後附上官方文檔網址供參考。
serena
這個我做了好久 ==
Lay
只能怪 Twitter 沒跟上時代
serena
上星期終於做出來了
feifan
请问,您所使用的测试post工具是什么? :thinking:
Lay
使用工具為 Postman 哦
feifan
谢谢 :laughing: