再読み込み防止にtokenを使う

PHPアイキャッチ

例えば新規登録ページを作る、という時に

  • 会員登録フォーム(registration_form.php)
  • 登録確認(registration_check.php)
  • 登録完了(registration_insert.php)

のように、3つページを作る方法と、フラグを使って1ページで完結させる方法があります。
かなりコード量も増えてややこしくなるので3ページに分けるべきですが、今回は勉強のためにフラグを用いて、1ページの中で3段階に遷移させる方法をとりました。

  • 会員登録フォーム($page_flg = 0)
  • 登録確認($page_flg = 1)
  • 登録完了($page_flg = 2)

コードだと、

<?php

$page_flg = 0;

//何らかの処理
$page_flg = 1;

//何らかの処理
$page_flg = 2;



?>
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  

<?php if($page_flg === 0) : ?>

  投稿画面です。
  
<?php elseif($page_flg === 1) : ?>
    
  確認画面です。
    
<?php elseif($page_flg === 2) : ?>
      
  完了画面です。

<?php endif; ?>

</body>
</html>

ここで一番詰まったのが再読み込み防止というものでした。
$page_flgを2にする処理の時、つまり確認画面から完了画面へ移す時が一番問題です。
そこで何をしているのかというと、データベースへ会員情報を登録しています。

再読み込みをすると、もう1度その処理が走るため多重登録になってしまいます。
検索すると再読み込み防止には、header()を使うべしなどと出てきます。
確かに掲示板のように投稿したらまたそのページへリダイレクトをかければいいのですが、今回の場合は
例えば$page_flg = 0の状態から $page_flg = 1になった時に
リダイレクトをかけるとまた0からやり直しになってしまいます。

下記teratailにて、興味深い話が。

CSRF 対策の tokenと兼ねているケースも多いと思います。

https://teratail.com/questions/194646

ほほう、と。
実は最近セキュリティの勉強も密かにしておりまして、
CSRF対策としてトークンを発行する、というのをやっていました。

https://html-coding.co.jp/knowhow/security/csrf/

CSRFについては上記参考にしてください。

if(!isset($_SESSION['token'])) {
$_SESSION['token'] = bin2hex(random_bytes(32));
$token = $_SESSION['token'];
}
$token = $_SESSION['token'];

このトークンを、form内に

<input type="hidden" name="token" value="<?=$token?>">

として入れておきます。
ページ移動後に、

  if ($_POST['token'] !== $_SESSION['token']){
    echo "不正アクセスの可能性あり";
    exit();
  }

としておくことで、CSRF対策ができる、というものです。

さて、前置きが長くなりましたが、これを利用して再読み込みを防止するのが多いという事だったので実装してみました。

<?php
session_start();


$page_flg = 0;

//$_SESSION['token']がない時、tokenを作り、
//更に、変数に収納。
if(!isset($_SESSION['token'])) {
  $_SESSION['token'] = bin2hex(random_bytes(32));
  $token = $_SESSION['token'];
}
//あれば、$tokenに入れる
$token = $_SESSION['token'];



//page_flg = 0からのPOSTがあったとき。
//1度目とわかるように、最初のsubmitボタンにname属性をつけておく。
if(!empty($_POST['flg0'])) {

  //POSTの中のtokenと、セッションのtokenが同じかどうかを見ている
  //違った場合、エラーとして後続処理を断つ。
  if($_POST['token0'] !== $_SESSION['token']) {
    echo 'エラーです';
    exit();
  }

  //1度目でのPOSTでも再読み込みを禁止したい場合
  //$_SESSIONをunsetし、再びトークンを生成して、$tokenに入れれば
  //POSTとSESSIONは同じになるから、次ページ移動してもエラーにならない

  unset($_SESSION['token']);
  $_SESSION['token'] = bin2hex(random_bytes(32));
  $token = $_SESSION['token'];

  $page_flg = 1;
}

//2度目のPOSTがあったとき。
if(!empty($_POST['flg1'])) {
  //POSTの中のtokenと、セッションのtokenが同じかどうかを見ている
  //違った場合、エラーとして後続処理を断つ。
  if($_POST['token1'] !== $_SESSION['token']) {
    echo 'エラーです';
    exit();
  }
  //最後のページでunsetをかける(次ページないから必要ないから)
  unset($_SESSION['token']);

  
  //DB処理か何かを書く
  
  $page_flg = 2;

}



?>
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  

<?php if($page_flg === 0) : ?>

  投稿画面です。
  <form method="post">
    <input type="hidden" name="token0" value="<?=$token?>">
    <input type="submit" name="flg0">
  </form>
  
<?php elseif($page_flg === 1) : ?>
    
    確認画面です。
    <form method="post">
      <input type="hidden" name="token1" value="<?=$token?>">
    <input type="submit" name="flg1">
  </form>
    
<?php elseif($page_flg === 2) : ?>
      
    完了画面です。


<?php endif; ?>
</body>
</html>

本来ならば、最初にtokenをSESSIONに入れといて、他のページに移動するときにunsetして、次ページでまたtokenを…という流れなのだと思いますが、
今回は1ページ内で完結させるため少しややこしくなったので書いておきました。実際には複数ページで実装するのがコードも少なく、わかりやすいのでいいと思います。