Mojoliciousスタートアップ - Perlによる新規Web開発

  1. Mojoliciousスタートアップ
  2. here

ログイン機能を使ったユーザー認証の作り方 - 会員登録機能の実装

ログイン機能を使ったユーザー認証の作り方について書きます。Webアプリケーションで会員登録の機能を使って、ユーザー認証をするのって、難しそうだなと感じていませんか。どうやって会員登録機能とログイン機能を作ったらよいかわからない、そういう方の疑問を解決できる記事を書きます。

記事を書いた後に、Webサービスのスタートアップに必要な、MojoliciousとMySQLにおける会員登録機能とログイン機能を使った、ユーザー認証のサンプルコードを書いてみますね。

この記事を読めば、セキュリティの専門家から見ても、だいじょうぶといえるような、セキュリティ的にも安心の会員登録とログイン機能を、手順を覚えれば実装できるようになります。

(この記事は、2019年におけるもっともよいやり方を試行錯誤しながら書いていますので、検証が必要です。)

(記事執筆途中。追加、修正あり。実際の全体の検証は未。最終更新2019年11月14日)

ログイン機能を使ったユーザー認証とは何か

ログイン機能を使ったユーザー認証とは何かについて解説していきます。

ユーザー認証とは何か

ユーザー認証とは、ユーザーを識別する機能のことを言います。木本さんが操作している、田中さんが操作しているなど、どのユーザーが今操作しているかを識別するための機能です。

ログイン機能とは何か

一般的に、Webアプリケーションでは、ユーザーIDとパスワード、あるいは、メールアドレスとパスワードを指定して、ログイン機能を使って、ユーザー認証を実装します。この記事では、この機能のことをログイン機能と呼びます。

ユーザー認証には、ベーシック認証や、OAuthなどの別の認証方式もあるので、アプリケーションで実装するユーザー認証機能をログイン機能という名前で呼ぶことにします。

広くよく見られるログイン画面からログインする機能のことです。

ユーザーID

ユーザーIDは「kimoto_yuki01」のように英数字の場合もあれば「0012345」のように数字だけのものもあります。どちらの場合も同じロジックで処理できます。

英数字の場合はASCIIコードの「a-zA-Z0-9_」だけに限定しておくようにすればよいかと思います。

ユーザーIDは、ユーザーが決めることができるようにすることもできますし、サービス提供側が決めることができるようにすることもできます。

ユーザーIDまたは、メールアドレスは、一意であることが必要です。ユニーク制約を設定します。

ユーザーIDを使ってユーザー認証を行う場合は、ユーザーのメールアドレスの変更を行うことができるのは、当然ですが、メールアドレスを使ってユーザー認証を作った場合でも、メールアドレスの変更は可能ですので、安心してください。ユーザーテーブルを作るときに、行を一意に識別できるIDの列を作成して、プライマリーキー制約とオートインクリメントを設定します。

パスワードの安全性

パスワードは、ASCIIコードの見ることが可能な文字で表現します。「a-zA-Z0-9」とASCIIの記号「&@」など。キーボードで打てる文字で、文字の制限を行うと考えてください。

セキュリティを高めるために、文字数は、8文字以上と制約を書けると安全です。パスワードは長ければ長いほど安全ですが、利便性もあるので、安全性について、パスワードを決める画面で、ユーザーが知ることができると、親切です。

英語と数字と記号のすべてが入っているというのも、少しだけ安全性を上げるでしょう。

パスワードは、ユーザーテーブルに、そのまま保存しては、絶対にいけません。ユーザーデータが流出すると、そのパスワードをそのまま入力することで、他のサービスにログインできる危険性があるからです。そんなことは、当然だと、思うかもしれませんが、僕はパスワードが生で保存されているテーブルを実際に目にしてきました。

パスワードは、ハッシュ関数を使って、その値を保存します。ハッシュ関数を使うと、元のパスワードが、どんな文字列であったのかを、知ることができません。もう少し、正確に言うと、知ることがほぼ不可能になります。ソルトと呼ばれる値を、付け加えておくと、より安全性が高まります。これについては、後で、詳しく解説します。

HTTP通信とユーザー認証

