パスワード認証でログイン処理を実装

html5レベル1試験、合格しました!

これでひとまず安心です。試験の最後は祈りながら終了ボタンを押下しました(笑)

今回は、PHPによる会員制webサイトのパスワード認証を具体的にまとめておきます。

htmlで表示画面づくり(view)

まずはログインフォームページを作ります。今回は流れ(特にパスワード認証などのセキュリティ系)に重きを置きたいので、こちらはお好みで。

<h1>ログイン画面</h1>
      <form action="login_done.php" method="post">
        <input type="text" name="id" placeholder="ユーザーID" required>
        <input type="password" name="password" placeholder="パスワード" required>
        <input type="submit" value="ログイン">
      </form>

次のような表示になります。

フォームの出力先の「login_done.php」で具体的な処理を記述していきます。そのほとんどがセキュリティチェックです。後述します。

DBサーバーで会員データベースを用意(model)

次に、データベースを用意します。認証用のid名とパスワードを保存しておくテーブルです。

カラム
user_idtext
passwordtext
ユーザーテーブル

最低限これだけあれば大丈夫です。あとは必要に応じてメールアドレス、ニックネームなどを追加していきます。

今回は、パスワード認証の流れを扱いたいのでデータベース関連の設定・制作はパスします。

ログイン情報入力からの一連の流れ

今回実装するのがこちらです。

  • 不正な侵入の防止
    • POSTメソッドのチェック
    • CSRF対策
  • バリデーションチェック
    • htmlの機能
    • 正規表現によるチェック
  • パスワード照合
    • ハッシュ値を使用

不正な侵入の防止

パスワード認証はプログラミングだけで実装できる認証方式なので指紋認証やIDカードのチェックほどの効力はありません。つまり、パスワードを知っているひとなら誰でも突破できてしまう程度です。

なので、何の施策も施さなければ、悪意のあるアクセスを許してしまうと辞書型攻撃や、ブルートフォースアタックによってモノの数時間で突破されてしまいます。

そのために、正規に用意されたフォーム画面(上図のような)からのアクセスしか許可しない設定にします。

// 不正な侵入を検知した時に呼び出される関数
function attension(){
  $date = date('Y-m-d-His');
  // 記録を残す場合こんな感じ、もっと記録するデータを整えてもいいかも
  file_put_contents("/var/log/attension{$date}.json", json_encode($_SERVER));
  // 警告文だけ残しておく
  die('警告:不正な処理を検知した場合、ユーザー送信元情報を管理者に通達したのちログとして残ります。');
}

一応、こんな感じの関数を作成しました。不正なアクセスがあればattention()が作動します。

POSTメソッドのチェック

まずはリクエストメソッドからフィルタリングしていきます。

// 通信がPOST以外はブロック
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
  attension();
}

CSRF対策

CSRFとは、クロスサイトリクエストフォージェリの略で、偽物のサイトのフォームからパスワードを入力させてアクセスをさせ、パスワードを盗み取る攻撃です。

検知した場合、早急にURLや必要な値を変更し、ユーザーにはパスワードの変更を催促する必要があります。

送信フォームの送信先URLも本来であれば推測されないようにjavascriptで動的に呼び出すか、webフレームワークの機能を用いて指定すべきで、直接記述してはいけません。

// CSRF対策を実装
if (
  !preg_match(PATTERNS['referer'], $_SERVER['HTTP_REFERER']) |
  $_SESSION['check'] !== $_POST['check']
) {
  attension();
}
// ワンタイムトークンのデータは残さない
unset($_SESSION);

バリデーションチェック

htmlの機能

htmlにはフォームに必要なバリデーション機能が備わっています。

  • type属性
  • require属性
  • pattern属性
  • max属性
  • min属性

だいたいこの5つでバリデーションチェックは済んでしまいます。

ただし、これは送信前にチェックして、わざわざ間違った情報を送信させないようにさせる配慮なので、ユーザビリティの向上にはなりますが、セキュリティ的にみればサーバー側でもしっかりバリデーションチェックをすべきでしょう。

正規表現によるチェック

こちらが、サーバー側のバリデーションチェックです。

  • 不正な文字は入っていないか
  • サイズは適切か
  • 条件を満たしているか

だいたいこんなところをチェックします。

// idとパスワードに不正な文字が入っていたり、条件に一致しなかったら
if (!preg_match(PATTERNS['id'], $_POST['id']) | !preg_match(PATTERNS['pass'], $_POST['password'])) {
  header("Location: {$_SERVER['HTTP_REFERER']}?deny=1");
  exit;
}

