FuelPHPで会員管理(アプリ編その4)

パスワードのユーザーによる再発行

前回は、ユーザーのパスワードを変更して自動的に新しいパスワードを発行するプログラムを作成しましたが、他人に勝手にパスワード申請のボタンをクリックされる可能性があります。勝手にパスワードを変更されるのは面倒なので、自分自身でパスワードを変更できるように修正したいと思います。

1. 今回は、アクションメソッドを2種類作成します。まず、パスワード再発行申請用のapplyアクション、そして申請メールを受信して、実際に再発行手続きを取るrepassアクション、この2つのアクションメソッドで構成したいと思います。

2. 前回と同様、他人のユーザー名とEメールアドレスを知っている人に勝手に再発行手続きを取られては困りますので、ワンタイムパスワードも作成したいと思います。

3. 通常のワンタイムパスワードとは違うかもしれませんが、私が考えているワンタイムパスワードは、パスワード再発行申請時に発行してメール添付し、メールのリンクをクリックした時(又は、再発行手続きを完了したとき)に、変更されるようなワンタイムパスワードを作成したいと思います。

4. usersテーブルに元々ある、login_hashをワンタイムパスワードに代用しようとも考えたのですが、login_hashはloginしない限り変更されることはありません。そこで、usersテーブルに新しくonepassというフィールドを追加します。

ALTER TABLE `users` ADD `onepass` VARCHAR( 255 ) NOT NULL ;

5. 尚、前回も言ったようにログインしなくてもアクセス可能なアクションがかなり増えてきました。そこで、今回から、userフォルダを作成し、その中にLoginコントローラを追加したいと思います。

6. Loginモデルを作成してもいいのですが、Userモデルで使い回しできると思いますので、特にLoginモデルは作成しません。只、Userモデルに新しいプロパティ『onepass』は、追加して下さい。

7. users/login.phpビューファイルを削除し、user/login/index.phpファイルを作成します。20行目のリンク先が変更になったぐらいです。

app/views/user/login/index.php

<div class="row">
 <div class="span7 offset2 hero-unit">
 <h2 style="text-align:center"><?php echo Asset::img('winlogo.png');?></h2><br>
 <?php echo Form::open(array('name'=>'login','method'=>'post','class'=>'form-horizontal')); ?>
 <?php echo '<div class="alert-error"><p>'.Session::get_flash('error').'</p></div>'?>
 <div class="control-group">
 <label class="control-label" for="username">ユーザー名</label>
 <div class="controls">
 <?php echo Form::input('username',Input::post('username'));?>
 </div>
 </div>
 <div class="control-group">
 <label class="control-label" for="password">パスワード</label>
 <div class="controls">
 <?php echo Form::password('password');?>
 </div>
 </div>
 <?php echo Form::submit('submit','ログイン',array('class' => 'btn btn-primary btn-large span7'));?>
 <?php echo Form::close();?>
 <?php echo Html::anchor('user/login/apply','パスワードをお忘れですか?');?>
 </div><!--/span7 offset2-->
 </div><!--/row-->

8. applyビューファイルを下記のように記述します。これは、前回のautorepassビューファイルと同じですね。

app/views/user/login/apply.php

<div class="row">
 <div class="span7 offset2 hero-unit">
 <h2 style="text-align:center"><?php echo Asset::img('winlogo.png');?></h2><br>
 <p>パスワードの再発行手続きを行いますので、あなたのお名前とご登録のEメールアドレスを入力して下さい。</p>
 <?php echo Form::open(array('name'=>'apply','method'=>'post','class'=>'form-horizontal')); ?>
 <?php echo '<div class="alert-error"><p>'.Session::get_flash('error').'</p></div>'?>
 <div class="control-group">
 <label class="control-label" for="username">あなたのお名前</label>
 <div class="controls">
 <?php echo Form::input('username',Input::post('username'));?>
 </div>
 </div>
 <div class="control-group">
 <label class="control-label" for="email">メールアドレス</label>
 <div class="controls">
 <?php echo Form::input('email',Input::post('email'));?>
 </div>
 </div>
 <?php echo Form::submit('submit','再発行手続き',array('class' => 'btn btn-primary btn-large span7'));?>
 <?php echo Form::close();?>
 </div><!--/span7 offset2-->
 </div><!--/row-->

9. とりあえず、ここまでは、私事ですので、皆さんは、userコントローラの中に作成しても構わないと思います。尚、重複しますので、userコントローラの中のloginアクションは削除しました。

Loginコントローラの作成

10. Loginコントローラとindexアクションを下記のように作成します。。コントローラ名がController_User_Loginになっているところに注意して下さい。