Webの標準的なプロトコルであるHTTP通信は、ステートレスであることが前提です。ステートレスとは、状態を持たないことで、HTTPリクエストがあって、HTTPレスポンスを返すというのがひとつの処理で、それぞれのHTTPリクエストが独立しています。

HTTP自体は、接続を持続するという機能を持っていません。(HTTP 2.0のことはこの記事では触れません)。

ベーシック認証や、ダイジェスト認証が定義されていますが、これは、HTTP通信上の認証なので、アプリケーションに統合することができません。

結局のところ、ユーザーにサービスを提供するためのユーザー認証は、アプリケーション側で、行う必要があるということです。

HTTPは、クッキーと呼ばれる、クライアント側にデータを保存する機能を持っています。このクッキーの機能をうまく使って、ユーザー認証を実装することになります。

ログインのタイミングで、セッションIDと呼ばれるものを、アプリケーション側で発行して、それを、クッキーに保存してもらいます。そして、クッキーに保存されたセッションIDを、送信してもらい、アプリケーション側で、ユーザーを識別します。

パスワードをどのように保存するか

ログイン機能を使ったユーザー認証を作る場合は、ユーザーがパスワードを入力します。このパスワードを盗まれても安全なように保存しておく必要があります。

パスワードそのままを保存した場合は、サーバーで作業している人は、データベースでSQLを実行して、ユーザーIDとパスワードをそのまま見ることができます。これは、セキュリティ上よろしくありません。

ですので、パスワードをそのまま保存するのではなく、パスワードにハッシュ関数を実行した値を保存しておくことになります。

ハッシュ関数とは

ハッシュ関数とは、ある値を与えたときに、ひとつの値を出力する関数です。

# ハッシュ関数
my $hash_value = hash_func($value);

「えっ、これがハッシュ関数」なのと言われると、これがハッシュ関数なのだというしかありません。すごく難しいと信じ込んでいた方は、びっくりされるでしょう。

ただし、良いハッシュ関数と悪いハッシュ関数があり、良いハッシュ関数を選ぶ必要があるのです。

良いといわれるハッシュ関数の条件は以下です。

  • ハッシュ値から元の値を推測されないこと
  • 異なる入力に対して、出力されるハッシュ値がなるべく重複しないこと

元の値を推測されるということは、パスワードが知られてしまうということです。パスワードが簡単に知られてしまうようでは、困りますね。ですので、良いハッシュ関数は、元の値を推測されにくいということが必要です。

パスワードの場合は、ハッシュ値が重複しないことは、あまり必要な要件ではありませんが、ハッシュ値を、ユニークIDとして扱いたい場合は、異なる入力に対して、出力されるハッシュ値がなるべく重複しないということが重要です。これは、後ほど、セッションIDを作成する上で、必要になりますので、頭に入れておいてください。

ちなみに、ハッシュ関数は、Perlのハッシュとは何の関係もありません。

bcryptでパスワードをハッシュ化する

2019年の調査の結果、パスワードをハッシュ化するには、bcryptが良いようです。PHP 7では、bcryptのアルゴリズムが、デフォルトで採用されています。

bcryptは、入力値が短くても(パスワードは短い)、元の値を推測するのに、ある程度の計算時間がかかるハッシュ関数のアルゴリズムのようです。

Perlにおけるbcryptの実装はCrypt::Eksblowfish::Bcryptで利用できます。

Mojoliciousでは、プラグインとしてMojolicious::Plugin::Bcryptがあります。

# 登録時にハッシュ化されたパスワードを作る
sub signup {
    my $c = shift;
    my $crypted_pass = $c->bcrypt( $self->param('password') );
    ...
}

# ログイン時にパスワードをチェック
sub login {
    my $self = shift;
    my $entered_pass = $self->param('password');
    my $crypted_pass = $self->get_password_from_db();
    if ( $c->bcrypt_validate( $entered_pass, $crypted_pass ) ) {
 
        # Authenticated
        ...;
    }
    else {
 
        # Wrong password
        ...;
    }
}

bcryptで生成されたハッシュ化されたパスワードを、ユーザーテーブルのパスワードに保存しましょう。

