Friction River Software

  • お問い合わせ

CakePHP5入門【CakePHP5実用編⑪】ユーザ登録申込み③

B美

さて、ようやくお試しユーザ自動登録機能のラストだよ

メールに記載されたURLを使ってアクセスする「welcome」メソッドを実装してね

A子

引数として渡された「ランダム文字列」が等しく、かつ「regist_flag」が「1(申込み直後)」であるものを検索しよう
(もちろん「applicants」テーブルが対象ね)

んで、有効期限内かどうかのチェックをしたあと、Viewビューファイルである「welcome.php」を画面表示すれば良いんじゃない?

C菜

「users」テーブルへの登録も「welcome」メソッドでやっちゃうんですか~?

B美

あー、Viewビューファイルである「welcome.php」にはパスワードの入力欄を作って、申込み時に入力したパスワードを再度入力させたほうが良いかな
(最終チェックってことで…)

A子

なるほど

それじゃ、フォーム送信先のメソッドを作らないとね
(「finalCheck」メソッドという名前にしよう)

C菜

あとは「src/Controller」の中にある「ChatRoomsController.php」ですけど、「role_num」にもとづいて「チャットルームの新規作成」にも制限をかけないとですね~
(お試しユーザがチャットルームを作れないようにしないと~)

A子

OK

んじゃ、まずは「src/Controller」の中にある「ApplicantsController.php」からだね
この中に「welcome」メソッドを実装するよ

まず、エラーになるパターンを考えよう

1.引数となるランダム文字列が指定されていない
2.ランダム文字列がデータベーステーブルにヒットしない
3.有効期限(申込みから1時間以内)を過ぎている

…くらいかな?

C菜

では、エラーが無い場合を「0」として、その三つをエラー番号として定数化しましょう~

define("WELCOME_NO_ERROR", 0);    //エラー無し
define("WELCOME_ARGUMENT_ERROR_1", 1);    //引数が無い
define("WELCOME_ARGUMENT_ERROR_2", 2);    //引数がヒットしない
define("WELCOME_DEADLINE_ERROR", 3);    //有効期限切れ

で、どうでしょうか~?

それと、Viewビューファイルについては、「templates/Applicants」の中に「welcome.php」を作ってみました~




A子

うん、良いね
(エラー番号を定数化することで、0~3の数字をそのまま記述するよりも分かりやすいと思う)

それじゃ、「welcome」メソッドを実装してみよう

public function welcome($token = null)
{
    $error_num = WELCOME_NO_ERROR;
    $email = '';

    if (is_null($token)) {
        $error_num = WELCOME_ARGUMENT_ERROR_1;
    } else {
        $applicant = $this->Applicants->find()->where(['token' => $token, 'regist_flag' => 1])->first();
        if (is_null($applicant)) {
            $error_num = WELCOME_ARGUMENT_ERROR_2;
        } else {
            $email = $applicant->email;
            $time_now = DateTime::now();    //現在時刻
            $time_deadline = new DateTime($applicant->deadline, 'Asia/Tokyo');    //有効期限
            if ($time_now->i18nFormat("yyyy-MM-dd HH:mm:ss") > $time_deadline->i18nFormat("yyyy-MM-dd HH:mm:ss")) {
                $error_num = WELCOME_DEADLINE_ERROR;
            }
        }
    }

    $this->set(compact('error_num'));
    $this->set(compact('token'));
    $this->set(compact('email'));
}

あ、もちろんファイルの先頭にはuseを書くよ

use Cake\I18n\DateTime;


B美

有効期限チェックの箇所って、よく実装できたわね
(それで全く問題ないわ)

A子

いや、ChatGPTに教えてもらったんだけどね(苦笑)

B美

生成AIを使いこなしているわけだから、それはそれでOKよ

A子

んじゃ、最後は「finalCheck」メソッドだね

