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

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

A子
(もちろん「applicants」テーブルが対象ね)
んで、有効期限内かどうかのチェックをしたあと、Viewファイルである「welcome.php」を画面表示すれば良いんじゃない?

C菜

B美
(最終チェックってことで…)

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

C菜
(お試しユーザがチャットルームを作れないようにしないと~)

A子
んじゃ、まずは「src/Controller」の中にある「ApplicantsController.php」からだね
この中に「welcome」メソッドを実装するよ
まず、エラーになるパターンを考えよう
1.引数となるランダム文字列が指定されていない
2.ランダム文字列がデータベーステーブルにヒットしない 3.有効期限(申込みから1時間以内)を過ぎている |
…くらいかな?

C菜
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子

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())->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美
(具体的に言えば「Salt付きハッシュ」において、Salt値が毎回異なるってことなんだけど…)

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

B美

A子
何それ…

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

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

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美

A子

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菜
で、先ほどのパスワードを入力すると~

↓


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


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

A子
有効?無効?

B美
有効同値とは、正しい値の範囲のこと
無効同値とは、誤った値の範囲のことよ

C菜

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

A子

C菜

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

A子