Catalyst::AuthenticationでTwitter認証を使う方法

CatalystTwitter認証をやろうとしたら結構ハマったのでメモを作っておく。
Catalyst::Authentication::Credential::Twitter(以下CAC:Twitter)ってのがあるんだけど、使い方が難しかった。

認証の流れは、

  1. アプリからTwitterの認証URLにアクセス
  2. Twitterでアクセスを「許可する」ボタンを押す
  3. アプリのcallback URLで認証処理
    1. アプリのDBにユーザー情報がなければ、ユーザー情報を登録。
  4. あとはよしなに。

アプリ名はMyApp、DB名はMyDBとした。
環境は、さくらレンタルサーバの一般プラン(VPSじゃない)で、local::libを使ってCPAN環境を作っている。詳しくはこちらを参考に。
http://blog.hide-k.net/archives/2009/02/locallibrootcpa.php

1.Twitterにアプリケーションを登録

PHPでTwitter APIのOAuthを使う方法まとめ - 頭ん中
この記事の「Twitter にアプリケーションを登録する」を参考に登録した。
アプリケーションの種類はブラウザアプリケーション。
コールバックURLはアプリのコンフィグで上書きできるので適当でよさそう。
Twitterでログインするは、有効にしておく。
登録したら、Cunsumer Key と Consumer secret が表示されるので、コピペしておく。

2.MyApp.pmの編集

package MyApp;
use Moose;
use namespace::autoclean;

use Catalyst::Runtime 5.80;

# (1)プラグインを追加
use Catalyst qw/
    -Debug
    ConfigLoader
    Static::Simple
    Unicode
    Authentication
    Session
    Session::PerUser
    Session::Store::File
    Session::State::Cookie
/;

extends 'Catalyst';

our $VERSION = '0.01';
$VERSION = eval $VERSION;

__PACKAGE__->config(
    name => 'MyApp',
    disable_component_resolution_regex_fallback => 1,
    ENCODING => 'utf-8',
    TEMPLATE_EXTENSION => '.tt',

     "Plugin::Authentication" => {
         default_realm => "twitter",
         realms => {
             twitter => {
                 credential => {
                     class => "Twitter", # (2) CAC::Twitterを指定
                 },
                 store => {
                     class => 'DBIx::Class', # (3) StoreはDBIx::Classを使用
                     user_model => 'MyDB::twitteruser', # (4) ユーザー情報を格納するテーブル名を指定
                 },
                 auto_create_user => 1, # (5) 自動ユーザー登録をON
                 consumer_key    => 'xxxxxxxxxxxxxxxxxxxxx', # (6) Twitterにアプリケーションを登録した時に取得した値を入れる。
                 consumer_secret => 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', # (6)
                 callback_url => 'http://<アプリのベース>/callback', # (7)
             },
         },
     },
);

プラグイン(1)は、Authentication以下が追加したもの。Session::PerUserはCA::Credential::Twitterが使う。ソース(Twitter.pm)に以下の記述があるけど、

use Catalyst qw/
	Session::PerUser
/;

僕の環境では効いてないっぽい。
Session管理はお好みで。今回はStore::FileとState::Cookieを使うことにした。

configでは、AuthenticationのcredentialクラスにTwitterを指定(2)。CAC::Twitterは、StoreにDBIx::Classを使うみたいなので、それを指定(3)。ユーザー情報を格納するテーブル名はtwitteruserとした(4)。
CAC::Twitterでは、認証処理時、テーブルにデータがないと認証エラーになるので、初回ログイン時にテーブルにデータを作ってやる必要がある。そのオプションを有効にする(5)。
consumer_keyとconsumer_secretは、1.Twitterにアプリケーションを登録の手順で取得した値を入れる(6)。
callback_urlには、認証処理をするアクションを指定する(7)。

3.Root.pmの編集

ログインURLへアクセスするアクションと、認証処理をするアクションを書く。ほぼCAC::Twitterにあるサンプル通り。

sub login : Local {
   my ($self, $c) = @_;
   
   my $realm = $c->get_auth_realm('twitter');
   $c->res->redirect( $realm->credential->authenticate_twitter_url($c) ); # (1)
}

Twitterの認証URLにリダイレクト(1)。

sub callback : Local {
   my ($self, $c) = @_;
   
   if (my $user = $c->authenticate(undef,'twitter')) { # (1) 認証処理
       $c->res->redirect("/"); # (2) 認証成功したら / にリダイレクト
       }
       else {
            die 'login error.'; # (3) 認証失敗したらdie。
       }
}

認証処理を実施して(1)、成功していたら / にリダイレクトする(2)。失敗したらdie(3)。auto_create_userを有効にしているので、callbackに来てから認証に失敗するのは例外でいい。

ユーザー情報テーブルの作成

ユーザー情報を格納するテーブルを作成する。テーブル名は、MyApp.pmで指定した"twitteruser"。

CREATE TABLE IF NOT EXISTS `twitteruser` (
  `id_field` int(11) NOT NULL AUTO_INCREMENT,
  `twitter_user` varchar(100) DEFAULT NULL,
  `twitter_user_id` int(11) NOT NULL,
  `twitter_access_token` varchar(100) DEFAULT NULL,
  `twitter_access_token_secret` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`id_field`)
) ENGINE=MyISAM  DEFAULT CHARSET=utf8 AUTO_INCREMENT=0 ;