public function finalCheck()
{
    $success_flag = false;

    if ($this->request->is('post')) {
        $token = $this->request->getData('token');
        $email = $this->request->getData('email');
        $password = $this->request->getData('password');

        $applicant = $this->Applicants->find()->where(['token' => $token, 'regist_flag' => 1])->first();

        if (!is_null($applicant)) {
            //パスワードチェック
            if ((new DefaultPasswordHasher())->hash($password) == $applicant->password) {
                //データベース登録
                $user = $this->Users->newEmptyEntity();
                $user->email = $email;
                $user->password = $password;
                $user->handle_name = $applicant->handle_name;
                $user->role_num = GUEST;    //お試しユーザ

                if ($this->Users->save($user)) {
                    //フラグ更新
                    $applicant->regist_flag = 2;    //登録済み
                    $this->Applicants->save($applicant);

                    $success_flag = true;
                }
            }
        }
    }

    $this->set(compact('success_flag'));
}

B美

あー、テストしてみればすぐに分かることなんだけど、それでは絶対にパスワードチェックを通らないわよ

A子

え?
なんでよ!

元データが同じなら、必ず同じ値になるのがハッシュ値ってやつじゃないの?

B美

DefaultPasswordHasherクラスが生成するハッシュ値って、(hashメソッドの実行時)元データが同じであっても毎回異なる値を生成するの
(具体的に言えば「Salt付きハッシュ」において、Salt値が毎回異なるってことなんだけど…)

C菜

Saltって「塩」のことですよね~?

普通のハッシュ」と「Salt付きハッシュ」では、どこが違うんでしょうか~?

B美

まず「ハッシュ関数」が一方向性を持つってことは理解してる?

A子

「いちほうこうせい」?

何それ…

C菜

「元データからハッシュ値を得る」ことはできますけど、「ハッシュ値から元データを得る」ことはできないってことですよね~?

要するに「一方通行」ということです~

B美

C菜、正解!

もしも「パスワードを含む個人情報」が外部に流出したとしても、ハッシュ値になっていれば「本来のパスワード」が判明することはない…ってわけ

A子

うーん、なるほどねぇ

あれ?
でもさ、あらかじめ大量のランダム文字列をハッシュ化しておけば、その中から一致するもの(ハッシュ値)を探し出すことで、元データ(本来のパスワード)を知ることができるんじゃない?

B美

おっ、よく気付いたわね

それこそが「レインボーテーブル攻撃」という攻撃手法なのよ
んで、その攻撃に対処する方法が「Salt付きハッシュ」ってわけ

その方法は簡単で、元データに「適当な文字列」を付け加えてハッシュ化するだけ
(その「適当な文字列」のことをSaltと呼ぶの)

C菜

なるほどですね~

あれ?
でも、そうするとフォーム送信された平文の「パスワード」とデータベースに格納された「(パスワードの)ハッシュ値」をどうやって照合するのでしょうか~?

B美

簡単よ
DefaultPasswordHasherクラスには、照合用のメソッドがきちんと準備されてるの

