PHPでの認証処理~  365日の紙PHP(7日目)

近代的なPHP開発を行うために

おススメ!記事
Raspberry Pi 用「HAL」で、
カップラーメン・タイマーを作ってみよう!
ラズパイDIYの決定版! ソケットサーバー「HAL」をご紹介します。

昨日までで、モックアップでのログイン画面の遷移の確認は終わりました。今日は実際のログイン認証処理を書いていきます。プログラム言語はPHP、やっと本題に入れましたね。

初めてのPHPプログラムなので簡単かと思いきや、かなり難しい話を含んでいます。今は意味がよく分からなくてもよいですがとても重要な内容なので将来かならず意味がわかるようになってください。

送信された値の検査

さて、これまで作成した<form/>でsubmit(送信)を行うと、そのactionであるlogin.phpが実行されます。入力されたIDやパスワードも、このlogin.phpに渡されます。ですので、ここに認証処理を書いていきましょう。

<!DOCTYPE html>
<!--
To change this license header, choose License Headers in Project Properties.
To change this template file, choose Tools | Templates
and open the template in the editor.
-->
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<h1>ようこそ</h1>
<p>ログイン処理が成功しました。ようこそ●●さん。</p>
</body>
</html>

まず、<!-- から --> まではHTMLのコメントですので、邪魔ですから削除してしまいましょう。

そしてファイルの先頭に、PHPコードを書くための領域を作ります。

<?php

?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<h1>ようこそ</h1>
<p>ログイン処理が成功しました。ようこそ●●さん。</p>
</body>
</html>

PHPコードはこの、<?php から ?> までの間に書くことになります。

まず初めに、ログインフォームから送られてきたIDとパスワードを識別してみたいと思います。ログインフォームのIDフィールドにはnameとして"user_id"が、パスワードフィールドにはnameとして"pass"が指定してありました。これらの値をフォームから"POST"で送信しています。

PHPでPOSTで送信された値を取得するには、$_POSTというスーパーグローバル変数が利用できます。

スーパーグローバル

どういうことかというと、IDの値は $_POST["user_id"] で取得でき、同じようにパスワードは $_POST["pass"] で取得できるということです。

※ なお、PHPのスーパーグローバル変数はどこからでもアクセスでき、さらに書き換えもできてしまうため直接操作すべきではない、というのが一般的な考え方です。しかしながら、ここではまず一番簡単な取得方法として直接$_POSTを参照して値を取得しています。

さて、取得方法がわかったので、実際に値が取得できているか画面に出力して確認してみましょう。

<?php
echo "入力された値は ID: {$_POST["user_id"]} パスワード: {$_POST["pass"]} です。";
?>

これでログイン処理をしてみてください。次のように、値が文字列の中に埋め込まれて表示されるはずです。

altテキスト

echo文は、PHPで出力を行うための基礎的な構文です。ここでは、その後のダブルクオート " で囲まれた内容を出力します。PHPではダブルクオートで囲った場合、変数の中身をその文字列の中に展開することができます。シングルクオート ' では展開されず、変数がそのまま文字として埋め込まれます。

$val = "test";

echo "値 $val";
echo '値 $val';

上記の場合、上のechoの出力結果は

値 test

ですが、下のシングルクオートでの出力結果は

値 $val

になります。PHPの変数の頭には$を付けることになっていますので、出力する文字列中に$があって、そこを変数として展開させたくない時にはシングルクオートを利用することができます。たとえば

$valは変数です。

という注釈を表示させたいのに、$valの値が展開されて

testは変数です。

と表示されてしまうのを避けることが出来ます。

{$_POST["user_id"]}のように中括弧 {} で囲むと、その中身が変数であることを強制できます。今回の場合、スーパグローバル$_POSTについて、"user_id"と"pass"を利用していて、中括弧でくくらないと、

"入力された値は ID: $_POST["user_id"] パスワード: $_POST["pass"] です。";

