おどおどしつつCatalystをモバイルサイト開発に導入してみた:Catalyst機能拡張編

おっと、前回からかなり日が空いてしまった・・まだまだちまちま書いていきますよ。


再びCatalyst関連のドキュメントに戻り、主要なモジュール・プラグインについて習得を進めていきました。

  • セッション
  • 認証
  • FillInForm
  • その他のモジュール・プラグイン

セッション

さて、Catalystチュートリアルを読み進めて行った訳なんだけれども、

これが有用なことを書いてそうなんだけど、何を言おうとしているのかさっぱり理解できない。

The Authentication plugin supports Authentication while the Session plugins are required to maintain state across multiple HTTP requests.

どうも、Sessionというプラグインの知識を前提に話が進んでるっぽいが、これ自体を理解する必要がありそう。


という訳で、Sessionプラグインのドキュメントを読む。

とくに、付属してるチュートリアルから入って行ったほうが分かりやすかったです。

読むと、Sessionプラグインは、StateとStorageという2種類の関連プラグインから構成されることが判明。それぞれのプラグインを選択導入することによって、

Stateプラグイン
セッションの状態(要はID)を保持する方式(Cookieか、URI付加か)を決定する。
Storageプラグイン
セッションの保存方式(ファイルに保存するか、DBに保存するか、など)を決定する。


上記の方式を決めることができます。


さて、モバイルでは、


iモードではCookieが使えない


という大変重要なことがあります。


なので、Catalyst::Plugin::Session::State::URIの利用自体が必須になりますが、HTML出力時にセッションIDを自動で付加してくれるので、それだけでも非常に有用です。従来は、テンプレートで

<a href="/hogehoge?sid=<TMPL_VAR name=sid>">link</a>

などと、セッションID部分を忘れずに含めていたのですが、手動で一生懸命セッションIDを埋めなきゃいけない時代はもう終わったということですね。。


さて、前述のiモードの制約について。従来であれば、この制約のせいで、「すべてのキャリアについて、セッションIDはURLに付加してつけ回す」という分別のあるエンジニアであればECサイトのようなサイトなどはとても怖くて構築できないような手法が横行していました。iモードはやむなしとしても、EZwebSoftbank、ひいては同システムでPCサイトを展開するような場合は極力Cookieを使いたいと考えていました。つまり、アプリケーション側で

  • iモードと判別された場合は、URL付加方式でセッションを維持する
  • それ以外の場合は、Cookieでセッションを維持する

といったことをしたいと常々考えていました。ところが、プラグイン方式だと、アプリケーションロード時に決まることになるので、アプリケーション自体でどちらかに統一せざるを得ないと考え、何とかする方法はないものかと悩みに悩みました。


もしかしたら、「自前でCatalyst::Plugin::Session::Stateのプラグインを開発する必要があるのかも」と考えました。そうなるとひと苦労です。


そこでソースコードをひっくり返し、ドキュメントをひっくり返していたら次のような一文が。

session_should_rewrite


This method is consulted by finalize. The body will be rewritten only if it returns a true value.


This method will not return true unless $c->config->{session}{rewrite} is true (the default). To globally disable rewriting simply set this parameter to false.


If $c->config->{session}{no_rewrite_if_cookie} is true (the default), Catalyst::Plugin::Session::State::Cookie is also in use, and the user agent sent a cookie for the sesion then this method will return false.


http://search.cpan.org/~nuffin/Catalyst-Plugin-Session-State-URI-0.07/lib/Catalyst/Plugin/Session/State/URI.pm#METHODS

  • session_should_rewriteが真を返す場合のみレスポンス書き換えが行われる。
  • session_should_rewriteは$c->config->{session}{rewrite}が偽の場合は偽を返す(デフォルトは真)
  • $c->config->{session}{no_rewrite_if_cookie}が真(デフォルト)でありかつ、Catalyst::Plugin::Session::State::Cookieは合わせて利用可能であり、セッションにuaがクッキーを送信してくる場合、このメソッドは偽を返す。

ちょっと分かりにくいんですが、まとめると


『デフォルト設定で、Catalyst::Plugin::Session::State::CookieCatalyst::Plugin::Session::State::URIを一度に使うと、セッションにuaがクッキーを送信してきたら、URI書き換えは行われない。』


ということになります。実際にFirefoxWeb Developerアドオンなどで試してみると、ちゃんとそうなることが確認できます。これはめちゃくちゃ便利!

モバイルであれば、デフォルトのまま両方のプラグインを導入すれば、

  • DoCoMoとかのクッキーが使えない(カスな)端末ではURI書き換えでセッションIDを持ちまわす
  • auやPCの端末ではCookieでセッションIDやりとりするので、URI書き換えは行われない

となりますね。


これは使わない理由がない。

認証

セッションの知識を得てからチュートリアルを読み直すと、内容が理解できてきた。

要は、いったん認証が成功すると、その結果をセッションに自動的に保存しておいてくれて、

my $ud = $c->user if ($c->user_exists);

みたいにすれば、認証済みかということや認証に該当したユーザーデータを簡潔に参照できるということらしい。これは便利そう。

だけど概念的な部分がちょっとややこしく、これを理解しないと適切に設定できないぽい。。という訳で、がっつり本体のドキュメントも読んでみる。

まとめると*1

  • 認証はレルム(Realms)という単位で管理される
  • レルムはストア(Store)と証明書(Credential)で構成される。
  • ストアは対象ユーザーの情報とユーザーに関連する任意の量のデータを保存する単位。
  • 証明書はストアからの情報を使ってどうやってユーザーの確認を行うかを決定する設定。