app/classes/controller/user/login.php

<?php
 class Controller_User_Login extends Controller{
 //ログイン
 public function action_index(){
 //POST送信なら
 if(Input::method() == 'POST'){
 //Authのインスタンス化
 $auth=Auth::instance();
 //資格情報の取得
 if($auth->login(Input::post('username'),Input::post('password'))){
 //禁止ユーザーならWithoutページへ
 if(!$auth->has_access('user.index')){
 //Withoutページへ
 Response::redirect('user/login/without');
 }
 //認証OKならトップページへ
 Response::redirect('user/index');
 }else{
 //認証が失敗したときの処理
 Session::set_flash('error', 'ユーザー名かパスワードが違います。');
 }
 }
 return Model_User::theme('template','user/login');
 }
 }

11. パスワード変更申請用のapplyアクションを下記のように追加します。

app/classes/controller/user/login.php

//パスワードの再発行申請
 public function action_apply(){
 //POST送信なら
 if(Input::method() == 'POST'){
 //受信データの整理
 $username=Input::post('username');
 $email=Input::post('email');
 //登録ユーザーの有無の確認
 $user_count=Model_User::find()
 ->where('username',$username)->where('email',$email)
 ->count();
 //該当ユーザーがいれば
 if($user_count>0){
 //ユーザーの特定
 $user=Model_User::find('first',array(
 'where'=>array('email'=>$email)));
 //ワンタイムパスワードの作成
 $onepass=md5(time());
 //ワンタイムパスワードの保存
 $user->onepass=$onepass;
 $user->save();
 //送信データの整理
 $data['onepass']=$onepass;
 $data['username']=$username;
 $data['email']=$email;
 $data['anchor']='user/login/repass/'.$onepass;
 $body=View::forge('user/email/repass',$data);
 //Eメールのインスタンス化
 $sendmail=Email::forge();
 //メール情報の設定
 $sendmail->from('nakada@winroad.info','WinRoad徒然草');
 $sendmail->to($email,$username);
 $sendmail->subject('パスワードの再発行');
 $sendmail->html_body($body);
 //メールの送信
 $sendmail->send();
 //再発行案内ページへ移動
 return Model_User::theme('template','user/login/repass-info');
 //該当ユーザーがいなければ
 }else{
 //エラー表示
 Session::set_flash('error', '該当者がいません。');
 }
 }
 //テーマの表示
 return Model_User::theme('template','user/login/apply');
 }

ワンタイムパスワードですが、文字列のmd5ハッシュ値を計算するPHPのmd5関数で、現在時刻のタイムスタンプを変換しています。その変換した値をusersテーブルのonepassフィールドに一端保存し、それと同時にメールのリンクに添付するようにしました。

12. 再発行申請用のメール本文を下記のように記述します。

app/views/user/email/repass.php

<h2><?php echo $username;?> 様</h2>
<p>パスワード再発行の依頼をお受けいたしました。
下記のリンクをクリックして、新しいパスワードを作成して下さい。</p>
<p><?php echo Html::anchor($anchor,'パスワードの再発行')?></p>
<p>WinRoad徒然草ユーザーによりパスワードの紛失手続きが取られましたので、
このメールを発行いたしました。
これは新しいパスワードを作成する手続きの一部です。
もしあなたが新しいパスワードの発行を依頼していないのであれば、
このメールを無視して下さい。
以前のパスワードのままでご利用になれます。</p>
<p>ありがとうございました。</p>

 

13. 再発行処理用のrepassアクションを下記のように記述します。

app/classes/user/login.php

//パスワードの再発行
 public function action_repass($onepass){
 //ワンタイムパスワードが一致しなければ
 if(Model_User::find()->where('onepass',$onepass)->count()==0){
 //再発行手続き中止ページへ移動
 Response::redirect('user/login/without');
 }
 //POST送信なら
 if(Input::method() == 'POST'){
 //バリデーションの初期化
 $val=Model_User::validate('repass');
 $val->add_field('email', 'Eメール', 'required|valid_email');
 //バリデーションOKなら
 if($val->run()){
 //ユーザー情報の取得
 $user=Model_User::find('first',array(
 'where'=>array('onepass'=>$onepass)));
 //受信データの整理
 $username=Input::post('username');
 $email=Input::post('email');
 $password=Input::post('password');
 //ユーザー名もメールアドレスも一致すれば
 if($username==$user['username'] && $email==$user['email']){
 //ワンタイムパスワードの変更
 $user->onepass=md5(time());
 $user->save();
 //パスワードの変更
 $auth=Auth::instance();
 //一旦パスワードをリセット
 $old=$auth->reset_password($username);
 //再度ユーザー申告のパスワードに変更
 $auth->change_password($old,$password,$username);
 //ログインページへ移動
 Response::redirect('user/login');
 //該当者がいなければ
 }else{
 //エラー表示
 Session::set_flash('error', '該当者がいません。');
 }
 }
 //バリデーションNGなら
 Session::set_flash('error', "<p>".$val->show_errors()."</p>");
 }
 //テーマの表示
 return Model_User::theme('template','user/login/repass');
 }

