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

アクティベートによるユーザー登録

前回の、『FuelPHPで会員管理(アプリ編その1)』では、Adminユーザーによるユーザー登録を行いましたが、ユーザー数が多い場合、全ての登録をAdminが行うのには、無理があります。かといってユーザー登録ページをフリーにすれば、ボットプログラムによる大量スパム登録が発生する可能性が高いです。

1. やはり、スパム対策はCAPTCHAがいいのかなと思い、CAPTCHAによるユーザー登録について調べてみたら、googleの『reCAPTCHA』を99%の確率で突破するスクリプト迄登場したみたいなので、CAPTCHAの設置は労力の割に効果が薄いのではないかと思うようになりました(そこは天下のGoogleですので、すぐに強化版を出して対抗したみたいですが)。

2. メールアドレス必須で会員サイトを作成しているのであれば、登録時点で、メールを自動送信して、アクティベートする方法が一番確実だと思い、アクティベートプログラムを作成してみることにしました。

ユーザー仮登録用のビューファイルの作成

3. それでは、まずUser仮登録用のビューファイルを作成します。

app/views/user/create.php

<div class="row">
<div class="span6 offset3">
<h2 style="text-align:center">新規ユーザ登録</h2>
<?php echo Form::open(array('name'=>'create','method'=>'post','class'=>'form-horizontal')); ?>
<?php echo '<div class="alert-error">'.Session::get_flash('error').'</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">Eメール</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');?>
</div>
</div>
<div class="form-actions">
<?php echo Form::submit('submit','新規登録',array('class' => 'btn btn-primary span3'));?>
</div>
<?php //echo Form::hidden('group','-1');?>
<!-- hiddenでgroupの数値をPOSTするのは危険ですので削除します -->
<?php echo Form::close();?>
</div><!--/span4 offset2-->
</div><!--/row-->

Admin用のcreateビューファイルと大して違いはありませんが、Admin用のcreateファイルはパーミッションを作成者が選択できるようになっていました。User用のcreateファイルはgroupのパーミッションは自動的に-1がhiddenデータとして送信されます(27行目)。このパーミッション-1のユーザー(Banned)は、全てのアクションを禁止するように設定します。

User用のcreateビューファイルではgroupの数値は送信しません。createアクションで、自動的に数値(-1)を代入します。

ログイン不要なアクションの追加

4. 只、このままでは、User仮登録用のページはbeforeアクションで、弾かれます(ログイン以外は拒否しています)。そこで、beforeアクションを下記のように修正します。

app/classes/controller/user.php