やはり概念がちょっととっつきにくいんだけど、通常みたくユーザーデータがusersというテーブルに保存されていて、パスワードで認証されるんなら、

  • ストア→usersに対応するDBICスキーマオブジェクトデータ
  • 証明書→パスワードフィールドと、どう認証するか

を記述することになる。


パスワードフィールドは、いざというときデクリプトもしたいので、Crypt::CBCを使いたいと考えていました。Catalyst::Authentication::Credential::Passwordのpassword_typeで、暗号化もしくはダイジェスト方式を指定できるんだけど、Crypt::CBCでは対応できない。


ここも悩んだんだけど、password_typeを見返すと、self_checkというのがあった。

self_check

This option indicates that the password should be passed to the check_password() routine on the user object returned from the store.

ストアにcheck_password()って関数実装しろって書いてある。なので、MyApp::Schema::Usersに、以下のように関数を実装。

sub check_password {
    my ( $self, $password ) = @_;
    return ( decrypt_password( $self->password ) eq $password );
}

これで想定どおりに動作!

サブスクライバIDなど、端末個体IDによる認証、いわゆる「簡単ログイン」については、ユニークキーでユーザーデータを特定してパスワードで認証する、というプロセスからは外れるので、そのままこの認証機構を使うことはできません。なので、以下のようなロジックになります。

  1. 取得された端末個体IDからユーザーデータを抽出。1端末複数ユーザー可能なら、簡単ログイン設定済みフラグが有効であるという条件も付加する。
  2. 抽出されたユーザーデータから、対応するユニークキーとパスワードデータを取得しなおして、認証

↓こんな感じになりますね。

sub auth_easy {
    my ( $self, $c, $subscriber_id ) = @_;

    # 簡単ログインが有効なユーザーがあった場合のみ試行
    my $rs_u = $c->model('DB::Users')->search(
        {   subscriber_id       => $subscriber_id,
            easylogin_available => 1,
            status_id           => 3   # 会員登録中
        }
    );

    return unless ( my $ud = $rs_u->first ); # 該当ユーザーなし→undef

    return $c->authenticate({
        login_id => $ud->login_id,
        password => decrypt_password( $ud->password )
    });
}

FillInForm

Catalystについての情報を漁っていると、Catalyst::Plugin::FillInFormを使うのが常套になっていることを知った。ので、このドキュメントについても読破する。

調べてみると、HTML::FillInFormが元になってるからこれ見れと書いてある。そもそもWebアプリケーションの世界では常識的なモジュールということか。

という訳で恥ずかしながら初導入。…これは便利すぎる!!既存データからロードとか表示、とかも極めて簡単に実装できるしね。

設定ファイル

マニュアルによると、Catalystでは、設定ファイルのロードはCatalyst::Plugin::ConfigLoaderが担っているとのこと。以前は設定ファイルはYAML形式だったが、ApacheのConfigファイルの形で記述するものらしい。


なんでそうなったのか経緯はよく分からなかったんだけど、モノの本やらITProの記事くらいの前の記事だと、この辺りには触れられていないようで、元になっているConfig::Generalのドキュメントも読んでおく必要があった。


YAMLでできなかったことで便利だーと感じたことといえば、おそらくファイルのINCLUDEじゃなかろうか。Config::Generalなら、INCLUDEの構文が使えるので、ファイルが分割でき、保守がかなり楽になる。


この他にもコンストラクタオプションがかなり多様にあるみたいで、状況に応じて柔軟に使えそう。


一方で、YAMLより不便だなーと思ったこと。要素が一個以下の配列のリファレンスを表現できない?

例えば、

#!/usr/bin/perl

use strict;
use warnings;

use Config::General;

my %hash = (
    Somewhat => {
        Flower    => 'Rose',
        Fruit     => [qw/Melon Grape Strawberry/],
        Vegetable => 'Piment'
    }
);

print new Config::General->new( \%hash )->save_string, "\n";

こんな例の場合、

<Somewhat>
    Flower   Rose
    Fruit   Melon
    Fruit   Grape
    Fruit   Strawberry
    Vegetable   Piment
</Somewhat>

こういう風に記述できるんだけど、

#!/usr/bin/perl

use strict;
use warnings;

use Config::General;

my %hash = (
    Somewhat => {
        Flower    => 'Rose',
        Fruit     => [qw/Melon/],
        Vegetable => 'Piment'
    }
);

print new Config::General->new( \%hash )->save_string, "\n";

こんな場合は、

<Somewhat>
    Flower   Rose
    Fruit   Melon
    Vegetable   Piment
</Somewhat>

こうなってしまい、

my %hash = (
    Somewhat => {
        Flower    => 'Rose',
        Fruit     => 'Melon',
        Vegetable => 'Piment'
    }
);

これと区別できない。


もちろんアプリケーションの側で都度解釈を記述できればいいんだけど、でもFormValidator::Simpleの設定なんかを静的に書きたいときなんかはどうしても要素1個の配列リファレンスとか出てくる。


なにかオプショナルな記述方法があるのかしら・・マニュアル読んでも不明なまま。。


YAMLだと適切に書けるんだけどなぁ。なので、都度YAMLで記述する設定も共存することに。

その他のモジュール・プラグイン


以下のプラグインも目を通しました。これらは大して複雑でもなかったかしら?

Catalyst::Plugin::Static::Simple
(特に開発環境用に)静的コンテンツをアプリケーション越しに配信する
Catalyst::Plugin::CustomErrorMessage
アプリケーションエラー画面をカスタマイズして出力する


他にも何かあったかもしれないけど忘れた…一通り書き終わったら細々メモ程度に残しておくことにします。

*1:訳語の割り当てはピンと来なかったので独自のものです。