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