Flickr-Likeな認証をこなすWebService::Simple::Signature作ったよ

前に,Flickr用のモジュールとしてWebService::Simple::Flickrを作りましたが, 昨日調べてたらRemember The Milkもほぼ同様のAPIを使っていることが わかりました(というかRTMがパクっただけw?).そこで,Flickrの様な 認証形式に汎用的に使える様にモジュールを書き直して,WebService::Simple::Signature という名前のモジュールを新たに作りました.再度,Flickrを例にして使い方を紹介. ソースはいつもどおりgithubにあげています.

また,WebService::Simpleのpostメソッドがどうも実装が気に食わないというか, getと同じ引数で使えないのが嫌だったので,使えるように書き直したWebService::Simpleも 同時に上げています.WebService::Simple::Signatureは僕が手を入れたバージョンの WebService::Simpleでないとpostがエラー起こすので注意して下さい.

WebService::Simple

書き換えたpostメソッドのコードを上げておきます.基本的にgetの中身を パクってきて,query URLの中にパラメータを入れないようにしてます. また,ファイルなどがあった場合,どうやらパスを配列にしないといけないらしいので パラメータの与え方がすこし変則です.

$flickr->post( 'rest/',
    { title => 'title',
      file => { photo => '/home/you/image.jpg' }
     } );

getの引数と全く同じで動作するので,動いてるやつのgetをpostに書き換えるだけで postになります.

WebServise/Simple.pm

...
sub post {
    my $self = shift;
    my ( $url, %extra );

    if ( ref $_[0] eq 'HASH' ) {
        $url = "";
        %extra = %{ shift @_ };
    }
    else {
        $url = shift @_;
        if ( ref $_[0] eq 'HASH' ) {
            %extra = %{ shift @_ };
        }
    }

    my $uri = $self->request_url(
        url => $self->base_url,
        extra_path => $url,
    );

    warn "Request URL is $uri\n" if $self->{debug};

    my @headers = @_;

    if(defined $extra{file}){
        my %file = %{$extra{file}};
        delete $extra{file};
        foreach my $key(keys(%file)){
            $file{$key} = [$file{$key}] if ref $file{$key} ne "ARRAY";
        }
        %extra = (%file, %extra);
    }

    my $response;
    $response = $self->SUPER::post( $uri, { %{ $self->basic_params }, %extra }, @headers );
    if ( !$response->is_success ) {
        Carp::croak( "request to $url failed: " . $response->status_line );
    }

    $response = WebService::Simple::Response->new_from_response(
        response => $response,
        parser => $self->response_parser
    );
    return $response;
}
...

WebService::Simple::Signature

上のパッチを当てたWebService::Simpleを使って(といってもpost以外は同じですが), 例の署名付き認証を行うモジュールです.コンストラクタの引数の与え方は こんな感じです.

my $flickr = WebService::Simple::Signature->new(
    base_url => 'http://api.flickr.com/services/',
    sig => {api_sig => $secret_key},
    params => {api_key => $api_key, auth_token => $token}
    );

api_keyやauth_tokenといった,ハッシュのキーの部分はそのままAPIに渡される時の 名前になります.この2つは普通にparamsに入れてしまってOK.また,secretキーの api_sigの部分は署名のパラメータ名でそのまま使われます.ちょっと直感と ずれるので注意して下さい.

tokenさえ取得していれば,あとは普通にWebService::Simpleの様に使えます. tokenがなければ認証無しの普通のAPIを叩くだけです.いずれもapi_keyは必須です.

$ref = $flickr->get('rest/', {method => 'flickr.test.echo', name => 'value'});
print Dumper $ref->parse_response;

まだやってませんが,URLの部分さえ調整すれば,RTMでも使えるはずです.

tokenの取得方法(デスクトップアプリの場合)

前回紹介した方法と基本的に同じです.下のスクリプトを適当に参考にして下さい. 一応,PODにも書いておきましたw

use WebService::Simple::Signature;
use Data::Dumper;

my $api_key = '*****************';
my $secret_key = '***********';

my $flickr = WebService::Simple::Signature->new(
    base_url => 'http://api.flickr.com/services/',
    sig => {api_sig => $secret_key},
    params => {api_key => $api_key},
    );