将来的にさらに強度の高いアルゴリズムがでてきた場合のために、bcryptを自作のpassword_hash関数で、bcrypt_validateを自作のpassword_validate関数で、ラッピングしておくと、保守性が高くなるかもしれませんね。

Mojolicious以外の場合は、Mojolicious::Plugin::Bcryptのソースコードは、Mojoliciousに依存していなくて、とても簡単なので、コピペして利用できそうです。

セッションIDを生成する方法

ユーザー認証において、少し難しいのは、初回のログイン以外は、セッションIDを使って、ユーザーを識別するということです。

ログイン画面から、ログインをするときは、ユーザーIDとパスワードを使ってユーザーを認証するのですが、認証後は、セッションIDを使ってユーザーを識別します。

セッションIDとは

セッションIDとは、ユーザーを識別するための文字の並びのことです。セッションIDのサンプルを書いてみます。

# セッションIDのサンプル
aabc73ce3
deab33cea
a567c73c1

本物のセッションIDはもっと長い方がよいのですが、ここでは雰囲気だけ。16進数の文字を並べていますが、「a-z」「A-Z」「0-9」などのランダムな文字の並びで構いません。

セッションIDは一意性が高く、十分に長くなければならない

セッションIDは、単なる文字列の並びですが、ユーザー識別に利用するために、いくつかの要件を満たしている必要があります。

ユーザー識別には、まずそのユーザーを識別できるということが求められます。

たとえば、ユーザーが1万人いるとしたら、セッションIDが5000人分しかないと、困りますね。

また、セッションIDを発行したときに、他のユーザーと重なってしまったというのも困りますね。

またセッションIDが短ければ、推測される可能性も高まります。セキュリティ的にも、文字の種類が多く、長い方がよいのです。

セッションIDは単なるランダムな文字列の並びで良いのですが、一意性を確保したい場合に、衝突が起こるのが極めて小さいハッシュ関数を使うのが簡単です。

ハッシュ関数で生成する場合は、文字種は「0-9a-z」の16文字ですので、なるべく文字の長さが長くなるようにするのが安全です。

SHA-1で40文字、SHA-256で64文字、SHA-512で128文字です。

SHA-1
356a192b7913b04c54574d18c28d46e6395428ab

SHA-256
6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b

SHA-512
4dff4ea340f0a823f15d3f4f01ab62eae0e5da579ccb851f8db9dfe84c58b2b37b89903a740e1ee172da793a6e79d560e5f7f9bd058a12a280433ed6fa46510a

セッションIDは、ユーザーが覚える必要がないので長くしても大丈夫です。

セッションIDは、ユーザーを識別し、永続的ではなく、推測されにくい必要がある

セッションIDを作成する場合に、ハッシュ関数を使うのですが、入力を何にするかということは、考える必要があります。

もし、ユーザー名にすれば、簡単に推測されてしまいそうです。この場合、ユーザー名とハッシュ関数の種類がわかれば、出力がわかるわけですね。

また、セッションIDが永続的に変わらないということにもなってしまします。セッションIDは、期限が来ると無効になる必要があるわけです。

時刻の情報を、ユーザー名の末尾に、付け足しましょう。こうすると永続性はなくなります。ユーザー名に対して一意ということもありません。

これで、十分でしょうか? まぁ、心配はなさそう? でも、もう少しセッションIDを強くしましょう。

ユーザー名と時刻というのは、ランダムな情報ではないですよね。攻撃者は、ユーザー名と時刻とハッシュ関数がわかれば、突破できます。

そこで、ランダムな数字というのを、末尾に付け足してあげます。100万くらいにしておきましょうか。

100万以下のランダムな数字を付け加える。

ですので、上記を踏まえて、セッションIDを生成してみましょう。ユーザー名と時刻とランダムな数をSHA-512のアルゴリズムのハッシュ関数に与えています。

use strict;
use warnings;

use Digest::SHA 'sha512_hex';

# セッションIDを作成
my $user_id = 'kimoto';
my $time = time;
my $rand = int rand 1_000_000;
my $session_id = sha512_hex($user_id . $time . $rand);
print "$session_id\n";