となり、ダブルクオートでくくられている範囲は以下の赤文字の範囲

"入力された値は ID: $_POST["user_id"] パスワード: $_POST["pass"] です。";

となって、構文エラーになってしまいます。このため、$_POST["user_id"]と$_POST["pass"]がそれぞれ1まとまりの変数であることをPHPに知らせるために、中括弧 {} でくくっています。文字列をダブルクオートで囲ってその中に変数を埋め込むときは、常に中括弧でくくってしまうのが確実です。

さて、入力された値が取得できることがわかったので、この値を検査するプログラムを書きます。

まず、このシステムでログイン出来るIDとパスワードの組み合わせを定義しましょう。最終的にはデータベースに値を登録してそこを参照するようになりますが、ここではまず、直接このlogin.phpファイルにIDとパスワードの組み合わせを記述してみます。

<?php
$allow = array(
"taro" => "taro_pass",
"hanako" => "hanako_pass",
"kenji" => "kenji_pass",
);
?>

array() は、PHPでの配列の定義です。カンマ , で区切って複数登録できます。それぞれが a => b の形式になっているのがわかるでしょうか? このようにすると、aというキーでbという値が定義され、$allow[a]で値bが取得できるようになります。

例えばこの場合、$allow["taro"]で"taro_pass"が取得できます。

先ほどスーパーグローバル$_POSTに対し、$_POST["user_id"]で送られてきた値を取り出しましたね? つまり、スーパーグローバル$_POSTもまた配列だったということです。

配列はいくつでも要素を定義できますので、あなただけのIDとパスワードの組み合わせをこの配列に追加してみてください。

なお、今行っている方法はセキュリティ的にはとてもまずい方法ですが、今後少しずつセキュリティを強化していきますのでご安心ください。

IDとパスワードの組み合わせを登録したら、入力された値と登録内容を付きあわせて検証しましょう。

<?php
$allow = array(
"taro" => "taro_pass",
"hanako" => "hanako_pass",
"kenji" => "kenji_pass",
);

$login_ok = false;
foreach($allow as $key => $val){
if($key === $_POST["user_id"] && $val === $_POST["pass"]){
$login_ok = true;
break;
}
}

if($login_ok === false){
http_response_code(403);
header("Location: index.php");
exit;
}
?>

$login_ok = false; で、まず、とにかく全員ログインできない状態にします。false(フォルス)は偽と訳され、簡単な日本語にすると「ダメ!」とか「不適切!」という意味です。

    if($login_ok === false){
http_response_code(403);
header("Location: index.php");
exit;
}

で、$login_okがfalseであった場合はheader()メソッドを使ってindex.phpに処理を転送します。index.phpはログインフォームがあるファイルでしたね? つまり、間違ったIDとパスワードなので再びログイン処理を要求することになります。

http_response_code(403)は、HTTPでのステータスコード 403 を返すという意味です。あまり深く考えなくてもいいですが、403はアクセス権がないためにアクセスが拒否された場合等に利用されます。

HTTPステータスコード

最後の exit; は、それ以降のプログラムコードを実行せずに、この PHP ファイルの処理を終了するという意味です。

認証が拒否された場合の処理は分かったと思いますので、認証を行うコードを見てみましょう。

    foreach($allow as $key => $val){
if($key === $_POST["user_id"] && $val === $_POST["pass"]){
$login_ok = true;
break;
}
}

この部分が認証処理です。

    foreach($allow as $key => $val){

}

は、繰り返しの処理を行う命令です。$allowという配列から定義を1項目ずつ取り出し、取り出した項目について キー:$key と 値:$val を設定して { } 内を実行します。

今回の場合は、まず、

$key:"taro" $val:"taro_pass"  

が取り出され、次のループで

$key:"hanako" $val:"hanako_pass"  

最後に

$key:"kenji" $val:"kenji_pass"  

が取り出されます。

