日向夏特殊応援部隊

俺様向けメモ

Catalyst/DBICでDigest認証する

自分の為のメモですよ。

準備

まずはCatalystプロジェクトを作ります
$ mkdir -p /path/to/dir
$ cd /path/to/dir
$ catalyst.pl AuthSample
ユーザー用のDBICスキーマを定義します。
$ module-starter --module AuthSample::Schema
$ cd AuthSample-Schema

でこのディレクトリにて、

CREATE TABLE user (
	user_seq INTEGER PRIMARY KEY,
	user_id TEXT UNIQUE,
	password TEXT,
	created_on DATETIME,
	updated_on DATETIME
);

こんなスキーマを定義して、schema.sqlとして保存して、

$ sqlite3 -init schema.sql authsample.db

として初期化する。

次にスキーマ生成の為の簡易スクリプトtypesterさんの奴をベースに ./script/schema.pl として作る。

#!/usr/bin/perl

use strict;
use warnings;

use FindBin;
use File::Spec;
use lib (
    File::Spec->catfile( $FindBin::Bin, qw/.. lib/ ),
    File::Spec->catfile( $FindBin::Bin, qw/.. schema/ )
);

use DBIx::Class::Schema::Loader qw(make_schema_at);

die('Required arguments dsn dbuser dbpass') unless (@ARGV);

my $schema_class = 'AuthSample::Schema';

unlink(
    glob(
        File::Spec->catdir( $FindBin::Bin, '..', 'lib',
            split( /::/, $schema_class ) )
            . '/*.pm'
    )
);

make_schema_at(
    $schema_class,
    {   components => [
            qw/ResultSetManager UTF8Columns InflateColumn::DateTime TimeStamp DigestColumns/
        ],
        dump_directory => File::Spec->catfile( $FindBin::Bin, qw/.. lib/ ),
        debug          => 1,
        really_erase_my_files => 0,
    },
    \@ARGV,
);

こんな感じ。改良点は、

  1. 各テーブルクラスは自分で自ら消す
  2. really_erase_my_files を 0 にしてるので Schema::Loader 自身は何も消さない
    1. つまり Schema クラスは残ったままで、自由に編集出来る

みたいな点。詳しくはCatalystConで話す。


実行権限を与えて、さらに事前にmodule-starterでSchemaクラスが出来ちゃってるからそれを消してからschema.plを実行する。

$ chmod +x ./script/schema.pl
$ rm -f ./lib/AuthSample/Schema.pm
$ ./script/schema.pl dbi:SQLite:dbname=authsample.db

これでひな形は完成。

DBIC関連を今回は手動でちょっと弄る
*** lib/AuthSample/Schema/User.pm.orig	2008-04-15 11:49:27.000000000 +0900
--- lib/AuthSample/Schema/User.pm	2008-04-15 11:54:33.000000000 +0900
***************
*** 5,10 ****
--- 5,13 ----
  
  use base 'DBIx::Class';
  
+ __PACKAGE__->mk_classdata('digest_user_name_column');
+ __PACKAGE__->mk_classdata('digest_realm' => '');
+ 
  __PACKAGE__->load_components(
    "ResultSetManager",
    "UTF8Columns",
***************
*** 20,34 ****
    "user_id",
    { data_type => "TEXT", is_nullable => 0, size => undef },
    "password",
!   { data_type => "TEXT", is_nullable => 0, size => undef },
    "created_on",
!   { data_type => "DATETIME", is_nullable => 0, size => undef },
    "updated_on",
!   { data_type => "DATETIME", is_nullable => 0, size => undef },
  );
  __PACKAGE__->set_primary_key("user_seq");
  __PACKAGE__->add_unique_constraint("user_id_unique", ["user_id"]);
  
  
  # Created by DBIx::Class::Schema::Loader v0.04004 @ 2008-04-15 11:39:32
  # DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:7O29VfwnVCsyZL2avThORA
--- 23,61 ----
    "user_id",
    { data_type => "TEXT", is_nullable => 0, size => undef },
    "password",
!   { data_type => "TEXT", is_nullable => 0, size => undef, digest_check_method => 'check_password' },
    "created_on",
!   { data_type => "DATETIME", is_nullable => 0, size => undef, set_on_create => 1, },
    "updated_on",
!   { data_type => "DATETIME", is_nullable => 0, size => undef, set_on_create => 1, set_on_update => 1 },
  );
  __PACKAGE__->set_primary_key("user_seq");
  __PACKAGE__->add_unique_constraint("user_id_unique", ["user_id"]);
  