出力されたセッションIDの例

e182aa93605c82a472c4986f2749b111f35042f2bb01a3ba9eba3603653cf5ca4d9eb540f1ae32eb4c8d280f18ccb4d5f58f614652c067c7c3175e9b58d29d1a

ちなみにMojoliciousのセッション機能は、セッションIDを生成する方法とは何の関係もないので注意してください。Mojoliciousのセッションは、セッションIDを保存するのに利用できます。

$c->session(session_id => $session_id);

セッションIDをどこに保存するか

セッションIDはログインのタイミングで発行しますが、これをどこに保存しておくのでしょうか?

セッションIDは二か所に保存しておく必要があります。それは、クライアントのクッキーと、サーバー側のユーザーのIDに紐づけられた場所、たとえばユーザーテーブルに保存します。

セッションIDをクライアントのクッキーに保存

クライアントは、クッキーに保存されたセッションIDをサーバーに送信することで、ユーザー認証をしてもらいます。セッションIDの期限が切れていなければ、あなたであるということが、サーバー側で分かるという仕組みです。

Mojoliciousでは、セッションという機能を使って、セッションIDを保存できます。Mojoliciousのセッションは、この記事で書かれている、セッションIDとは何の関係もないので注意してください。

Mojoliciousのセッションは、クッキーをより安全に利用できるようにした機能だと考えておくのがよいでしょう。電子署名が付いているので、改ざんされた場合に、検知できるという機能がクッキーに追加されたというものです。

暗号化はされていませんので、ユーザーを識別できるような情報、たとえば、ユーザーIDやパスワードを保存しておくのに適していません。解読は簡単で、もし生のパスワードを保存したとすれば、クッキーを盗まれたときに、わかってしまいます。

Mojoliciousのセッション(署名付きクッキー)に、セッションIDを保存します。これは、ユーザーがログインしたタイミングで、一度だけ行います。

Mojoliciousのセッション(署名付きクッキー)にセッションIDを保存
$c->session(session_id => $session_id);

Mojoliciousではない場合も、フレームワークのクッキーに保存する機能を利用すれば、同じです。

セッションIDをサーバー側のユーザーIDが紐づけられた場所に保存

セッションIDを保存する場所は、サーバー側であれば、ユーザーIDと紐づけることができれば、どこでも構いません。メモリであったり、ファイルであったり、リレーショナルデータベース(MySQL, PostgreSQL)であったり、揮発性のキーバリューストアー(Redis, memcached)であったりします。

メモリである場合は、サーバーが、フォークしている場合対応できません。ファイルであった場合は、アプリケーションサーバーが、並列に並んでいるときは対応できません。リレーショナルデータベースは、データべースへのアクセスがあります。揮発性のキーバリューストアーは、リレーショナルデータベースよりは速いですが、サーバーをインストールし、利用するコストがあります。

Mojoliciousスタートアップは、Webの新規開発がPerlでできるようになるサイトですので、複雑性を避けて応用が利きやすいように、リレーショナルデータベースを使った方法を紹介します。

基本的な考え方がわかれば、他の方法にすることができると思います。

この記事では、MySQLとPerlのDBIをサンプルにして、紹介しますね。

セッション管理のためのユーザーテーブル定義

ユーザーテーブルは以下のようにします。MySQLの設定でデータベースはInnoDBで、文字コードがutf8mb4であるとします。必ずInnoDBをデフォルトのテーブルのエンジンとして、設定してください。InnoDBは行ロックの機能を持っており、テーブル全体をロックしません。

create table user (
  id int primary_key auto_increment,
  code varchar(150) not null default '',
  name varchar(150) not null default '',
  mail varchar(150) not null default '',
  session_id varchar(150),
  session_expiration bigint not null default 0,
  temp_regist_id varchar(150),
  temp_regist_expiration bigint not null default 0,
  authenticated tiny int default 0,
  unique(code),
  unique(mail),
  unique(session_id),
  unique(temp_regist_id)
);

主要な点は、セッションに関する部分ですが、どのような考えに基づいて、このユーザーテーブルなのかを簡潔に書きます。