if(){ } 文についてはJavaScriptの時と全く同じ意味です。() 内の条件の通りなら{ }内を実行します。違うのは条件の列挙方法が||ではなく&&であることです。

||は「または」という意味でしたが、&&は「尚且つ」という意味です。つまり

$key === $_POST["user_id"] && $val === $_POST["pass"]

は、「$key と $_POST["user_id"] が同じ値で尚且つ $val と $_POST["pass"] が同じ値なら」という意味になります。

この場合に

$login_ok = true;

で、$login_okをtrue(トゥルー)にします。trueは「真」と訳され、簡単な日本語にすると「正しい」「合っている」といった意味になります。

その後の

break;

は、foreach(){}のループ文を中断して処理を抜けるという意味です。例えばID:taro、パスワード:taro_passが入力され、ログイン出来るユーザーであることがわかった後でもループを続けてhanako、kenjiについて検査するのはとても無駄です。

そこで、break を宣言して以降のループを終了させてしまいます。

以上が、このプログラムコードの意味です。では、実際にログイン認証が行われるか確認してみましょう。

arrayで指定したIDとパスワードの組み合わせのみでログインでき、それ以外の時はindex.phpに戻されることが確認できたでしょうか?

パスワードのハッシュ化

ところで、login.phpにはログイン出来るIDとパスワードがそのまま記載してあります。

    $allow = array(
"taro" => "taro_pass",
"hanako" => "hanako_pass",
"kenji" => "kenji_pass",
);

もし、何かの手違いでこのlogin.phpの内容が漏れてしまったら非常にまずいことになりますね。

ですから、この情報が漏れてもログインできるIDとパスワードがわからないようにしたいところです。

通常、このような時にはパスワードをハッシュ化してしまいます。ハッシュという言葉の元々の意味は、「細切れにする」といった意味のようですね。「ハッシュドポテト」のハッシュと同じ語源のようです。

生のパスワードを一定の規則にしたがってハッシュ値と呼ばれる値に変換しておき、パスワードが入力されたら同じ規則でハッシュ化し、それが一致するか確認します。ハッシュ値から元の文字列が特定できないようなハッシュ化アルゴリズムを使うことで、万が一ハッシュ値が外に漏れてしまっても元のパスワードが分からないようにするわけです。

ハッシュ化のよいところは、元に戻せないために、サイトの管理者にも元のパスワードがわからなくなることです。せっかく強固なパスワードを指定しても、サイト管理者がユーザーになりすますことが出来てしまったら意味がありません。ですからパスワードを暗号化するのではなく、ハッシュ化してそのユーザー以外は誰もパスワードを知らない状態にするため、ハッシュ化が用いられることになります。

メジャーなハッシュ化アルゴリズムにはmd5やsha1、sha256などがあり、md5が頻繁に用いられています。

例えば

echo md5("taro_pass");

を実行すると

06d8d2569c0ab4f15c53bc63ac0532bc 

という文字列が出力されることと思います。

この文字列を

    $allow = array(
"taro" => "06d8d2569c0ab4f15c53bc63ac0532bc",
);

のように設定しておけば、万が一login.phpの内容が流出しても、元のtaro_passが分からないのでログインできない、という寸法でした。

ところが、パソコン技術の進歩は恐ろしいもので、高速なCPU(正確にはGPU)を利用すると総当りをすればかなり現実的な時間で元のパスワードが推定されてしまう時代になってしまいました。それ専用に作られたシステムでは、制限がない状況だと1秒で1800億通りもの試行が行えるようです。8文字の英数字のみのパスワード(約220兆パターン)のmd5値を総当りするのに20分程度しかかからないそうです。英数字のみのパスワードがいかに危険かよく分かる数値ですね。パスワードに記号を混ぜることが推奨されるのはこういう理由からです。

未だにmd5やsha1などでパスワードのハッシュ化を行っているシステムが沢山あるのは非常に恐ろしい事です。これらは、コンピュータのパワーを使った力技で簡単かつ短時間に元のパスワードを推測されてしまう危険をはらんでいます。

