ログインのリダイレクト処理~  365日の紙PHP(8日目)

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

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

さて、昨日まででログインの認証処理がひとまず書き終わりました。

altテキスト

このように、ログインが成功するとメッセージが表示されます。

しかし、ログインが失敗すると入力した内容が消えてしまう問題がありました。今日は、この問題を修正したいと思います。

まず、ログイン失敗した時にログインフォーム画面に戻るコードを見てみましょう。

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

ここですね。ここで、入力されたIDとパスワードをログインフォームに送ることができれば入力欄が空で表示されるのを防げそうです。

ログインフォームを作っていた時に説明しましたが、ログインフォームではPOSTというメソッドを使って値をlogin.phpに送信しました。同じようにindex.phpに値を送るにはどうしたらよいでしょう?

残念ながら、header() メソッドではPOSTで値を送信することはできません。しかし、GETであれば値を送ることができます。どうするかというと、

header("Location: index.php?key1=value1&key2=value2");

このようにします。遷移するページのURLの後ろに?をつけてそれ以降が受け渡すパラメターであることを明示したあと、&区切りで key=value の形で受け渡す値を記述していきます。このような文字列のことをクエリーストリングと呼びます。

今回の場合、

header("Location: index.php?user_id={$_POST["user_id"]}&pass={$_POST["pass"]}");

ということになります。

こうして index.php に渡された値は、$_GET で取得できます。POSTの時は$_POSTでしたが、今回はGETですので $_GET です。

では index.php の<form/>タグ内を以下のように書き換えて試してみましょう。isset()は、()内の変数が定義されていた場合に真となります。

<form id="login-form" action="login.php" method="POST">

<span class="login-label">ID</span> : <input type="text" id="id-field" name="user_id" value="<?php if(isset($_GET["user_id"])){ echo $_GET["user_id"]; } ?>"><br>

<span class="login-label">パスワード</span> : <input type="password" id="pass-field" name="pass" value="<?php if(isset($_GET["pass"])){echo $_GET["pass"]; } ?>"><br>

<button type="button" id="submit-button">送信</button>

</form>

altテキスト

フィールドが空にならずに文字列が入力された状態でページが開きました。

これで…、は、ぜんぜん良くないですね…。アドレスバーを見てみましょう。

altテキスト

入力されたパスワードが丸見えです。

インターネットでは色々なサーバーを経由して通信を行いますが、その際、このアドレスバーに表示されているURLは経由したサーバーにログとして残ってしまいます。このパスワードは間違ったパスワードとはいえ、本物のパスワードに近いものが入力されているはずです。IDもわかるので、似たようなパスワードを数回試すだけで別の第三者にログインされかねません。

これではセキュリティも何もあったものではありませんね。どうやら、この方法ではダメなようです。

ではどうしましょう? 1つには、後日説明するセッションという機能を使う方法があります。サーバーに値を保存しておいて、それを各ページで読み出すという方法です。

でも、ちょっと考えてみましょう。今はこんな状態ですね?

altテキスト

見ての通り、index.php から login.php には IDとパスワードが送れています。この値はアドレスバーには表示されませんし、経由するサーバーにも残りません。通信内容をモニタリングされると見られてしまいますが、それについては将来説明するhttps通信を使えば見られないようにできます。

と、いうことは、こうしてしまえばいいわけです。

altテキスト

つまり、index.php からIDとパスワードを送るのは問題ないので、login.php ではなく自分自身 index.php に送ってしまいます。そうして index.php でIDとパスワードの照合を行い、間違っていたらリダイレクトするのではなく合っていたらリダイレクトします。

ではやってみましょう。

まず、login.php に書いた認証処理をごっそり index.php に移します。

login.php からは認証処理がなくなり、さっぱりとしたHTMLだけになります。

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

こちらが index.php です。

<?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){
http_response_code(403);
header("Location: index.php?user_id={$_POST["user_id"]}&pass={$_POST["pass"]}");
exit;
}
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
<link rel="stylesheet" type="text/css" href="login.css" media="screen">
<script src="https://code.jquery.com/jquery-1.12.0.min.js"></script>
<script src="login.js"></script>
</head>
<body>
<div id="login-panel">
<form id="login-form" action="login.php" method="POST">
<span class="login-label">ID</span> : <input type="text" id="id-field" name="user_id" value="<?php if(isset($_GET["user_id"])){ echo $_GET["user_id"]; } ?>"><br>
<span class="login-label">パスワード</span> : <input type="password" id="pass-field" name="pass" value="<?php if(isset($_GET["pass"])){echo $_GET["pass"]; } ?>"><br>
<button type="button" id="submit-button">送信</button>
</form>
</div>