if ((new DefaultPasswordHasher())->hash($password) == $applicant->password)) {

の箇所を

if ((new DefaultPasswordHasher())->check($password, $applicant->password)) {

に変えるだけでOKよ

C菜

ん~?

Saltが分からないのに、なぜ照合できるのでしょうか~?

B美

実は、hashメソッドによって生成された「ハッシュ値」の中には「Salt値」が埋め込まれているからよ

A子

それって、はたして(Salt付きのハッシュ化を行う)意味があるの?

B美

ふふ
あまり無いわね

だからこそ、個人情報が流出しないように気を付けないといけないの
(やろうと思えば、ハッシュ化されたパスワードデータから元のパスワードを調べることは可能…ってこと(苦笑))

A子

うーん、なんだか納得いかないけど、まぁ良いわ

public function finalCheck()
{
    $success_flag = false;

    if ($this->request->is('post')) {
        $token = $this->request->getData('token');
        $email = $this->request->getData('email');
        $password = $this->request->getData('password');

        $applicant = $this->Applicants->find()->where(['token' => $token, 'regist_flag' => 1])->first();

        if (!is_null($applicant)) {
            //パスワードチェック
            if ((new DefaultPasswordHasher())->check($password, $applicant->password)) {
                //データベース登録
                $user = $this->Users->newEmptyEntity();
                $user->email = $email;
                $user->password = $password;
                $user->handle_name = $applicant->handle_name;
                $user->role_num = GUEST;    //お試しユーザ

                if ($this->Users->save($user)) {
                    //フラグ更新
                    $applicant->regist_flag = 2;    //登録済み
                    $this->Applicants->save($applicant);

                    $success_flag = true;
                }
            }
        }
    }

    $this->set(compact('success_flag'));
}

で良いのよね?

B美

そういうこと

それじゃ、あとはViewビューファイルとして、「templates/Applicants」の中に「final_check.php」を作成してね

C菜

こんな感じでいかがでしょうか~?

$success_flagの値によって、メッセージ表示を切り分けてるだけですけど~

A子

うん、良いと思う

おっと忘れるところだった…
「beforeFilter」メソッドに'finalCheck'を追加しないとね

C菜

あと、「お試しユーザ」でログインした場合、チャットルームの「新規作成」「編集」「削除」ができないようにするです~

「templates/ChatRooms」の中にある「index.php」を修正して、「お試しユーザ」の場合は「チャットルーム作成」のリンクを表示しないようにしましょう~
(ちなみに、「編集」や「削除」に条件指定を追加しなかったのは必要ないからです~)

A子

あー
一応「src/Controller」の中にある「ChatRoomsController.php」のほうでも制限をかけておこうかな

private function checkGuest()
{
    if ($this->identity->role_num == GUEST) {
        $this->Flash->error(__('お試しユーザはこの機能を実行できません。'));

        return $this->redirect(['controller' => 'ChatRooms', 'action' => 'index']);
    }
}

上記のメソッドを追加して、「add」「edit」「delete」メソッドの中から呼び出すよ

$this->checkGuest();

C菜

これで完成でしょうか~?

B美

ふむ、大丈夫だと思うわよ(多分)

とりあえず、一連の流れを最初からテストしてみなさい

A子

わかったよ

まずはログインページの右下にある「お試しユーザ登録はこちら」というリンクをクリックすることからだね



C菜

適当なテストデータを入力して「登録」ボタンをクリックします~

そのあと、下の確認画面で「申込み」ボタンを押しますね~

A子

うん、大丈夫だね

申込み完了」になったよ

C菜

メールが届いたので、記載されたURLをクリックします~

で、先ほどのパスワードを入力すると~



A子

完璧だね

それじゃ、さっそくログインしてみよう

B美

うん
問題ないみたいね

あ、有効同値だけじゃなく、無効同値についてもテストしておきなさいね

A子

何それ?

有効?無効?

B美

分かりやすく言えば、正しい値でテストするだけじゃなく、間違った値でもテストしときなさいね…ってこと

有効同値どうちとは、正しい値の範囲のこと
無効同値どうちとは、誤った値の範囲のことよ

C菜

要するに、わざとエラーが出るようにして、そのエラー処理がきちんとできているかを検証しろ…ってことでしょうか~?

B美

その通り

システムを構築するにあたって、エンジニアは常に「フールプルーフ」を心がけなければならないってわけ

A子

フールって、エイプリルフール(四月馬鹿)のフール?

C菜

もしも「Fool Proof」というつづりであるならば、直訳して「馬鹿に耐える」、一般的には「馬鹿でも扱える」と訳されますね~

B美

システムを操作するユーザって、どんな馬鹿な操作をするか分かったものじゃないの

どのような(非常識な)操作をされたとしても、(プログラマは)システムが止まることなく動き続けることができるようにしなければならないのよ
それが「フールプルーフ」ってわけ

A子

なるほどねぇ