データベースのテーブル名とフィールド名の命名規則はデータベースのテーブル名とフィールド名の命名規約に基づいて記述しています。

ユーザーIDが、int型でauto_incrementであるのは、ユーザーは、登録されるデータだからです。列を一意に識別できるIDを持たせています。CODEというのはkimotoのような、ユーザーIDと呼ばれているものです。IDは行を識別する情報として利用しているので、ユーザーIDはCODEに保存しています。

MAILはメールアドレスで、会員登録する場合は、メールを送信して、本人確認をするので、必要です。メールアドレスは重複がないようにユニーク制約をつけています。

mysqlでは、varchar(150)というのは、varcharは255までであれば、どの長さにしても、保存サイズも変わらないということで、なるべく大きな150という値にしています。

mysqlには、古いMySQLの場合、utf8mb4を指定した場合は、インデックスが191の長さまでしか効かないという仕様がありますので、環境依存の落とし穴にはまらないように、191より小さく覚えやすい150にしています。

not null制約は、nullを許可しなければならない絶対的な必要性がない限りは、つけておきます。アプリケーションのロジックにおいて、NULLを排除しておいた方が、判定が簡単だからです。

さてここからがセッションのお話です。セッションIDは、ユニークでなければならないので、ユニーク制約をつけます。ただし、not null制約はつけません。会員登録した段階では、セッションIDが存在しないからです。クッキーに保存されているセッションIDがわかれば、ユーザーテーブルを検索して、どのユーザーからのアクセスなのかを判定できます。

セッションは有効期限を持ちますが、bigintで、64bit整数型で保存しておくと2019年問題をクリアできます。datetime型ではない理由は、セッションIDのチェックのたびに、日付の変換のロジックを書かなくてもよいという理由です。セッションは、ユーザーがどのページにアクセスした場合にも、チェックする必要がありますから、なるべくパフォーマンスが良い方がよいのです。

ユーザーが増えてきて、セッションのチェックによる、アプリケーションサーバーのコストが判明したタイミングが、保存先をリレーショナルデータベースから、揮発性のキーバリューストアに、切り替えるタイミングかと思われます。

セッションIDの保存

セッションIDは、ログインしたタイミングで発行しますが、このタイミングで、ユーザーテーブルのセッションに保存しましょう。セッションの有効期間は、2週間とします。セッションIDは「セッションIDを生成する方法」で解説したセッションIDです。サンプルでは、DBIの例外処理は、書いていませんが、実際は、書いてください。

# セッションを保存。期限は二週間後。
my $two_week_seconds = (60 * 60 * 24 * 14);
my $session_expiration = time + $two_week_seconds;
my $sth = $dbi->prepare('update user set session_id = ?, session_expiration = ?');
$sth->execute($session_id, $session_expiration);

もし、セッションIDにはユニーク制約がついているので、重複した場合は、この処理は失敗しますが、重複する確率が非常に小さいようにセッションIDを作っていますので、重複する確率は、天文学的に小さくなるでしょう。

絶対に起こらないようにしたい場合は、失敗した場合に、セッションIDを発行して、処理を繰り返しましょう。

セッションの確認

ユーザーがログインしている場合は、クッキーにセッションIDが保存されています。クッキーに保存されているセッションIDを、データベースに保存されているユーザーIDと比較しましょう。

セッションIDが一致することと、有効期限が切れていないことを確認しましょう。

MojoliciousとDBIによるコードのサンプルです。

# クッキーからセッションIDを取得
my $cookie_session_id = $c->session('session_id');

# 現在時刻を取得
my $cur_time = time;

# セッションが一致して、有効期限が切れていない、ユーザー情報を取得
my $sth = $dbi->prepare('select id, session_expiration from user where session_id = ?, session_expiration < ?');
$sth->execute($cookie_session_id, $cur_time);
my $user = $sth->fetchrow_hashref;
$sth->finish;

# もしなければ、認証失敗
unless ($user) {
  # 認証失敗した場合の処理を書く
}

# ユーザーIDを取得
my $user_id = $user->{ID};

# 現在のセッションの有効期限を取得
my $user_session_expiration = $user->{SESSION_EXPIRATION};

