PHP
curl
はてなブックマーク
OAuth

PHPでOAuth1認証をして、はてブのAPIを叩いてブクマする


経緯

手動ブクマだるいなと思って探してたらメール投げてブクマする奴がなくなっていた

メールでのブックマーク投稿機能を終了します

なので、OAuthを用いたAPIを叩こうと思い(これならまだ長生きしそう)だし、要件やら勉強がてらやらでPHPを使い、素でOAuth1を書きました(やたらと細かくてつまづきまくった)

scopeとかはよしなにすると良いと思う()

以下のコードが主

<?php

namespace hatebu;

class Hatebu
{
private $oauth_consumer_key;
private $oauth_consumer_secret_key;
private $request_token_url = "https://www.hatena.com/oauth/initiate";
private $access_token_url = 'https://www.hatena.com/oauth/token';
private $bookmark_api_url = 'http://api.b.hatena.ne.jp/1/my/bookmark';
private $oauth_signature_method = 'HMAC-SHA1';
private $oauth_version = '1.0';

/**
* Hatebu constructor.
* @param $consumer_key
* @param $oauth_consumer_secret_key
*/

public function __construct($consumer_key, $oauth_consumer_secret_key)
{
$this->oauth_consumer_key = $consumer_key;
$this->oauth_consumer_secret_key = $oauth_consumer_secret_key;
}

/**
* @return array
*/

public function get_request_token()
{
$method = 'POST';
$url = $this->request_token_url;
$authorization = array(
'oauth_callback' => "oob",
'oauth_consumer_key' => $this->oauth_consumer_key,
'oauth_nonce' => md5(uniqid(rand(), true)),
'oauth_signature_method' => $this->oauth_signature_method,
'oauth_timestamp' => time(),
'oauth_version' => $this->oauth_version,
);

$body = array(
'scope' => 'read_public,write_public,read_private,write_private'
);
$authorization['oauth_signature'] = $this->create_signature(
$authorization,
$method,
NULL,
$url,
$body
);

$response = $this->send_request($url, $authorization, $method, $body);
$responses = explode("&", $response);
return array(
'oauth_token' => rawurldecode(explode("=", $responses[0])[1]),
'oauth_token_secret' => rawurldecode(explode("=", $responses[1])[1])
);
}

/**
* @param $oauth_request_token
* @param $oauth_request_token_secret
* @param $oauth_verifier
* @return array
*/

public function get_access_token($oauth_request_token, $oauth_request_token_secret, $oauth_verifier)
{
$method = 'POST';
$url = $this->access_token_url;
$authorization = array(
'oauth_consumer_key' => $this->oauth_consumer_key,
'oauth_nonce' => md5(uniqid(rand(), true)),
'oauth_signature_method' => $this->oauth_signature_method,
'oauth_timestamp' => time(),
'oauth_token' => $oauth_request_token,
'oauth_verifier' => $oauth_verifier,
'oauth_version' => $this->oauth_version
);
$authorization['oauth_signature'] = $this->create_signature(
$authorization,
$method,
$oauth_request_token_secret,
$url
);
$response = $this->send_request($url, $authorization, $method);
$responses = explode("&", $response);
return array(
'oauth_token' => rawurldecode(explode("=", $responses[0])[1]),
'oauth_token_secret' => rawurldecode(explode("=", $responses[1])[1]),
'url_name' => rawurldecode(explode("=", $responses[2])[1]),
'display_name' => rawurldecode(explode("=", $responses[3])[1])
);
}

public function post_bookmark(String $bookmark_url, String $comment, String $oauth_access_token, $oauth_access_token_secret)
{
$url = $this->bookmark_api_url;
$method = 'POST';
$authorization = array(
'oauth_consumer_key' => $this->oauth_consumer_key,
'oauth_nonce' => md5(uniqid(rand(), true)),
'oauth_signature_method' => $this->oauth_signature_method,
'oauth_timestamp' => time(),
'oauth_version' => $this->oauth_version
);

$body = [
'url' => $bookmark_url,
'comment' => $comment,
];
// ここでsignatureにbodyを含めると、認証が通らなくなる
$authorization['oauth_signature'] = $this->create_signature(
$authorization,
$method,
$oauth_access_token_secret,
$url
);

return json_decode(
$this->send_request($url, $authorization, $method, $body),
true
);
}

/**
* @param $url
* @param $authorization
* @param $method
* @param $body
* @param $additional_headers
* @return bool|string
*/

private function send_request($url, $authorization, $method, $body = NULL, $additional_headers = NULL)
{
$oauthHeader = 'OAuth ';
foreach ($authorization as $key => $val) {
$oauthHeader .= $key . '="' . rawurlencode($val) . '",';
}
$oauthHeader = substr($oauthHeader, 0, -1);
$header = array(
'Authorization:' . $oauthHeader,
'User-Agent:Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:26.0) Gecko/20100101 Firefox/26.0',
'Expect:',
);
$curl = curl_init();
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method);
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 30);
curl_setopt($curl, CURLOPT_TIMEOUT, 30);
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
if (isset($additional_headers)) {
$header = array_merge($header, $additional_headers);
}

