2012年8月27日月曜日

[PHP][cURL] PHPのcURLを使ってGoogleにログインする

YAHOO! 知恵袋に(どなたか本当にお願いします!phpのcurlに関して教えて頂きたいです。)というPHPのcURLを使って、Googleにログインする方法が聞かれていたのでちょっと組んでみた。

実はこういうのは意外と面倒くさくて、
POSTデータでIDとパスワードを飛ばせばいいというものではない。
もちろんそれでログインできる(できてしまう)サイトもあるのだが、
セキュリティポリシーの高いサイトではそうはいかない。
不正なログインを防ぐためにフォーム内にトークンを埋め込み、
かつCookieにもそのトークンを埋め込んでおき、
サブミットされた際にフォームから飛んできたPOSTとCookieを比較しているのだ。
ちなみにGoogleとPixivはこの方式を採用している(2012/08/27現在)。

とりあえず早速ソースを見ていこう。

//URLを指定する
$url='https://accounts.google.com/ServiceLoginAuth';
//POST用のデータを作っておく
$data=array
(
    //ID部分(適宜置き換え)
    'Email'=>'',
    //パスワード部分(適宜置き換え)
    'Passwd'=>'',
    //ログインを維持するかのチェックボックス部分
    //'PersistentCookie'=>'yes',
);
//テンポラリファイルを作成する
$cookie=tempnam(sys_get_temp_dir(),'cookie_');

//cURLを初期化して使用可能にする
$curl=curl_init();
//オプションにURLを設定する
curl_setopt($curl,CURLOPT_URL,$url);
//文字列で結果を返させる
curl_setopt($curl,CURLOPT_RETURNTRANSFER,true);
//クッキーを書き込むファイルを指定
curl_setopt($curl,CURLOPT_COOKIEJAR,$cookie);
//URLにアクセスし、結果を文字列として返す
$html=curl_exec($curl);
//cURLのリソースを解放する
curl_close($curl);

//Document初期化
$dom=new DOMDocument();
//html文字列を読み込む(htmlに誤りがある場合エラーが出るので@をつける)
@$dom->loadHTML($html);
//XPath初期化
$xpath=new DOMXPath($dom);
//inputのtypeがhiddenの要素をとってくる
$node=$xpath->query('//input[@type="hidden"]');
foreach($node as $v)
{
    //POST用のデータに追加する
    $data[$v->getAttribute('name')]=$v->getAttribute('value');
}

//cURLを初期化して使用可能にする
$curl=curl_init();
//オプションにURLを設定する
curl_setopt($curl,CURLOPT_URL,$url);
//メソッドをPOSTに設定
curl_setopt ($curl,CURLOPT_POST,true);
//POSTデータ設定
curl_setopt($curl,CURLOPT_POSTFIELDS,$data);
//クッキーを読み込むファイルを指定
curl_setopt($curl,CURLOPT_COOKIEFILE,$cookie);
//Locationをたどる
curl_setopt($curl,CURLOPT_FOLLOWLOCATION,true);
//URLにアクセスし、結果を表示させる
curl_exec($curl);
//cURLのリソースを解放する
curl_close($curl);

//テンポラリファイルを削除
unlink($cookie);

という感じになる。
さて、実際に何をしているか見ていくと

//URLを指定する
$url='https://accounts.google.com/ServiceLoginAuth';
//POST用のデータを作っておく
$data=array
(
    //ID部分(適宜置き換え)
    'Email'=>'',
    //パスワード部分(適宜置き換え)
    'Passwd'=>'',
    //ログインを維持するかのチェックボックス部分
    //'PersistentCookie'=>'yes',
);
//テンポラリファイルを作成する
$cookie=tempnam(sys_get_temp_dir(),'cookie_');

の部分で必要な変数を用意している。
ID部分とパスワード部分は適宜置き換えて欲しい。