ですからここでは、より安全なハッシュ化について説明します。

現在のPHPにおいて、より安全なハッシュ化を行うにはPHP5.5から導入されたpassword_hash()関数を用いるべきです。

password_hash

password_hash()関数では、ハッシュ化された文字列にソルトと呼ばれる、ハッシュ化に使った文字列を付加します。ソルトはこの関数が呼ばれるたびにランダムな文字列が生成されます。

ソルトが違うと、ハッシュ化された値は別のものになります。md5やsha1のようなハッシュ化ではパスワードが同じなら得られるハッシュ値も同じなので、もし同じパスワードの人が二人いたら、片方が総当りで破られればもう片方は総当りすることなく、一瞬で破られてしまうことになります。

しかし、別のソルトをつかってハッシュ化を行えば、同じパスワードでも別のユーザーでは別のハッシュ値になり、片方が総当りで推定されても別の同じパスワードについてはもう一度総当りしないと解析できません。ソルトをユーザーアカウント毎に別のものにすれば、パスワードの総当り攻撃による連鎖的な解析を困難にすることが出来るわけです。先程述べたように英数字8文字のパスワードの場合、md5では20分で全てのパスワードの組み合わせが解析され、つまり全てのアカウントのパスワードが20分で判明してしまうことになりますが、ソルトを使ったハッシュ化でソルトをアカウント毎に変えれば同じ英数字8文字のパスワードでも20分×アカウント数分の時間が必要になるわけです。

password_hash()ではこのソルトを自動でランダムに生成し、ハッシュ値に付加してくれます。

用法は、

password_hash("some-password", PASSWORD_DEFAULT);

のようにします。第1引数はハッシュ化するパスワードを指定します。第2引数はハッシュ化につかうアルゴリズムです。PASSWORD_DEFAULTはPHP標準のアルゴリズムで、そのPHPでの最適なアルゴリズムが選択されます。例えば今後PHPがバージョンアップを重ねる段階でこれまでのアルゴリズムに脆弱性(ぜいじゃくせい)が見つかった場合、新しいPHPではより強固なアルゴリズムパターンをこのPASSWORD_DEFAULTに適用するようです。このため、現在のPASSWORD_DEFAULTでのハッシュ値は60文字で足りますが、将来的に拡張がなされても良いように、ハッシュ値を格納するデータベースのカラム許容文字数を255文字のように余裕を持たせておくことが推奨されています。他には、PASSWORD_BCRYPTというBLOWFISHアルゴリズムを使ったハッシュ化を利用することも出来ます。

こうして得られたハッシュと入力されたパスワードを比較するにはpassword_verify()関数を用います。

password_verify

password_verify("some-password", $hash);

ハッシュ値の比較が一致するとこの関数はtrueを返し、一致しなければfalseを返します。ハッシュ化につかわれたソルトは$hashに含まれているので、そのソルトを使って"some-password"をハッシュ化し、比較するわけです。

さて、では実際に

    echo password_hash("taro_pass", PASSWORD_DEFAULT) ."<br>";
echo password_hash("hanako_pass", PASSWORD_DEFAULT) ."<br>";
echo password_hash("kenji_pass", PASSWORD_DEFAULT) ."<br>";

として、それぞれのパスワードをハッシュ化してみましょう。

$2y$10$9xUPdF/l2deV2gCo2BbqAeJ6Znh1eTDfQ4e7Rgy0Jy8lzvmyYIZcu
$2y$10$nBaOXLHZgXAGobWD1JZMxOnLUwQ1BAYdxJpyqQSJRfVxRwQ/o6ln2
$2y$10$u2GdyeCYUtImZAgkyqlpYeWrIsuLwmXtsxCzCOX7NHhEO/q0AkBE.