パスワードの変更は、chenge_passwordメソッドで変更できるのですが、chenge_passwordメソッドは、古いパスワードが必要です。パスワードを紛失して再発行手続きをしているのに、古いパスワードを入力できるわけがありません。そこで、一端パスワードをリセットして、リセット時に作成されたパスワードを古いパスワード、ユーザーが入力したパスワードを新しいパスワードとしてパスワードの変更手続きを行っています。パスワードを変更した時点で、ワンタイムパスワードも変更されますので、再度同じメールのリンクをクリックしてもwithooutページへ移動されます。withoutアクションとwithoutビューは簡単ですので、自分で作成して下さい。

セキュリティの強化

14. これで、一応ユーザー自身でパスワードを変更するプログラムは完成したのですが、ワンタイムパスワードといえども、リンクに貼り付けるのが不安な方は、もう1種類(簡単な4文字程度のリセットコード)を発行して、メール添付し(リンクには添付しないで)、再発行申請時にそのリセットコードも入力させるようにすれば、セキュリティ的に不安が無くなるのではないでしょうか。下記に例を記述しておきます。

app/classes/user/login.php

/************************************************
 * パスワードの再発行申請
 *
 * ①POSTデータに該当者がいれば
 * ②リセットコードとワンタイムパスワードを作成
 * ③メールにリセットコードとワンパスを添付
 * ④メールのリンクにワンパスを添付
 *
 ***********************************************/
 public function action_apply(){
 //POST送信なら
 if(Input::method() == 'POST'){
 //受信データの整理
 $username=Input::post('username');
 $email=Input::post('email');
 //登録ユーザーの有無の確認
 $user_count=Model_User::find()
 ->where('username',$username)->where('email',$email)
 ->count();
 //該当ユーザーがいれば
 if($user_count>0){
 //ユーザーの特定
 $user=Model_User::find('first',array(
 'where'=>array('email'=>$email)));
 //リセット(再発行)コードの作成
 $reset=mb_substr($user->last_login,-4);
 //ワンタイムパスワードの作成と保存
 $onepass=md5(time());
 $user->onepass=$onepass;
 $user->save();
 //送信データの整理
 $data['reset']=$reset;
 $data['onepass']=$onepass;
 $data['username']=$username;
 $data['email']=$email;
 $data['anchor']='user/login/repass/'.$onepass;
 $body=View::forge('user/email/repass',$data);
 //Eメールのインスタンス化
 $sendmail=Email::forge();
 //メール情報の設定
 $sendmail->from('nakada@winroad.info','WinRoad徒然草');
 $sendmail->to($email,$username);
 $sendmail->subject('パスワードの再発行');
 $sendmail->html_body($body);
 //メールの送信
 $sendmail->send();
 //再発行案内ページへ移動
 return Model_User::theme('template','user/login/repass-info');
 //該当ユーザーがいなければ
 }else{
 //エラー表示
 Session::set_flash('error', '該当者がいません。');
 }
 }
 //テーマの表示
 return Model_User::theme('template','user/login/apply');
 }

 /************************************************
 * パスワードの再発行手続き
 *
 * ①ワンパスのチェックNGなら再発行手続きの中止
 * ②バリデーションチェック
 * ③リセットコードのチェック
 * ④ワンパスデータとPOSTデータが一致すれば
 * ⑤ワンパスの変更
 * ⑥ユーザーパスワードの変更 
 *
 ************************************************/
 public function action_repass($onepass){
 //ワンタイムパスワードが一致しなければ
 if(Model_User::find()->where('onepass',$onepass)->count()==0){
 //再発行手続き中止ページへ移動
 Response::redirect('user/login/without');
 }
 //POST送信なら
 if(Input::method() == 'POST'){
 //バリデーションの初期化
 $val=Model_User::validate('repass');
 $val->add_field('email', 'Eメール', 'required|valid_email');
 //バリデーションOKなら
 if($val->run()){
 //ユーザー情報の取得
 $user=Model_User::find('first',array(
 'where'=>array('onepass'=>$onepass)));
 //last_loginの下4桁を取得
 $last_login=mb_substr($user['last_login'],-4);
 $reset=Input::post('reset');
 //リセットコードが一致すれば
 if($last_login==$reset){
 //受信データの整理
 $username=Input::post('username');
 $email=Input::post('email');
 $password=Input::post('password');
 //ユーザー名もメールアドレスも一致すれば
 if($username==$user['username'] && $email==$user['email']){
 //ワンタイムパスワードの変更
 $user->onepass=md5(time());
 $user->save();
 //Authのインスタンス化
 $auth=Auth::instance();
 //一旦パスワードをリセット
 $old=$auth->reset_password($username);
 //再度ユーザー申告のパスワードに変更
 $auth->change_password($old,$password,$username);
 //ログインページへ移動
 Response::redirect('user/login');
 }else{
 //該当者がいなければ
 Session::set_flash('na', '<p><span class="alert-error">該当者がいません</span></p>');
 }
 }else{
 //リセットコードが一致しない場合
 Session::set_flash('reset', '<p><span class="alert-error">再発行コードが一致しません</span></p>');
 }
 }
 //バリデーションNGなら
 Session::set_flash('error', "<p>".$val->show_errors()."</p>");
 }
 //テーマの表示
 return Model_User::theme('template','user/login/repass');
 } 

 /************************** 
 * Withoutページ 
 **************************/
 public function action_without(){
 return Model_User::theme('template','user/login/without');
 }