my $ref;
my $ref = $flickr->get('rest/', {method => 'flickr.auth.getFrob'});
my $frob = $ref->parse_response->{frob};
print $frob . "\n";
print $flickr->request_auth_url('auth/', {perms => 'write', frob => $frob});

my $line =<stdin>;
$ref = $flickr->get('rest/', {method => 'flickr.auth.getToken', 'frob' =>$frob});
my $token = $ref->parse_response->{auth}->{token};
print $token . "\n";

$flickr->{basic_params}->{auth_token} = $token;

$ref = $flickr->get('rest/', {method => 'flickr.photosets.getList'});
print Dumper $ref->parse_response;

これで,プライベートにしてるsetも表示されてることが確認できます.

WebService/Simple/Signature.pm

package WebService::Simple::Signature;

use Digest::MD5 qw(md5_hex);
use base qw(WebService::Simple);

sub new {
    my $class    = shift;
    my %args     = @_;
    my $sig = delete $args{sig} || "";

    my $self = $class->SUPER::new(%args);

    my ($sig_name, $secret) = each(%{$sig});
    $self->{sig_name} = $sig_name;
    $self->{secret} = $secret;
    return $self;
}

sub sign_args {
    my ($self, $args) = @_;

    my $sig  = $self->{secret};
    foreach my $key (sort {$a cmp $b} keys %{$args}) {
        my $value = (defined($args->{$key})) ? $args->{$key} : "";
        $sig .= $key . $value;
    }
    warn "sig=" . $sig . "\n" if $self->{debug};

    return md5_hex($sig);
}

sub set_signature {
    my ($self, %args) = @_;

    my %sig_args = (%{$self->{basic_params}}, %args);
    if(defined $self->{secret} && length $self->{secret}){
        my $sig = $self->sign_args(\%sig_args);
        $args{$self->{sig_name}} = $sig;
    }
    return %args;
}

sub get {
    my $self = shift;
    my ( $url, %extra );

    if ( ref $_[0] eq 'HASH' ) {
        $url   = "";
        %extra = %{ shift @_ };
    }
    else {
        $url = shift @_;
        if ( ref $_[0] eq 'HASH' ) {
            %extra = %{ shift @_ };
        }
    }

    my @headers = @_;
    my %params = $self->set_signature(%extra);
    return $self->SUPER::get($url, {%params}, @headers);
}

sub post {
    my $self = shift;
    my ( $url, %extra );

    if ( ref $_[0] eq 'HASH' ) {
        $url   = "";
        %extra = %{ shift @_ };
    }
    else {
        $url = shift @_;
        if ( ref $_[0] eq 'HASH' ) {
            %extra = %{ shift @_ };
        }
    }

    my @headers = @_;
    my %params;
    if(defined $extra{file}){
        my %file = %{$extra{file}};
        delete $extra{file};
        %params = (file => {%file}, $self->set_signature(%extra));
    }else{
        %params = $self->set_signature(%extra);
    }
    return $self->SUPER::post($url, {%params}, @headers);
}

sub request_auth_url {
    my $self = shift;
    my ( $url, %extra );

    if ( ref $_[0] eq 'HASH' ) {
        $url   = "";
        %extra = %{ shift @_ };
    }
    else {
        $url = shift @_;
        if ( ref $_[0] eq 'HASH' ) {
            %extra = %{ shift @_ };
        }
    }

    $extra{api_key} = $self->{api_key};
    if(defined $self->{api_secret} && length $self->{api_secret}){
        $extra{api_sig} = $self->sign_args(\%extra);
    }
    my $uri = $self->request_url(
        url        => $self->base_url,
        extra_path => $url,
        params     => {%extra}
    );

    warn "Request URL is $uri\n" if $self->{debug};

    return $uri;
}

1;

あとがき

というわけで,Flickrタイプの認証ならさくさく対応できるようになったはずです. コンセプトとしては,なるべく引数でチューニングするようにしつつ, WebService::Simpleの機能をなるべく存分に活用しました.いやー,使いやすい.

ところで,Flickrのこのタイプの認証ってスタンダードになるんですかね? はてなが似た様なのを使ってたりしますので一応そちらも意識して,パラメータの 名前とかもモジュール内部では固定せずに,引数で与えるハッシュのキー名を 利用しています.

あとは,Flickr用に皮のモジュール作れば簡単に使えるようになると思いますが, まぁそれはお好きにして下さい.正直汎用的に作ると皮まで作る気がなくなったりしますw