//beforeアクション
 public function before(){
 parent::before();
 //許可するアクション
 $action=array('login','create');
 //アクティブなアクション
 $active=Request::active()->action;
 //ログインしていなくて、許可アクション以外は
 if(!Auth::check() and !in_array($active,$action)){
 //ログインページへ移動
 Response::redirect('user/login');
 }

これで、一応loginアクションとcreateアクションは、ログイン無しでアクセスできるようになりました。login無しで許可するアクションが増えたら、5行目の配列に追加登録すればアクセスできるようになります。

rolesの作成

5. ここで、Configファイルのsimpleauth.phpを修正します。64行目あたりの、rolesを設定します。まず、全てのユーザーに許可するアクション、一般ユーザーに許可するアクション、全てのアクションを許可するAdmin、全てのアクションを禁止するBannedを作成します。

app/config/simpleauth.php

'roles' => array(
 //全てのユーザーに許可するアクション
 '#' => array('user' => array('login','logout')),
 //一般ユーザーに許可するアクション
 'user' => array('user' => array('index','create')),
 //Adminユーザーは全てを許可
 'admin' => true,
 //Bannedユーザーは全てを禁止
 'banned' => false,
 ),

名前が重複して分かりづらいと思いますので、解説します。’#’は全てのrolesを指定します。このrolesはConfigファイルsimpleauth.phpのgroups配列の中のrolesに匹敵します。5行目の最初のuserはrolesのuserを指します。次のuserは、コントローラ名を指します。つまり、5行目は、rolesのuserにuserコントローラのaction_createを許可するということです。下位のUserでアクションを許可していれば、上位のModeretarとAdministratorにも許可しているということになります。

createアクションの作成

6. createアクションを下記のように作成します。結構長いですが、こまめにコメントを記述していますので、コメントを読んでいただければ、ほぼ分かると思います。

app/classes/controller/user.php

public function action_create(){
 //POST送信なら
 if(Input::method() == 'POST'){
 //バリデーションの初期化
 $val=Model_User::validate('create');
 $val->add_field('email', 'Eメール', 'required|valid_email');
 //バリデーションOKなら
 if($val->run()){
 //POSTデータを受け取る
 $username=Input::post('username');
 $email=Input::post('email');
 $password=Input::post('password');
 //groupのidを指定
 $group=-1;
 //重複確認
 $username_count=Model_User::find()->where('username',$username)->count();
 $email_count=Model_User::find()->where('email',$email)->count();
 //ユーザー名が重複していたら
 if($username_count>0){
 Session::set_flash('error', 'ユーザー名が重複しています');
 Response::redirect('user/create');
 //Eメールアドレスが重複していたら
 }elseif($email_count>0){
 Session::set_flash('error', 'Eメールアドレスが重複しています');
 Response::redirect('user/create');
 }
 //Authのインスタンス化
 $auth=Auth::instance();
 //もしユーザーが仮登録(Bannedでの登録)されたら
 if($auth->create_user($username,$password,$email,$group)){
 //作成日のタイムスタンプを取得します
 $created=Model_User::find('first',array(
 'where'=>array('email'=>$email)))->created_at;
 //メール本文の作成
 $body='<h2>ようこそWinRoad徒然草へ</h2>';
 $body.='<p>WinRoad徒然草への新規登録ありがとうございます。';
 $body.='登録が安全に行われるようにアクティベートをお願いします。</p>';
 $body.='<p>アクティベートするには下記のリンクをクリックして下さい。</p>';
 $body.= '<p>'.Html::anchor('user/activate/'.$email.'/'.$created,'登録完了(アクティベート)').'</p>';
 $body.='<p>48時間内にアクティベートを完了させて下さい。';
 $body.='そうでなければ、登録は無効になり、再登録する必要があります。</p>';
 $body.='<p>あなたのお名前 :';
 $body.= $username.'</p>';
 $body.='<p>あなたのEメール :';
 $body.=$email.'</p>';
 //Eメールのインスタンス化
 $sendmail=Email::forge();
 //メール情報の設定
 $sendmail->from('nakada@winroad.info','WinRoad徒然草');
 $sendmail->to($email,$username);
 $sendmail->subject('アクティベート');
 $sendmail->html_body($body);
 //メールの送信
 $sendmail->send();
 //登録成功のメッセージ
 Session::set_flash('success', '<span class="btn btn-primary span8">『'.$username.'』を仮登録しました</span><br>');
 //仮登録ページへ移動
 Response::redirect('user/provisional');
 }else{
 //データが保存されなかったら
 Session::set_flash('error', '登録されませんでした');
 }
 }
 //バリデーションNGなら
 Session::set_flash('error', $val->show_errors());
 }
 return Model_User::theme('template','user/create');
 }

 31~54行目までは、メール送信用のプログラムです。仮登録されたらデータベースから作成日のタイムスタンプを取得して、アクティベートアクションの第1引数にEメールアドレス、第2引数に作成日のタイムスタンプを送信するようにリンクを作成しました。これで指定のリンクをクリックするとuserコントローラのactivateアクションが起動するようになっています。

尚、Eメールの設定に関しては、『FuelPHPで簡単Eメール送信』を参照して下さい。

仮登録ページの作成

7. 仮登録後に移動するビューファイルを作成します。

app/view/user/provisional.php

<div class="row">
<div class="span6 offset3">
<h3><?php echo Session::get_flash('success');?></h3>
<h2>ご登録ありがとうございます。</h2>
<p>あなたのメールアドレスへメールを送信させていただきました。</p>
<p>お受け取りになりましたメールの『登録完了』のリンクをクリックしていただければ、正式な登録となります。</p>
<p>登録完了後、下記のページよりログインして下さい。</p>
<br><br>
<p><?php echo Html::anchor('user/login','ログインページへ');?></p>
</div>
</div>

8. 仮登録ページへ移動するためのprovisionalアクションを作成します。

app/classes/controller/user.php

public function action_provisional(){
 return Model_User::theme('template','user/provisional');
 }

アクティベート用アクションの作成

8. アクティベートする方法はいくつかあると思いますが、今回私は、このrolesを利用してアクティベートしたいと思います。上記のrolesの指定により、Bannedユーザーは全てのアクションを禁止されています。仮登録のユーザーはBannedで登録されます。このBannedをUserに変更することによりアクティベートしたいと思います。

app/classes/controller/user.php

public function action_activate($email,$created){
 //個人データの取得
 $active=Model_User::find('first',array(
 'where'=>array('email'=>$email,'created_at'=>$created)));
 //該当データの登録時間が48時間を過ぎていたら
 if(time()>strtotime('+2 day',$created)){
 //タイムアウトページへ
 Respose::redirect('user/timeout');
 //登録時間内で該当データがあったら
 }elseif($active->count()>0){
 //groupパーミッションの変更
 $active->group=1;
 //データの保存
 $active->save();
 //loginページへ移動
 Response::redirect('user/login');
 }
 return Model_User::theme('template','user/without');
 }

アクティベートするときに、仮登録後48時間が過ぎていたらタイムアウトページへ移動します。そして、48時間以内なら、パーミッションを-1(Banned)から1(User)に変更して、ログインページへ移動します。

9. 登録時間オーバー時のtimeoutビューと該当データが無いときのwithoutビューを下記のように作成します。

app/views/user/timeout.php

<div class="row">
 <div class="span6 offset3">
 <h2>アクティベートできませんでした。</h2>
 <p>アクティベート可能時間を過ぎてしまいました。</p>
 <p>サイトの管理人にご相談になるか、再度登録し直してください。</p>
 <p>お手数をおかけいたします。</p>
 </div>
 </div>

app/views/user/without.php

<div class="row">
 <div class="span6 offset3">
 <h2>ログイン資格がありません。</h2>
 <p>サイトの管理人にご相談になるか、再度登録し直してください。</p>
 <p>お手数をおかけいたします。</p>
 </div>
 </div>

許可アクションの追加

10. 『4.』で説明したように、userコントローラのbeforeアクションに許可アクション(provisional,activate,timeout)を追加します。修正は5行目だけです。

app/classes/controller/user.php

//beforeアクション
 public function before(){
 parent::before();
 //許可するアクション
 $action=array('login','create','provisional','activate','timeout');
 //現状のアクション
 $active=Request::active()->action;
 //ログインしていなくて、許可アクション以外は
 if(!Auth::check() and !in_array($active,$action)){
 //ログインページへ移動
 Response::redirect('user/login');
 }
 }

loginアクションの修正

11. そして、最後に肝心のログインアクションを修正します。このログインアクションで禁止ユーザーを振り分けないと、いくらrolesで禁止ユーザーを作成しても何にもなりません。

app/classes/controller/user.php

public function action_login(){
 //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/without');
 }
 //認証OKならトップページへ
 Response::redirect('user/index');
 }else{
 //認証が失敗したときの処理
 Session::set_flash('error', 'ユーザー名かパスワードが違います。');
 }
 }
 //テーマのインスタンス化
 $theme=\Theme::forge();
 //テーマにテンプレートのセット
 $theme->set_template('template');
 //テーマのテンプレートにタイトルをセット
 $theme->get_template()->set('title','Winroad徒然草');
 //テーマのテンプレートにビューとページデータをセット
 $theme->get_template()->set('content',$theme->view('user/login'));
 return $theme;
 }