+ __PACKAGE__->digestcolumns(
+     columns => [qw/password/],
+     algorithm => 'MD5',
+     encoding => 'hex',
+     auto => 1,
+     dirty => 1,
+ );
+ 
+ __PACKAGE__->digest_user_name_column('user_id');
+ __PACKAGE__->digest_realm('Are you TKSK?');
+ 
+ sub _get_digest_string {
+     my ($self, $value) = @_;
+ 
+     $self->digest_maker->reset;
+ 
+     return $self->next::method(
+         join(':',
+              $self->get_column($self->digest_user_name_column) || '',
+              $self->digest_realm,
+              $value || ''
+          )
+     );
+ }
  
  # Created by DBIx::Class::Schema::Loader v0.04004 @ 2008-04-15 11:39:32
  # DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:7O29VfwnVCsyZL2avThORA

主な変更点は、

  1. DigestColumnsを使ってDigest認証用のハッシュ値を格納するようにした
  2. TimeStampが勝手に挿入されるようにした

って感じ。

と言う訳で試してみる。

#!/usr/bin/perl

use strict;
use warnings;

use lib qw(lib);
use AuthSample::Schema;

my $schema = AuthSample::Schema->connect('dbi:SQLite:dbname=authsample.db');
$schema->resultset('User')->create({ user_id => 'zigorou', password => 'hogehoge' });

print $schema->find({ user_id => 'zigorou' })->password;

とすると、

c11485a6cebc09f30a61df78a33961df

などと出るので、どうやら問題なく動作している模様。念のためSQLiteコンソールでも確かめる。

sqlite> SELECT * FROM user;
1|zigorou|c11485a6cebc09f30a61df78a33961df|2008-04-15 02:59:12|2008-04-15 02:59:12

これでDBICはとりあえず出来た。

Catalyst側の実装とか設定とか

まずは最初にauthsample_server.plにモジュールのパスを追加しておきます。

use lib (
	"$FindBin::Bin/../lib",
	glob("$FindBin::Bin/../../*/lib")
);

こういう感じ。
今度はCatalyst側に移動して、./lib/AuthSample.pmの use Catalyst してる部分を次のように。

use Catalyst qw/
  -Debug 
  ConfigLoader 
  Static::Simple
  Cache
  Authentication
  Authentication::Store::DBIC
  Authentication::Credential::HTTP
/;

次にCatalyst::Model::AdaptorでDBIC::Schemaのadaptorを作ります。

$ ./script/authsample_create.pl model DBIC::Schema Adaptor AuthSample::Schema

出来上がったadaptorにprepare_arguments, mangle_argumentsを追加します。Catalyst::Utilsをuseしておく必要があります。
ついでにconfigのconstructorもnewからconnectに変更する。

__PACKAGE__->config( 
    class       => 'AuthSample::Schema',
    constructor => 'connect',
);

sub prepare_arguments {
    my ($self, $app) = @_;
    return $app->config->{Catalyst::Utils::class2classsuffix(__PACKAGE__)};
}

sub mangle_arguments {
    my ($self, $args) = @_;
    return $args ? @$args : ();
}

さらにAuthentication::Store::DBIC用にUserクラスを作ります。

$ ./script/authsample_create.pl model DBIC::Schema::User

生成されたModel::DBIC::Schema::UserにACCEPT_CONTEXT()を次のように追加する。

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

    return $c->model('DBIC::Schema')->resultset('User');
}

さらにyamlを次のように設定する。

------
Model::DBIC::Schema:
  - dbi:SQLite:dbname=/Users/zigorou/tmp/digestauth/AuthSample-Schema/authsample.db
authentication:
  dbic:
    password_field: password
    password_hash_type: MD5
    password_type: clear
    user_class: DBIC::Schema::User
    user_field: user_id
  http:
    algorithm: MD5
    type: digest
cache:
  backend:
	class: Cache::Memory
	default_expire: 600 sec
	namespace: test
name: AuthSample

これで準備完了です。

$ ./script/authsample_server.pl -d -r

とかで動くはず。

認証ページを作る

面倒なのでController/Root.pmに仕込みます。

sub default : Private {
    my ( $self, $c ) = @_;

    $c->authorization_required( realm => 'Are you TKSK?' );

    # Hello World
    $c->response->body( $c->welcome_message );
}

これで http://localhost:3000/ にアクセスすれば認証ページが出るはずです。
おしまい。