もし間違った書き方(全角文字や記号など)があったらログインページに返します。その時にエラーメッセージを表示するためにクエリーパラメーター[?deny=1]を入れました。

パスワード照合

以上をクリアしてようやくDBサーバーとの接続を開始します。

パスワード参照の前にユーザー情報の有無をチェックします。たまに、「ユーザー名かパスワードが間違っています」と表示されるwebサイトありますが、どっちだよ!って突っ込みたくなるので、僕は別々にメッセージを分けたい派です。

require_once ''; /* データベース接続ファイル */
$db = new Db();  /* データベースモデルのオブジェクト */


// 登録されていないユーザー名だったら
$user_id = $db->getUserId($user_id);
if (empty($user_id)) {
  header("Location: {$_SERVER['HTTP_REFERER']}?deny=2");
}

データベースの云々は割愛します。getUserId()メソッドは、ユーザーのIDを見つけてくる関数だとします。

もしなかったら、[?deny=2]として返します。

次にパスワードチェック。

ハッシュ値を使用

// パスワードが違ったら
$password = $db->getUserPass($user_id);
if (password_verify($_POST['password'], $password)) {
  header("Location: {$_SERVER['HTTP_REFERER']}?deny=3");
} else {

  // 最終的にログインできたらユーザーホーム画面へ遷移
  $_SESSION['user_id'] = $user_id;
  header('Location: https://sample.com/home');
}

もともと、パスワードを登録する時にハッシュ値を使用して登録します。hash(“password")などの関数が使えたはずです。

getUserPass()メソッドでハッシュ化されたパスワードを取得して、それを送信されたパスワードのハッシュ値で照らし合わせることでチェックできます。

間違っていたら[?deny=3]として返してあげます。

そしてようやく、最終的にセッション情報を入れることでログインできます。

パターン

前もってパターンのサンプルを用意しておきました。

const PATTERNS = [
  'referer' => '|^https?://sample.com.*$|',
  'id' => '/^[a-zA-Z][a-zA-Z0-9_-]{5,29}$/',
  'pass' => '/^[a-zA-Z0-9]{8,30}$/'
];

まとめ

<?php
  session_start();
  function random($length = 8)
  {
      return substr(base_convert(hash('sha256', uniqid()), 16, 36), 0, $length);
  }
  $check = random(48);
  $_SESSION['check'] = $check;
 ?>


<form action="login_done.php" method="post">
        <input type="hidden" name="check" value="<?=$check ?>">
        <input type="text" name="id" placeholder="ユーザーID" required>
        <input type="password" name="password" placeholder="パスワード" required>
        <input type="submit" value="ログイン">
      </form>
<?php
session_start();

// 不正な侵入を検知した時に呼び出される関数
function attension(){
  $date = date('Y-m-d-His');
  // 記録を残す場合こんな感じ、もっと記録するデータを整えてもいいかも
  file_put_contents("/var/log/attension{$date}.json", json_encode($_SERVER));
  // 警告文だけ残しておく
  die('警告:不正な処理を検知した場合、ユーザー送信元情報を管理者に通達したのちログとして残ります。');
}

const PATTERNS = [
  'referer' => '|^https?://sample.com.*$|',
  'id' => '/^[a-zA-Z][a-zA-Z0-9_-]{5,29}$/',
  'pass' => '/^[a-zA-Z0-9]{8,30}$/'
];

// 通信がPOST以外はブロック
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
  attension();
}

// CSRF対策を実装
if (
  !preg_match(PATTERNS['referer'], $_SERVER['HTTP_REFERER']) |
  $_SESSION['check'] !== $_POST['check']
) {
  attension();
}
// ワンタイムトークンのデータは残さない
unset($_SESSION);

// idとパスワードに不正な文字が入っていたり、条件に一致しなかったら
if (!preg_match(PATTERNS['id'], $_POST['id']) | !preg_match(PATTERNS['pass'], $_POST['password'])) {
  header("Location: {$_SERVER['HTTP_REFERER']}?deny=1");
  exit;
}


require_once ''; /* データベース接続ファイル */
$db = new Db();  /* データベースモデルのオブジェクト */


// 登録されていないユーザー名だったら
$user_id = $db->getUserId($user_id);
if (empty($user_id)) {
  header("Location: {$_SERVER['HTTP_REFERER']}?deny=2");
}

// パスワードが違ったら
$password = $db->getUserPass($user_id);
if (password_verify($_POST['password'], $password)) {
  header("Location: {$_SERVER['HTTP_REFERER']}?deny=3");
} else {

  // 最終的にログインできたらユーザーホーム画面へ遷移
  $_SESSION['user_id'] = $user_id;
  header('Location: https://sample.com/home');
}