セッションが確認できれば、だれがログインしているがわかりますね。必要な情報はユーザーIDだけですが、セッションの有効期限の更新で利用するために、セッションの有効期限も取得しています。

セッションの有効期限の更新

ユーザーがログインを継続的に行っている間は、セッションを更新しましょう。最終ログインの日時から、2週間ログインがなかった場合に、セッションが切れるという実装にしてみましょう。

ここでひとつ考えておく必要があるのは、updateによる更新処理は、selectよりも負荷が高いということです。selectは、スケールアウトしやすのですが、updateはマスターのデータベースを必ず更新しなければならないため、スケールアウトしにくいのです。また、update処理が行われる場合は、MySQLのInnoDBでは、対象の行がロックされます。

updateは、スケールアウトしにくいということと、行にロックがかかるという点が、selectとは異なるということを、覚えておきましょう。

ですので、アクセスがあるたびに、セッションの有効期限を更新という処理は書きたくないですね。ここでは、1日に1回だけセッションの有効期限を更新するというロジックにしてみましょう。

DBIを使ったサンプルです。変数名は、上記のサンプルから引きついだものもあります。

# 新しい有効期限は、現在から2週間後に設定
my $new_user_session_expiration = $cur_time + $two_week_seconds;

# 1日の秒数
my $one_day_seconds = 60 * 60 * 24;

# 有効期限が1日以内に更新されていない場合に、更新する
if ($new_user_session_expiration > $user_session_expiration + $one_day_seconds) {
  my $sth = $dbi->prepare('update user set session_expiration = ? from user where id = ?');
  $sth->execute($new_user_session_expiration, $user_id);
}

会員登録機能の実装

ユーザー認証によるログイン機能には、会員登録機能が必要です。会員登録機能を実装するにあたって、知っておく必要のある知識をまず解説します。

メールアドレスによる本人認証

この記事では、一般的な方法であるメールアドレスによる本人認証を使って、新規会員登録を行います。

メールアドレスによる本人認証とは、メールアドレスはその人が所有しているので、メールを見ることができるのは、その人だけだということを根拠にした本人認証です。

メールのパスワードが漏洩してしまうと、それで終わりなので、最近は、電話番号などの、二重認証などが、大手のサイトなどで採用されていますよね。スタートアップするWebサイトは、メールアドレスによる本人認証から始めましょう。

会員登録画面に必要なユーザー情報

会員登録画面に最低限必要なユーザー情報は、以下です。

  • ユーザーID(メールアドレスをユーザーIDにする場合は不要)
  • メールアドレス
  • パスワード

ユーザーIDを作成する場合は、ユーザーIDとメールアドレスとパスワードです。メールアドレスをユーザーIDにする場合は、メールアドレスとパスワードです。メールアドレスをユーザーIDにした場合でも、ユーザーの希望による、メールアドレスの変更は、可能ですので、安心してください。

以下の記事では、ユーザーIDを作成するという前提で、記事を書いていきます。

ビジネスの種類によっては「氏名」「住所」「電話番号」なども入力してもらうとよいでしょう。

ユーザーテーブルの作成

ユーザーIDとメールアドレスの対応は、1対1であって、ユーザーIDは一意で、メールアドレスも一意である必要があります。アプリケーションでチェックを行うことは、必要ですが、データの整合性が完全に取れているということが大切ですですので、ユーザーテーブルは、以下のように作成します。

これは上記「セッション管理のためのユーザーテーブル定義」で解説したものと同じです。IDは、行を一意に定義するもので、ユーザーIDではなく、整数が入ります。CODEにkimotoのようなユーザーIDが入ります。

またメールアドレスによる本人認証は、本人にメールアドレスを送信して、送信された仮登録IDを使ったリンクからアクセスしてもらうことで行います。そこで、ユーザーテーブルには、仮登録ID「TEMP_REGIST_ID」と仮登録の有効期限「TEMP_REGIST_EXPIRATION」を持たせています。

mysqlによるユーザーテーブルの作成例です。