curl_setopt($curl, CURLOPT_HTTPHEADER, $header);
curl_setopt($curl, CURLINFO_HEADER_OUT, true);
if (isset($body)) {
curl_setopt($curl, CURLOPT_POSTFIELDS, $body);
}

$res = curl_exec($curl);
$res_info = curl_getinfo($curl);
if ($res_info['http_code'] != 200 && $res_info['http_code'] != 201) {
var_dump($res_info);
var_dump($res);
exit(1);
}

curl_close($curl);
return $res;
}

/**
* @param $authorization
* @param $method
* @param $token_secret
* @param $url
* @param null $body
* @return string
*/

private function create_signature($authorization, $method, $token_secret, $url, $body = NULL)
{
if (isset($body)) {
$parameter_array = array_merge($authorization, $body);
} else {
$parameter_array = $authorization;
}

ksort($parameter_array);
$signatureBaseString = '';
foreach ($parameter_array as $key => $val) {
$signatureBaseString .= $key . '=' . rawurlencode($val) . '&';
}
$signatureBaseString = substr($signatureBaseString, 0, -1);
$signatureBaseString = sprintf("%s&%s&%s", $method, rawurlencode($url), rawurlencode($signatureBaseString));
if (isset($token_secret)) {
$signingKey = sprintf("%s&%s", rawurlencode($this->oauth_consumer_secret_key), rawurlencode($token_secret));
} else {
$signingKey = rawurlencode($this->oauth_consumer_secret_key) . '&';
}

return base64_encode(hash_hmac('sha1', $signatureBaseString, $signingKey, true));
}
}


叩く

こんな感じに書いて、デバックめっちゃしてた

<?php

require_once('hatebu.php');
$consumer_key = 'CONSUMER_KEY';
$consumer_secrete_key = 'SECRET KEY';
var_dump($consumer_key, $consumer_secrete_key);

var_dump('Create Request Token');
$hatebu = new hatebu\Hatebu($consumer_key, $consumer_secrete_key);
$request_tokens = $hatebu->get_request_token();
var_dump($request_tokens);

var_dump('Create Access Token');
print_r("https://www.hatena.ne.jp/oauth/authorize?oauth_token=" . $request_tokens['oauth_token'] . "\n");
print_r('input verify token: ');
$verify_token = trim(fgets(STDIN));
$access_tokens = $hatebu->get_access_token(
$request_tokens['oauth_token'],
$request_tokens['oauth_token_secret'],
$verify_token
);
var_dump($access_tokens);

var_dump("POST_BOOKMARK_________________");

$result = $hatebu->post_bookmark(
'https://www.nozograph.com/',
'API叩いてブクマしてみた',
$access_tokens['oauth_token'],
$access_tokens['oauth_token_secret']
);

ほんで、こんな感じにできる

スクリーンショット 2019-04-12 21.11.13.png

以上


所感

世に溢れてる、OAuth1ライブラリの中身を知れた

OAuth1といってもサービスによって微妙に仕様が違かったりするっぽく(歴史的な背景とかあるらしい)ので汎用的にやるの難しそうだなあと言った気持ち

OAuth1自体は、

Consumer key を取得して OAuth 開発をはじめよう

を見ればなんとなくわかるけど、HeaderのContent-Typeやらrequeset tokenを取得するとき だけ Sigunaturebodyを含むみたいなハマりどころがあります。

あと、urlencode周りで詰まりまくった

callbackを設定した方が良さそうなのでそのうちやりたい。

何か間違ってたり、色々ありましたらコメント等ください。

composerとか使って、ライブラリ化?すると良さそうですね(そのうちやります)


Special Thanks