WEBリクエストを行を行う際にページ遷移が行われると、元のページで入力した値はWEBブラウザ側では取得できなくなってしまいます。

ログイン認証に成功した場合は良いのですが、パスワードを間違えるなどして認証が失敗した場合、ユーザーは再びIDの入力等を行わなければならなくなってしまい不快な思いをすることになるでしょう。

この章では、元のページで入力した情報を遷移後のページでも引き続き利用する方法を学びましょう。

※この記事は執筆・公開から3年以上経過しています。記事の情報が古くなっている場合がありますのでご注意ください。

公開日時:2016/02/15 19:37
最終更新:2019/12/10 01:31

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

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

前回までで完成した画面

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

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

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

    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>

遷移後の画面

今度は、認証に失敗した場合にはフィールドが空にならずに文字列が入力された状態でページが開きました。

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

入力されたパスワードが見えている

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

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

これではセキュリティも何もあったものではありませんね。どうやら、この方法ではダメなようです。これが、最初のログインの時にPOSTメソッドを利用した理由です。POSTではGETの場合のように重要な情報がURLに含まれる事はありません。

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

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

画面遷移概念図

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

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

画面遷移の概念変更図

つまり、index.php からIDとパスワードをPOSTで送るのは問題ないので、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 c 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フィールドとパスワードフィールドについて、今回は

から値が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;
            }
        }
    }

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

認証処理の確認

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

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

より高度な隠蔽

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

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

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

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

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

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

http://localhost/login.php

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

残された問題点1

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

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

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

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

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

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

残された問題点2

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

alert(0);

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

これの何がまずいのかおわかりになりますか?

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

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

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

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

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

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

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

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

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

記事リンク