//cURLを初期化して使用可能にする
$curl=curl_init();
//オプションにURLを設定する
curl_setopt($curl,CURLOPT_URL,$url);
//文字列で結果を返させる
curl_setopt($curl,CURLOPT_RETURNTRANSFER,true);
//クッキーを書き込むファイルを指定
curl_setopt($curl,CURLOPT_COOKIEJAR,$cookie);
//URLにアクセスし、結果を文字列として返す
$html=curl_exec($curl);
//cURLのリソースを解放する
curl_close($curl);

の部分でフォーム内のトークンを分析するためのHTMLを取得する。

//Document初期化
$dom=new DOMDocument();
//html文字列を読み込む(htmlに誤りがある場合エラーが出るので@をつける)
@$dom->loadHTML($html);
//XPath初期化
$xpath=new DOMXPath($dom);
//inputのtypeがhiddenの要素をとってくる
$node=$xpath->query('//input[@type="hidden"]');
foreach($node as $v)
{
    //POST用のデータに追加する
    $data[$v->getAttribute('name')]=$v->getAttribute('value');
}

の部分でHTML解析を実際に行いトークンを取得し、POST用データ配列に入れる。

//cURLを初期化して使用可能にする
$curl=curl_init();
//オプションにURLを設定する
curl_setopt($curl,CURLOPT_URL,$url);
//メソッドをPOSTに設定
curl_setopt ($curl,CURLOPT_POST,true);
//POSTデータ設定
curl_setopt($curl,CURLOPT_POSTFIELDS,$data);
//クッキーを読み込むファイルを指定
curl_setopt($curl,CURLOPT_COOKIEFILE,$cookie);
//Locationをたどる
curl_setopt($curl,CURLOPT_FOLLOWLOCATION,true);
//URLにアクセスし、結果を表示させる
curl_exec($curl);
//cURLのリソースを解放する
curl_close($curl);

の部分で今まで作ったPOSTデータを飛ばしログインしている。

//テンポラリファイルを削除
unlink($cookie);

の部分でcookie保持用に用意したファイルを消しておく。
ただ、実際は放っておけばそのうち勝手に消えてしまうファイルなので、
わざわざ消す必要がないと言えばないのだが・・・

ポイントとなる関数
  • curl_init ・・・ cURL セッションを初期化する
  • curl_setopt ・・・ cURL 転送用オプションを設定する
  • curl_exec ・・・ cURL セッションを実行する
  • curl_close ・・・ cURL セッションを閉じる
  • DOMDocument クラス ・・・ HTML ドキュメントあるいは XML ドキュメント全体を表し、 ドキュメントツリーのルートとなります。
  • DOMXPath クラス ・・・ XPath 1.0 をサポートします。