create table user (
  id int primary_key auto_increment,
  code varchar(150) not null default '',
  password_hash varchar(150) not null default '',
  name varchar(150) not null default '',
  mail varchar(150) not null default '',
  session_id varchar(150),
  session_expiration bigint not null default 0,
  temp_regist_id varchar(150),
  temp_regist_expiration bigint not null default 0,
  authenticated tiny int default 0,
  unique(code),
  unique(mail),
  unique(session_id),
  unique(temp_regist_id)
);

会員登録画面の作成

会員登録画面には、ユーザーIDとメールアドレスとパスワードを指定してもらう必要があります。

Mojoliciousのサンプルを使った会員登録画面の例です。フォーム送信する場合はPOSTメソッドを使います。actionは、自分自身のURLを指定して、メソッドはPOSTで、オペレーションとして「temp-regist」を指定して、分岐できるようにしています。

hidden_fieldは隠しフィールドを作成するMojoliciousの関数です。text_field、password_fieldも同じです。Mojoliciousのフォームヘルパーは、バリデーションで値が間違っていた場合など、そのページに戻らなければならなかった場合に、値を自動で復元してくれるという機能があります。

<div class="login-form">
  <form action="<% url_for %>" method="POST">
    <div>
      <%= hidden_field op => 'temp-regist' %>
    </div>
    <div>
      ユーザーID
      <%= text_field 'user-id' %>
    </div>
    <div>
      メールアドレス
      <%= text_field 'mail' %>
    </div>
    <div>
      パスワード
      <%= password_field 'password' %>
    </div>
    <div>
      <input type="submit" value="送信">
    </div>
  </form>
</div>

ユーザーIDが正しいものかを確認

ユーザーIDが正しいものであることを確認しましょう。ユーザーIDが正しいものというのは以下のことをいいます。

  • ユーザーIDを構成する文字(この例では、英数字とアンダーバー)の1文字以上で構成されていること
  • ユーザーIDが、すでに存在するユーザーIDと重複しないこと
  • メールアドレスが、認証済みのすでに存在するメールアドレスと重複しないこと
  • ユーザーIDの長さがデータベースのCODE列の長さに収まること

ユーザーIDは、一意でなければならないので、これをチェックするために、データベースにアクセスする必要があることに注意しましょう。

ではMojoliciousとDBIで、サンプルを書いてみましょう。正しくない場合は、エラー情報に追加するようにしています。

# エラー情報
my $errors = {};

my $user_id = param('user-id');

# ユーザーIDを構成する文字をチェック
if ($user_id =~ /^[a-zA-Z0-9_]+$/) {

  # ユーザーIDの長さをチェック
  my $max_user_id_length = 100;
  if (length $user_id > $max_user_id_length) {
    $errors->{user_id} = 'ユーザーIDは100文字以内で作成してください。';
  }
  else {

    # ユーザーIDが重複していないことをチェック
    my $sth = $dbi->prepare('select code from user where code = ?');
    $sth->execute($user_id);
    my $user = $sth->fetchrow_hashref;
    $sth->finish;
    if ($user) {
      $errors->{user_id} = 'すでに存在するユーザーです。';
    }
    else {
      # メールアドレスが重複していないことをチェック
      my $sth = $dbi->prepare('select mail from user where mail = ? and authenticated = 1');
      $sth->execute($mail);
      my $user = $sth->fetchrow_hashref;
      $sth->finish;
      if ($user) {
        $errors->{user_id} = 'すでに登録されているメールアドレスです。';
      }
    }
  }
}
else {
  $errors->{user_id} = 'ユーザーIDは「a-zA-Z0-9_」の1文字以上で作成してください。';
}

ユーザーIDは「a-zA-Z0-9_」の一文字以上で構成しますが、このチェックはPerlの正規表現を使って行っています。

ユーザーIDの長さのチェックは、CODEの長さが150文字なので、きりのよいところで100にしています。100文字あれば、十分でしょう。

データベースにアクセスしてユーザーに重複がないことを確認しています。

エラー情報は、ハッシュリファレンスに保存しています。こうしておくと、個々のエラーメッセージを取り出したり、エラーメッセージだけを配列として取り出すことが可能になります。

パスワードが有効なものであるかを確認

次はパスワードのチェックです。有効なパスワードとして以下のことを定義します。

  • パスワードはASCIIコードのグラフィック文字の8文字以上で構成される