5項目すべてが必要。ひとつでも無いと認証でエラーになる。
id_fieldの名前は変更可能で、別の名前を使いたければ、MyApp.pmのコンフィグで指定してやればいい。他の項目名は変更できない。
id_fieldがこのテーブルのIDになる。自動付番(AUTO_INCREMENT)を有効にしておく。

項目名 入るデータ
twitter_user twitterユーザー名 hsksyusk
twitter_user_id twitterユーザーID 3705551
twitter_access_token -<文字列> 3705551-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
twitter_access_token_secret <文字列> xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

twitterユーザー名は変更可能なので、IDでユーザーを判別するようにする。
ためしに自分で登録したところ、twitter_access_tokenは50桁、twitter_access_token_secretは43桁だった。固定かどうかわからないので100桁とっておく。twitter_user_idはintで間に合うか不安。

モデルクラスの作成

テーブルを作ったら、そこにアクセスするモデルクラスを作る。モデルクラスは、Catalyst::Model::DBIC::Schemaを使って、動的に生成する。
この記事を参考に。
初めてのCatalyst入門(7) モデルを使ったプログラミング (1/5):CodeZine(コードジン)
モデルを作成するときのコマンドは、僕の環境だとこう。

$ ./MyApp/script/myapp_create.pl model MyDB DBIC::Schema MyApp::Schema create=static dbi:mysql:database=MyDB:host=mysql231.db.sakura.ne.jp <username> <password>

auto_createメソッドの追加

configでauto_create_userを有効にしているので、認証時にユーザー情報が無いとき、MyApp::Schema::ResultSet::Twitteruserのauto_createメソッドが呼ばれる。このメソッドは自分で用意する必要がある。
MyApp/lib/MyApp/SchemaにResultSetディレクトリを掘って、そこにTwitteruser.pmを作る。
MyApp/lib/MyApp/Schema/ResultSet/Twitteruser.pm

package MyApp::Schema::ResultSet::Twitteruser;
use strict;
use warnings;
use base qw/DBIx::Class::ResultSet/;
 
sub auto_create { # (1) auto_createメソッドを作る。
    my ( $class, $hashref, $c ) = @_;
    my $member = $class->create({
        twitter_user_id    => $hashref->{twitter_user_id}, # (2) twitterユーザーIDを登録する。
    });
    return $member;
}

1;

Twitteruser.pmの中にauto_createメソッドを作る(1)。twitterユーザーIDを持たせて、twitteruserテーブルに1レコード追加する。id_fieldは自動付番される。残りの項目はレコード作成時はNULLで、認証処理の中でUPDATEされる。

MyApp/lib/MyApp/Schema/Result/Twitteruser.pmの最後のほうに、1行追加する。
MyApp/lib/MyApp/Schema/Result/Twitteruser.pm

# Created by DBIx::Class::Schema::Loader v0.07002 @ 2011-01-21 19:00:34
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:MSuvL29yw4VVYWN2sNL20w

__PACKAGE__->resultset_class('MyApp::Schema::ResultSet::Twitteruser'); # (1) 追加行

# You can replace this text with custom content, and it will be preserved on reg

(1)の行を追加することで、ResultSet::Twitteruserにアクセスできるようになる。
メソッドの追加のしかたは、こちらの記事を参考にした。
第38回 DBIx::Class:拡張性の高さが売りではありますが:モダンPerlの世界へようこそ|gihyo.jp … 技術評論社

テスト

テスト用にRoot.pmにアクションを追加する。

sub index :Path :Args(0) {
    my ( $self, $c ) = @_;

    if ( $c->user_exists) { # (1) ログイン状態を判断
        $c->response->body( $c->user->get('twitter_user').' is logged in.' ); # (2) ログインユーザーのTwitterユーザー名を取得
        } else {
        $c->response->body( 'not login.' );
    }
}

sub logout :Local { # (3) ログアウト処理を実装
  my ( $self, $c ) = @_;
  $c->logout();
  $c->response->redirect($c->uri_for('/'));
}

callbackで認証後、/ に飛ばされるので、そこでログイン状態を判断して、表示メッセージを切り替える処理を入れる(1)。ログインしていれば、ユーザー名を取ってきて表示する(2)。ログアウト処理も作っておく(3)。

以下、テスト手順。

  1. http://<アプリのベース>/login にアクセス。
  2. https://api.twitter.com/oauth/authorize?oauth_token=xxxxxxxにリダイレクトされる。Twitterに登録したアプリケーション名が表示される。「許可する」ボタンを押す。
  3. http://<アプリのベース>/にリダイレクトされる。"<ユーザー名> is logged in."と表示される。
    1. リロードなどしても、"<ユーザー名> is logged in."と表示される。
    2. DBのtwitteruserテーブルを確認すると、ログインしたtwitterユーザーの情報が登録されている。
  4. http://<アプリのベース>/logout にアクセス。
  5. http://<アプリのベース>/にリダイレクトされる。"not login."と表示される。
    1. リロードなどしても、"not login."と表示される。

ここまで動けばOKです。あとは各ページでユーザーIDやらユーザー名やらを取得して、よろしくやってください。