リセットコードですが、既存のデータで使い回しできるデータがないか捜してみたら、last_loginのタイムスタンプがありました。これは、ログインするたびに変更されるので、リセットコードに代用できると思い、last_loginの下4桁をリセットコードに利用することにしました。

15. メール送信用のrepassビューファイルを下記のように修正します。

app/views/user/email/repass.php

<h2><?php echo $username;?> 様</h2>
 <h3>パスワードの再発行手続き</h3>
 <p>パスワード再発行の依頼をお受けいたしました。
 下記のリンクをクリックして、新しいパスワードを作成して下さい。</p>
 <p>再発行時には、下記のコードが必要です。</p>
 <h4>再発行コード:<?php echo $reset;?></h4>
 <h4><?php echo Html::anchor($anchor,'新しいパスワードの作成')?></h4>
 <p>WinRoad徒然草ユーザーによりパスワードの紛失手続きが取られましたので、
 このメールを発行いたしました。
 これは新しいパスワードを作成する手続きの一部です。
 もしあなたが新しいパスワードの発行を依頼していないのであれば、
 このメールを無視して下さい。
 以前のパスワードのままでご利用になれます。</p>
 <p>ありがとうございました。</p>

16. repassビューを下記に記述します。

app/views/user/login/repass.php

<div class="row">
 <div class="span7 offset2 hero-unit">
 <h2 style="text-align:center"><?php echo Asset::img('winlogo.png');?></h2><br>
 <p>パスワードの再発行を行いますので、再発行コード、あなたのお名前、Eメールアドレスと新しいパスワードを入力して下さい。</p>
 <?php echo Form::open(array('name'=>'repass','method'=>'post','class'=>'form-horizontal')); ?>
 <?php echo '<div class="alert-error"><p>'.Session::get_flash('error').'</p></div>'?>
 <div class="control-group">
 <label class="control-label" for="reset">再発行コード</label>
 <div class="controls">
 <?php echo Form::input('reset',Input::post('reset'));?>
 </div>
 <?php echo Session::get_flash('reset');?>
 </div>
 <div class="control-group">
 <label class="control-label" for="username">あなたのお名前</label>
 <div class="controls">
 <?php echo Form::input('username',Input::post('username'));?>
 </div>
 </div>
 <div class="control-group">
 <label class="control-label" for="email">メールアドレス</label>
 <div class="controls">
 <?php echo Form::input('email',Input::post('email'));?>
 </div>
 </div>
 <div class="control-group">
 <label class="control-label" for="password">新しいパスワード</label>
 <div class="controls">
 <?php echo Form::password('password',Input::post('password'));?>
 </div>
 <?php echo Session::get_flash('na');?>
 </div>
 <?php echo Form::submit('submit','パスワードの再発行',array('class' => 'btn btn-primary btn-large span7'));?>
 <?php echo Form::close();?>
 </div><!--/span7 offset2-->
 </div><!--/row-->

17. withoutビューは適当に作成して下さい。

本日は、以上です。

このエントリーを含むはてなブックマーク Buzzurlにブックマーク livedoorクリップ Yahoo!ブックマークに登録

トラックバック&コメント

この投稿のトラックバックURL:

コメントをどうぞ

このページの先頭へ