25 件のコメント:

  1. Yahooで質問させて頂いたものです!

    とても参考になりましたありがとうございます!

    実際にログインする事ができました!

    トークン=type="hidden"の部分でしょうか?

    初心者なのでわかりません;;

    また、こういう関連の情報っていうのがネットにはあまりないのですがどのようにして勉強されたのでしょうか?

    オススメの書籍やサイト等がありましたら教えて頂けるとうれしいです!

    返信削除
    返信
    1. はーい。
      参考になったようで何よりですよ。

      今回はおそらくtype="hidden"の所にトークンが隠されているようですね。
      いくつかhiddenがあるので、どれが実際にトークンになってるかはGoogleがどのような実装してるかによりますけどね。
      なので、今回はトークンかどうか関係なしに、すべてのhiddenをとってきてPOSTデータに突っ込むことにしてます。(どれが必要か検証するの大変ですしね)

      また、関連の情報はどれをさしてるのでしょう?cURLの手法?POSTとCookieのトークンの手法?Googleのログインに何のデータが必要かの手法?

      cURLであればPHPマニュアルを熟読することがまず近道になると思います。
      PHPマニュアルは非常に多くの役に立つ情報があるので、
      既に知ってる関数なんかも目を通すといいと思います。
      参考サイト:http://php.net/manual/ja/function.curl-setopt.php

      POSTとCookieのトークンであればこれは実は割とメジャーな手法で、
      CSRF(クロスサイトリクエストフォージェリ)という攻撃を防ぐために、
      ワンタイムトークンというものを利用する方法があるのですが、
      今回がまさにワンタイムトークンです。
      この辺はセキュリティ関連でググるか、
      PHPのフレームワークの中身を読んでたら勉強できると思います。
      参考サイト:http://www.jumperz.net/texts/csrf.htm

      Googleのログインに何のデータが必要かであればいくつか方法はありますが、
      私はChromeを使うので、Chromeのデベロッパーツールから、飛んでるPOSTデータと
      飛ばしてるCookieデータを調べて、それをもとに推測ですね。
      今回のことに限らず、Chromeのデベロッパーツールを使いこなすのはHTMLやCSS、JavaScriptだけでなくPHPなどのサーバサイドのプログラマにとっても非常に有益なので覚えておいて損はないです。
      参考サイト:http://gihyo.jp/dev/feature/01/devtools/0001

      上記ので答えになってますかね?
      不足があれば言っていただければ。

      削除
    2. すごくご丁寧にありがとうございます!

      まさに聞きたかった内容は上記の内容です!
      ※特にCSRF(クロスサイトリクエストフォージェリ)

      まずは一つずつ勉強できていければと思います。

      ありがとうございました!

      削除
    3. はーい。
      個人的には結構セキュリティ周りは気にするところなので、
      CSRFの理解が進んだなら何よりです。

      どのようなプログラムを作ってらっしゃるのかは分からないですが、
      そのプログラムがうまくいくといいですね。
      グッドラック!

      削除
  2. すみません、以前にこちらで質問させて頂いたものですが、
    curlを使ってIPアドレスを指定し任意のサイトへアクセスする事は
    可能でしょうか?

    ユーザーエージェントだけであれば、CURLOPT_USERAGENTで設定
    できると思うのですが、IPに関してがどのように設定したらよいのか
    わかりません;

    CURLOPT_FTPPORT

    ↑このオプションを使うよう気がするのですが・・・;

    お手すきな時にご返答頂けると幸いです!

    返信削除
    返信
    1. はーい。
      その任意のIPアドレスが相手のこと(つまり接続先)なのか自分のこと(つまり接続元)なのかによって少し変わりますが、
      基本的には可能です。

      接続先のIPアドレスだとしたら基本的に

      $url='http://74.125.235.120/';
      $curl=curl_init();
      curl_setopt($curl,CURLOPT_URL,$url);
      curl_exec($curl);
      curl_close($curl);

      の様なコードで実現可能です。
      上記は

      $url='http://google.co.jp/';
      $curl=curl_init();
      curl_setopt($curl,CURLOPT_URL,$url);
      curl_exec($curl);
      curl_close($curl);

      と同じ結果が得られます。
      (ただし、GoogleがIPを変えたらもちろんだめですが・・・)

      Googleにアクセスするためだけに74.125.235.120というのわざわざ覚えるのは大変です。
      でも、google.co.jpと覚えるのは数字の羅列に比べて覚える難易度は下がります。
      なので、google.co.jp=74.125.235.120というひも付けをしてgoogle.co.jpでもアクセスできるようにしてるのです。
      ゆえに、google.co.jpと74.125.235.120は同じところを指すのでURLのドメイン部分をそのままIPにするだけアクセスできます。
      詳しくは「DNS」でググるといいかもしれません。

      次に、接続元のIPだとしたらプロキシサーバというのを通してあげると、
      自分のIPをそのサーバのIPとして、アクセスすることができます。
      ゆえに、厳密には任意のIPというよりは任意のプロキシサーバのIPでアクセスするということになります。
      実現方法もそれほど難しくなくCURLOPT_PROXY系オプションを使えば実現できます。
      シンプルな例として

      $url='http://74.125.235.120/';
      //プロキシのIPやドメイン
      $proxy='http://xxx.xxx.xxx.xxx/';
      $curl=curl_init();
      curl_setopt($curl,CURLOPT_URL,$url);
      curl_setopt($curl,CURLOPT_PROXY,$proxy);
      curl_exec($curl);
      curl_close($curl);

      という用な感じになります。
      詳しくは「プロキシ」でググるといいかもしれません。

      削除
  3. ご回答頂きましてありがとうございます!☆

    お仕事バタバタで返信するのが遅れてしまいました><

    丁寧にお答え頂いたのにすみません><

    実際にやりたかったのは後者のものです!

    串をさせばよかったんですね!

    実際にやってみるとできました!!

    本当にありがとうございます!!

    返信削除
    返信
    1. はーい。
      役に立てたようで何よりです。
      これからもプログラミングを楽しんでくださいね。

      削除
  4. はじめまして、Googleへの認証方法を調べていて、とても勉強になりました。
    お聞きしたいのですが、Googleへのログインが確認できたあとのページ遷移は可能なのでしょうか?

    ログイン後のCookie情報を再利用して次のページに対するアクセスを記述しているのですが、ログアウトされてしまっています。orz

    お時間があります時にご返信いただければ幸いです。

    返信削除
    返信
    1. はーい。
      はじめまして~。
      もちろんページ遷移可能ですよ。
      お察しの通りログイン管理にクッキーが使われています。
      もし私のソースコードを使っていると仮定したら、
      メールとパスワードをPOSTでなげる際にクッキー読み込みだけして書き込んでないので、
      そこにcurl_setopt($curl,CURLOPT_COOKIEJAR,$cookie);をつける。

      //cURLを初期化して使用可能にする
      $curl=curl_init();
      //オプションにURLを設定する
      curl_setopt($curl,CURLOPT_URL,$url);
      //メソッドをPOSTに設定
      curl_setopt ($curl,CURLOPT_POST,true);
      //POSTデータ設定
      curl_setopt($curl,CURLOPT_POSTFIELDS,$data);
      //クッキーを読み込むファイルを指定
      curl_setopt($curl,CURLOPT_COOKIEFILE,$cookie);
      /*
      追加
      */
      //クッキーを書き込むファイルを指定
      curl_setopt($curl,CURLOPT_COOKIEJAR,$cookie);
      //Locationをたどる
      curl_setopt($curl,CURLOPT_FOLLOWLOCATION,true);
      //URLにアクセスし、結果を表示させる
      curl_exec($curl);
      //cURLのリソースを解放する
      curl_close($curl);

      次にクッキーデータの入ったファイルを最後に消しているので、
      それをすべての処理が終わった後に消すようにするする。

      /*
      これをすべての処理が終わった後にする
      */
      //テンポラリファイルを削除
      unlink($cookie);

      という点を改修した上で、行きたいURLにクッキー読み込みつつcURLで飛ばせば問題ないと思いますー。
      つまり

      //cURLを初期化して使用可能にする
      $curl=curl_init();
      //オプションにURLを設定する
      curl_setopt($curl,CURLOPT_URL,$url);
      //クッキーを読み込むファイルを指定
      curl_setopt($curl,CURLOPT_COOKIEFILE,$cookie);
      //Locationをたどる
      curl_setopt($curl,CURLOPT_FOLLOWLOCATION,true);
      //URLにアクセスし、結果を表示させる
      curl_exec($curl);
      //cURLのリソースを解放する
      curl_close($curl);

      こんな感じ。

      コメント欄だとソースコードが読みづらいので全体は省きますが、必要があれば載せますよー。
      また、逆にソースコード見せてもらったほうが不適切な点を指摘しやすいかもですね。

      うまくいくといいですね!

      削除
    2. ご回答ありがとうございます。
      教えていただいた方法でGoogleドライブ上のファイルまで遷移できました。(>_<)
      いろいろ試してみたいと思います、本当にありがとうございました。

      削除
    3. はーい。
      うまくいったならよかったです。
      機会があればプラウザもどきなども作ってみると面白いですよ!

      削除
  5. 記事からコメントまで大変参考にさせていただきました。
    乗っかりで質問します。
    とあるブログに(APIがない)PHPで投稿したいと思っているのですが、ログイン->記事をフォームに登録するところまでは上記の記事とコメントを参考に進むのですが、最後に記事の確認ページがあって、そのページは表示URLとPOSTするURLが別になっていてうまいことPOSTできません。(form action="url" とブラウザが表示しているurlが違う)

    (ネットを徘徊した感じ無理かなとは思いますが)
    POSTする方法はあるのでしょうか、ないのでしょうか?

    返信削除
    返信
    1. はーい。
      確認ページがあるようなサイトに投稿ですよね。
      結論だけ言えばそのサイトの作りにもよりますが、できる場所が大半です。
      もちろん意図的にそういうことができないように作ることも可能ですが、
      意外とそういう風に作るのは面倒くさくて、やってない場所が多いです。

      さて、とあるブログとしか書いてないので、正直あまり具体的な解決案は提示しにくいですが、いくつかポイントだけ。

      まずどういう風に確認ページを表示させてるかというのが一つのポイントになると思います。

      確認ページということは入力ページで情報を受け取って保持しておき表示させてるということです。

      では、どこで保持してるか?というのがまず最初のポイントだと思います。

      1.確認ページにinput type="hidden"を入れておき再度POSTするサイト(こういうサイトはセキュリティがぼろぼろなことが多いです)
      2.Cookieに入れるサイト(かなり昔はそういうところもあったりしました)
      3.SESSIONやRedisなんかに入れるサイト(こっちが今は主流ですかね)

      1.をやってるサイトはセキュリティ的には論外ですがショッピングサイトですらやってる場所もあります。
      クレジットカードすらhiddenに入れるその精神はあきれるを通り越して感心しますね。
      しかし、cURLでPOSTするということであれば、これが一番簡単。
      なぜなら、入力画面のURLにcURLでデータをPOSTするのではなくて、はじめから確認画面のURLにcURLでデータをPOSTすれば後はサーバが受け取って勝手に処理をしてくれるのですから。

      2.の場合はちょっと面倒で、自分でCookieにPOSTデータを相手のフォーマット通りに入れて確認画面のサブミット先(完了画面とでも言いましょうか)にPOSTすればうまく行くことが多いです。
      相手がどういうフォーマットでCookieを作ってるのか調べるのが面倒ですが、それが分かればPOSTデータではなくCookieでデータを渡してるというだけなので割とシンプルです。

      3.のSESSIONやRedisの場合は簡単だったり複雑だったりします。
      これはつまり入力画面でPOSTされたデータをサーバで保持しておき、
      確認画面ではサーバが持ってる情報を表示させ、
      完了画面ではサーバが持ってる情報をもとに処理する・・・という感じになると思います。
      この場合は入力画面にcURLでデータをPOSTしておき、サーバに覚えさせる必要があります。
      そして確認画面でサーバに保持してるデータを処理させる何かしらのトリガーが必要になります。
      一般的にこのトリガーはただのサブミットであることが多いです。(input type="submit" のnameであったりします
      つまりこの方式では、cURLで入力画面でデータをPOST、確認画面でトリガーをPOSTしてあげればうまく行くことが多いです。
      あと、もちろんSESSIONなどにはCookieが必要なため、CURLOPT_COOKIEFILEやCURLOPT_COOKIEJARなどのcURLオプションは適宜必要です。

      1.2.3.のパターンやPOSTのトリガーを調べるにはどうしたらいいかと言えば、
      Google Chromeのデバッグツールが便利です。
      デバッグツールではHTMLのinputはもちろん、
      HTTPのヘッダやPOSTデータ、Cookieが覗けるため、
      サーバとクライアントでどういうやり取りをしてるかがすぐ分かります。
      言い換えれば、cURLでこのデバッグツールと全く同じ値をやり取りすれば、
      全く同じ動作になるはずです。
      Googleログインのときもそうですが、この辺を解析すると割とすぐ自前でAPIを作ったりすることができるようになります。

      大分長くなっちゃいましたが、何のサイトなのかや他の追加情報が分かれば、また違ったアドバイスができると思います。

      うまく行くといいですね!

      P.S.
      form action="url"となっているということは、そのaction="url"のurlにPOSTデータを飛ばす作りになってるのかな?
      そのサイトが分からないので完全に推測ですが、そのaction="url"のurlにPOSTデータを飛ばしたりしたらうまく行くかもしれませんね。
      まぁ、3.の方法を使って画面遷移のためだけにaction="url"を使ってるだけかもしれませんが・・・

      削除
    2. んー・・・見直すとちょっと書き方悪いかも・・・?
      1.2.3.のいずれも確認画面でサブミットしてる先のURL(確認ページ自身だったり、完了ページだったりする)にPOSTをするイメージですかね。

      分かりにくかったらツッコミください。

      削除
  6. 丁寧&すばやい返信ありがとうございます。
    もう一度このページを読み返し、ふと考えたら、律儀にステップ踏まなくてもいいんじゃね?
    と思い、ログイン->投稿内容をあらかじめ用意して投稿URLへポストしたら、うまくいきました。
    しばらくChromeのデベロッパーツールとにらめっこしてましたが^^;

    とても勉強になりました、ありがとうございました。

    返信削除
    返信
    1. はーい。
      やり方的には1.の直でデータ叩き込む感じで解決したってことみたいですね。
      何にしてもうまくいったようでよかったです。

      Chromeは色々サーバとのやり取りを調べるのに非常に重宝します。
      後はCharlesなんかもあるとさらに便利なのですが、いかんせん有料なのでなかなか・・・。

      今後も色々面白いものを開発していってください!

      削除
  7. クッキー部分とロケーションを辿る項目、むっちゃ参考になりました。
    感謝です...

    返信削除
    返信
    1. はーい。
      参考になったようで何よりです。
      なんだかんだで本文もそうですが、コメントも色んな人の試行錯誤が見えるのでコメントも時間があれば目を通すとより役立つかもしれませんね。
      まぁ、もし何か疑問などがあればコメントに書いてたらなるべく答えさせて頂きますー。

      削除
  8. 5年前の記事ですが、非常に有用な記事で助かりました。
    やりたい事が事細かく説明されていて、役立ちました。

    返信削除
    返信
    1. もうそんなに前の記事なんですね。
      それでも役に立てたようでよかったですー。

      もし何か疑問が出てきたり質問がある場合はいつでもコメントにどうぞー。

      削除
  9. 質問よろしいでしょうか、グーグルの検索結果を毎日カールで取得したいと思っているのですが通常と異なるトラフィックのエラーとなります。参考にクッキーやユーザーエージェントまわりを設定試しているのですが何か解決策ありますでしょうか?

    返信削除
    返信
    1. うーん、それはGoogleに目をつけられちゃった感じですね。

      bot的な使い方でGoogle検索を使用すると・・・つまりは短期間で大量の回数リクエストしたりすると「あなたはロボットではありませんか?」的なチェックボックスなりが出る画面にいくことがあります。
      そうなった場合は時間をおくとでなくなるみたいですね。

      ただ、わりかしいろんな状況で起きるらしいので場合によってはプロキシの設定やもし使ってるならVPNの設定やらを見直すといいかもです。

      もしそのプログラムを書いた1発目から「通常と異なるトラフィックが~~~」なら環境や設定周りを、何度か実行した後に言われたなら暫く時間をおくか、バグとかでループにハマって大量リクエスト送っちゃってたりしてないか、仕様上短期間で何度もリクエストを送るようなことになってないか、あたりを一度チェックしてみてください。

      削除
    2. 返信おそらくなりました、ご回答ありがとうございます。

      削除
    3. どういたしましてー。

      セキュリティ周りは本当に年々色々と変わっていくのでこまめに情報を仕入れるといざという時に役に立つと思いますよー。

      削除