<div id="footer">
Copyright © 2016 feijoa.jp All rights reserved.
</div>
</body>
</html>

そして、$login_okが false ではなく true の場合の転送にします。

header() メソッドのリダイレクト先は login.php になり、今回は正常なログインによる転送ですので、HTTPレスポンスコードは 200 になります。

if($login_ok === true){
http_response_code(200);
header("Location: login.php");
exit;
}

そして、先ほど書いたIDフィールドとパスワードフィールドについて、今回は<form/>から値がPOSTで送信されてくるため、$_GET から $_POST に書き換えます。送信先であるaction は login.php ではなく 自分自身である index.php に書き換えます。

<form id="login-form" action="index.php" method="POST">

<span class="login-label">ID</span> : <input type="text" id="id-field" name="user_id" value="<?php if(isset($_POST["user_id"])){ echo $_POST["user_id"]; } ?>"><br>

<span class="login-label">パスワード</span> : <input type="password" id="pass-field" name="pass" value="<?php if(isset($_POST["pass"])){echo $_POST["pass"]; } ?>"><br>

<button type="button" id="submit-button">送信</button>

</form>

最後に、index.php に初めて訪れた時は $_POST["user_id"] と $_POST["pass"] に何も値が設定されていないため、これらが設定されていた時だけ照合を行うようにします。

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

さて、では試してみましょう。

altテキスト

今度はIDもパスワードも分からなくなりました。

ただし、パスワードについてはもう少々考慮が必要です。ログインできなかった時の画面を右クリックしてソースを確認してみましょう。

altテキスト

このように、ソースを見られれば入力したパスワードが知られてしまいます。例えばちょっと席を離れた隙に同僚がソースを覗いてパスワードを知り、その人になりすまして悪いことをして罪をなすりつける、ということがあるかもしれません。

ですから通常は、パスワード文字列は復元しません。それは出来ないからではなく、すると危険だからなのです。

今回のコードも、パスワードについては

<span class="login-label">パスワード</span> : <input type="password" id="pass-field" name="pass" value=""><br>

としておくほうが良いでしょう。

さあ、これでログインフォームはOKでしょうか? ダメですね。多分操作してみて既に気づいている人がいるかもしれませんが、アドレスバーに

http://localhost/login.php

と、直接入力してみましょう。

altテキスト

下の Notice については、この画面には user_id が渡されなくなったので出ているエラーだということはなんとなくわかると思います。

ですが大きな問題なのはそこではなく、ログイン認証に成功していないのにこの画面が開けてしまうことです。認証処理を index.php に全部移動したので当たり前ですね。

これはぜひとも修正しなければなりません。

それからもう一つ、少々問題があります。ログインフォームを開いて、IDに以下の文字列を入力、パスワードは適当に入力して送信してみましょう。

"><script>alert(0);</script>

こんな画面が開いたはずです。

altテキスト

これは一体なんでしょうか? 0という文字が表示されていますね。

alert(0);

こいつが原因でありそうなことは何となくわかるかと思います。これはJavaScriptの命令で、ダイアログを開いて()内の値を出力する命令です。

これの何がまずいのかおわかりになりますか? 今回はただ 0 と出力するだけだから問題なさそうですが、もし仮にここにあなたのパソコンの重要な情報を読み取って別のサーバーに送るJavaScriptが書かれたらどうなるでしょう? その場合、まず今回のように一目でわかるような alert は出してくれません。知らないうちにこっそり読み取って送信されます。

「だってIDは自分で入力するんだから、そんな変な JavaScript 書かないよ」

と思うかもしれません。しかし、SNSにこんな情報があったらどうでしょう?

「◯◯サイトの裏ワザみつけた! ID欄にこの文章入力すると持ってるポイントが100倍になるぞ!! 」

"><script>var id=$.cookie("user_id")..............

実際にこういう裏情報を装った悪意ある書き込みがSNS等で頻繁に拡散されています。先日はiPhoneの致命的なバグについて、特定の操作をするとiPhoneの動作が軽くなるという偽情報が流れました。実際にその操作をすると iPhone が立ち上がらなくなりサポートセンターに iPhone を送って修理してもらわなければならなくなるようです。危険ですね。

そんなものにあなたは引っかからないかもしれませんが、あなたが作ったサイトの利用者の誰かがひっかかるかもしれません。もしそれでユーザーの誰かが不利益を被るのなら、それはあなたの作ったサイトの落ち度でもあるのです。ですからあなたの作ったサイトが犯罪者の片棒を担ぐことがないよう、十分な安全対策を講じておく必要があります。

明日はこれらについて説明し、ログインフォームを強固なものにしていこうと思います。

今日は、お疲れ様でした。

この記事へのコメント

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


この記事に返信

このコメントに返信