出力される値は上記とは違うはずです。もう一度表示してみると、また別の文字列になるはずです。先ほど述べたようにソルトがアクセスするたびに変わるので、得られるハッシュ値もアクセスするたびに変わる、ということです。

ではこうして得られた文字列を$allow配列に設定しましょう。最初のほうで説明しましたが、ダブルクオート " で文字列を囲った中に$があると、PHPはそこを変数として認識して展開しようとしてしまいます。

上記のように、今回のハッシュ値には$が含まれていますので、今回はダブルクオート ” ではなく、シングルクオート ' で囲みましょう。

$allow = array(
"taro" => '$2y$10$9xUPdF/l2deV2gCo2BbqAeJ6Znh1eTDfQ4e7Rgy0Jy8lzvmyYIZcu',
"hanako" => '$2y$10$nBaOXLHZgXAGobWD1JZMxOnLUwQ1BAYdxJpyqQSJRfVxRwQ/o6ln2',
"kenji" => '$2y$10$u2GdyeCYUtImZAgkyqlpYeWrIsuLwmXtsxCzCOX7NHhEO/q0AkBE.',
);

この通り入力してもよいですし、あなたの環境で出力されたハッシュ値を設定しても構いません。全く別の文字列のはずですが正しく動作します。

次は、今回設定したハッシュ値と入力されたパスワードの照合です。password_verify()関数を使います。

    foreach($allow as $key => $val){
if($key === $_POST["user_id"] && password_verify($_POST["pass"], $val) === true){
$login_ok = true;
break;
}
}

これで、もし仮にlogin.phpの内容が流出してもIDとパスワードを割り出す事が非常に困難になりました。多分、3ヶ月後にこのプログラムを見たあなたでも、元のパスワードが何だったか推測する事はできなくなっているでしょう。

最後に、

<p>ログイン処理が成功しました。ようこそ●●さん。</p>

の●●の部分に、入力されたIDを出力して、今日のプログラムはおしまいです。HTMLの中でPHPの変数を呼び出すので<?php ?>で囲み、echo文で出力します。

<?php
$allow = array(
"taro" => '$2y$10$9xUPdF/l2deV2gCo2BbqAeJ6Znh1eTDfQ4e7Rgy0Jy8lzvmyYIZcu',
"hanako" => '$2y$10$nBaOXLHZgXAGobWD1JZMxOnLUwQ1BAYdxJpyqQSJRfVxRwQ/o6ln2',
"kenji" => '$2y$10$u2GdyeCYUtImZAgkyqlpYeWrIsuLwmXtsxCzCOX7NHhEO/q0AkBE.',
);

$login_ok = false;
foreach($allow as $key => $val){
if($key === $_POST["user_id"] && password_verify($_POST["pass"], $val) === true){
$login_ok = true;
break;
}
}

if($login_ok === false){
header("Location: index.php");
exit;
}
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<h1>ようこそ</h1>
<p>ログイン処理が成功しました。ようこそ<?php echo $_POST["user_id"] ?>さん。</p>
</body>
</html>

これがより強固なPHPでのパスワードのハッシュ化と照合です。

なお、先ほど申し上げたとおり、password_hash()はPHP5.5以降でないと利用できませんん。PHP5.4以前を使わなければいけない場合は、別の代替手段を講ずる必要があるでしょう。

加えて、今はよくわからなくても構いませんが、いまさら聞けないパスワードの取り扱い方 を一読しておくと、安全なWEBシステム作りに役に立つかと思います。

終わりに

さて、ところで今回作ったシステムで、間違ったIDとパスワードを入力するとログインフォーム画面に戻されますが、その際、入力したIDとパスワードが消えてしまうことに気づいていらっしゃいましたか?

これはユーザーにストレスを感じさせるでしょう。明日はこの辺りについて修正を行い、入力した値が消えないようにしたいと思います。

この記事へのコメント

※現在コメントはMarkdown記法が強制です。>>Markdown の書き方


この記事に返信

このコメントに返信