修正箇所は8行目から12行目です。このhas_accessメソッドが、rolesの設定に関係します。has_accessの中はスラッシュ(/)ではなく、ドット(.)で繋ぎます。userコントローラのindexアクションにアクセス権限があるかどうかを確認します。もしログインしたユーザーがuserコントローラのindexアクションに接続する権限が無ければ、withoutページへ移動するように変更しました。

12. withoutアクションも記述しておきます。

app/classes/controller.user.php

public function action_without(){
 //強制ログアウト
 Auth::logout();
 return Model_User::theme('template','user/without');
 }

13. これで、簡単な方法ではありますが、Eメールアドレスと作成時間のタイムスタンプでアクティベートする方法を作成してみました。セキュリティを考えるなら、もう一工夫した方がいいかもしれませんが、とりあえずこれでもボットプログラムによる不正登録を防ぐことが出来るのではないでしょうか。

本日は以上です。

 本日の修正

Kenjis様より、『hiddenで渡しているgroupを1とか100とかに改竄してPOSTすれば、botでも登録できるのではないでしょうか?』とのご指摘がありました。確かにhiddenの値を改竄してPOSTすれば、adminとしての登録も可能です。そこで、hiddenでは、groupの数値を渡さずに、createアクションでgroupに数値を代入するように修正したいと思います。

  1. app/views/user/create.phpの27行目を削除します。
  2. action_createの14行目を『$group=Input::post(‘group’)』から『$group=-1;』に修正します。

 

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

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

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

トラックバック

コメント

  1. kenjis より:

    hiddenで渡しているgroupを1とか100とかに改竄してPOSTすれば、botでも登録できるのではないでしょうか?

    • nakada より:

      Kenjis様
      早速のご指摘ありがとうございます。
      確かにhiddenでgroupのID(数値)を送信するのは危険ですね。
      createアクションでgroupにIDを付与するように修正します。


コメントをどうぞ

このページの先頭へ