ASCIIコードのグラフィック文字とは、ASCIIコード印字文字から空白を除いたものです。パスワードには空白を許可しませんが、キーボードで打てる英数字と記号を許可します。

このグループとして適切なものが、ASCIIコードのグラフィック文字です。

# 印字文字の一覧。これから空白を除いたものがグラフィック文字。
  @ `
! A a
" B b
# C c
$ D d
% E e
& F f
' G g
( H h
) I i
* J j
+ K k
, L l
- M m
. N n
/ O o
0 P p
1 Q q
2 R r
3 S s
4 T t
5 U u
6 V v
7 W w
8 X x
9 Y y
: Z z
; [ {
< \ |
= ] }
> ^ ~
? _    

パスワードは、短すぎると総当たりでログインされる危険性が高いので、8文字以上とします。

パスワードの長さによって、安全性がわかるUIであれば、より親切でしょう。

パスワードは、ユーザーテーブルのPASSWORD_HASHに平文で保存されるわけではなくbcryptによって、ハッシュ化されたものが保存されるので、最大長のチェックはなくても大丈夫です。

ではMojoliciousで、サンプルを書いてみましょう。

my $password = param('password');

# パスワードがASCIIのグラフィック文字の8文字以上であることを確認
unless ($password =~ /^\p{PosixGraph}{8,}$/) {
  $errors->{password} = 'パスワードは、英数字、記号の8文字以上で指定してください。';
}

「\p{PosixGraph}」は、ASCIIコードのグラフィック文字を表現する正規表現の文字クラスです。量指定子を組み合わせて、グラフィック文字の8文字以上であることを確認しています。

パスワードのハッシュ化

パスワードはbcryptを使ってハッシュ化します。

# パスワードのハッシュ化
my $password_hash = $c->bcrypt($password);

メールアドレスのチェック

メールアドレスのチェックは、厳密にやることはあまり意味がないような気もします。なぜなら、メールアドレスは、実際にユーザーに到達するまで、正しいかどうかがわからないからです。

ここではメールアドレスのチェックとして「@」が入っていることをユーザーのために確認する程度にとどめておきたいと思います。

my $mail = param('mail');

# メールアドレスに@が入っていることを確認
unless ($mail =~ /\@/) {
  $errors->{mail} = 'メールアドレスを正しく入力してください。';
}

ユーザーデータの保存と仮登録IDの発行

チェックしたユーザーデータをデータベースに保存します。このとき同時に仮登録に利用する一時的な仮登録IDを、発行して、データベースに保存します。

仮登録IDは、セッションIDの生成方法と同じでよいですが、メールの本文の折り返しでリンクを正しくクリックできなくなるという不安がありますので、Outlookの既定の折り返し文字数の76文字以内で生成しておくことにします。SHA1で生成して、長さは40文字です。

# 仮登録IDを作成
my $time = time;
my $rand = int rand 1_000_000;
my $temp_regist_id = sha1_hex($user_id . $time . $rand);

生成される仮登録IDの例

e182aa93605c82a472c4986f2749b111f35042f2

次に仮登録IDの有効期間を作成します。有効期間は24時間とします。

# 仮登録IDの有効期間は、24時間後。
my $one_day_seconds = (60 * 60 * 24);
my $temp_regist_expiration = time + $one_day_seconds;

注意事項

サンプルに関する注意。DBIとあるのは、実際に利用する場合は、DBIx::ConnectorかDBIx::Handlerと読み変えてください。Web開発でDBIを使う場合は、接続管理をしてくれるモジュールが必要になるからです。

簡易的に書くために、データベースの処理に関しては、例外処理を行っていないので注意してください。実際に書く場合は、例外処理を追加するか、例外処理を自動で行ってくれるO/Rマッパーを利用してください。

検討事項

ユーザーテーブル、仮登録ユーザーテーブル、ユーザー認証テーブルに分けた方がよいかも。

セキュリティの専門家の方に聞きたいこと。

パスワード認証は、bcryptでよいか?

セッションIDがSHA-512の場合に、128文字になるが、不具合がでてくる事象はないか?