CakePHP3を触ってみました 〜パスワードのハッシュ化はどうやるんだ?〜

この記事はだいぶ前に書かれたものなので情報が古いかもしれません
石丸さんを集めてみました

この記事を三行にまとめると

CakePHP3でのログイン処理についてご覧下さい(CV:石丸謙二郎)
5000秒! それは永遠という意味ですか?(CV:石丸小唄)
パイルダーオン!!(CV:石丸博也)
トゥルットゥットゥットゥトゥ〜ルトゥ〜♪ トゥ〜トゥ〜♪(世界の車窓からのBGM)

あかつきのお宿から。今日は、CakePHP3でのログイン処理についてご覧下さい(CV:石丸謙二郎)



ということで、相も変わらずCakePHP3の話です。

Authコンポーネントを使ってのログイン処理+α。そんなに大きく変わってもいないし難しくもなっていないだろうと思って油断していたら、意外と詰まって時間を取られてしまいました。

今日はそれやってみたいと思います。



基本的な設定

フォームでログイン処理を行う場合に、CakePHP2を使っていれば、コントローラーのbeforeFilter()とかに、こんなような設定を書いているんじゃないかと思います。

public function beforeFilter() {
  AuthComponent::$sessionKey = 'Auth.User';//セッションのキー
  $this->Auth->loginAction = '/users/login';//ログイン画面URL
  $this->Auth->autoRedirect = '/users/index';//ログイン後のリダイレクト先
  $this->Auth->authenticate = array(
    'Form' => array(
      'userModel' => 'User',//ユーザーデータを扱うモデル
      'fields' => array(
        'username' => 'username', 
        'password' => 'password'
       ),//認証用のフィールド
    ),
  );
}	

細かく設定を書こうと思ったらまだまだいっぱいあるんですけど、およそこの辺りの項目を設定すれば、自分の望むログイン処理ができるのではないかと。認証のフィールドをメールアドレスにしたりとか、ログイン後のリダイレクト先を変えたりとかね。

これをCakePHP3で書くと、以下のようになります。

public function initialize() {
  $this->loadComponent('Auth', [ 
    'loginAction' => [
      'controller' => 'Users',
      'action' => 'login'
    ],
    'loginRedirect' => [
      'controller' => 'Users',
      'action' => 'index'
    ],
    'authenticate' => [
      'Form' => [
        'userModel' => 'Users',
        'fields' => [
          'username' => 'username',
          'password' => 'password'
        ]
      ]
    ]
  ]);

  $this->Auth->sessionKey = 'Auth.User';
}

まず、beforeFilter()ではなく、initialize()に書くってとこが違いますね。beforeFilter()に書いても上手く動いてくれないっぽいです。

ログインアクションとかリダイレクト先、モデルやフィールドの設定は、2の頃とそんな変わりないですかね。

そういえば、命名規約に従う場合、モデルのクラス名が、単数系から複数形に変わったみたいです。だからuserModelのところも、デフォルトだと「User」ではなく「Users」になっております。

それから、セッションキーの指定は、「$this->Auth->sessionKey」に好きな文字を突っ込めばオーケーです。CakePHP1.3を使ってたときはこの書き方でキーを書き換えられてたと思うんですけど、2になったら上記のように「AuthComponent::$sessionKey」って書かないと書き換えられなかったんですよね。僕が何か設定を間違えてたのでなければだけど。

あとそう。配列の書き方が「array()」ではなく「[]」になってます。別にarray()で書いても動くけど、[]の方が文字数も少ないし、ちょっとだけ楽に感じるかもしれない?

配列を[]と書くのは、PHPのバージョンが5.4以降で可能になった機能のようです。CakePHP3は5.4以上が必須になってるから、こっち使って楽しとけみたいな感じなんですかね。

配列ってやたらと書く機会が多いから、もしarray()と書くところを[]と書くことで0.5秒節約できるとした場合……一つのプロジェクトで一万回くらい書いたとしたら、合計で5000秒も節約できる!!

5000秒! それは永遠という意味ですか?(CV:石丸小唄)



ログイン画面を実装してみる

さあ、それじゃあその節約できた永遠にも等しい時間を十全に使って、ログイン画面でも作ってみましょう。

やはりまずは、CakePHP2の書き方をちょっと見てみませう。

//コントローラー
public function login() {
  if($this->request->is('post')) {
    if($this->Auth->login()) {
      $this->redirect($this->Auth->loginRedirect);
    } else {
      $this->Session->setFlash('ログインエラーです');
    }
  }
}

//ビュー
<?php echo $this->Session->flash() ?>
<?php echo $this->Form->create('User') ?>
  <?php echo $this->Form->text('username') ?>
  <?php echo $this->Form->password('password') ?>
  <?php echo $this->Form->button('login') ?>
<?php echo $this->Form->end() ?>

ちょっと大雑把かもですが、およそこんな感じですね。

ほんじゃあこいつを、CakePHP3バージョンに書き換えてみやしょー。

//コントローラー
public function login() {
  if($this->request->is('post')) {
    $user = $this->Auth->identify();

    if($user) {
      $this->Auth->setUser($user);
      $this->redirect($this->Auth->redirectUrl());
    } else {
      $this->Flash->error('ログインエラーです');
    }
  }
}

//ビュー
<?= $this->Flash->render() ?>
<?= $this->Form->create('Users') ?>
  <?= $this->Form->text('username') ?>
  <?= $this->Form->password('password') ?>
  <?= $this->Form->button('login') ?>
<?= $this->Form->end() ?>

ビューで作成するテンプレートファイルは、CakePHP2ではViewフォルダの中でしたが、3ではTemplateというフォルダの下に作成します。もし「/users/login」のURLでログイン画面を作りたいなら、「/Template/Users/login.ctp」となるわけですね。

ビューの中身は、2のときとほぼ一緒ですね。エラーメッセージの表示のとこがちょっと違う書き方になってますけど、今日は無視で。いずれこれについても触れたいと思ってますが、今は、もし何やってんだか分かんないって場合は、とりあえずそのままコピペっといて。大丈夫、ちゃんと動くから。

ちなみにphpタグなんですが、phpと書く代わりに「=」と書くのが推奨されているっぽい感じなんでしょうか……? この書き方だとechoも省略できるので、さっきの配列じゃないけど、ちょい楽ではありますかね。

phpタグも使う箇所は多いですから、「php echo」を「=」と書くことで一秒節約できると考えると……一万回書けば、何と10000秒の節約になります!!

10000秒! 永遠の倍! 全くもって十全ですわね、ディアフレンド(CV:石丸小唄)



コントローラーの方は、CakePHP2だと、「$this->Auth->login()」でセッションにユーザーデータを入れるところまでやってくれていました。

でも今回は、「$this->Auth->identify()」で認証に成功するとユーザーデータが返ってくるので、「$this->Auth->setUser()」を使って、自分でセッションにデータを入れる必要があるみたいです。

よく分かりませんが、こっちの方が柔軟ってことなんですかね?

こっちもエラーメッセージの挿入んとこがちょっと変わってますけど、これも後日。



条件をちょいカスタマイズしたい

例えば、特定のステータスを持つユーザーだけがログインできる、みたいな場合。AuthComponentの設定で「scope」ってのを使うと、取得条件を追加することができます。これはCakePHP2にもあったけど、3でも書き方は一緒。

//CakePHP2
$this->Auth->authenticate = array(
  'Form' => array(
    'scope' => array('status' => 1),
  ),
);

//CakePHP3
$this->loadComponent('Auth', [ 
  'authenticate' => [
    'Form' => [
      'scope' => ['status' => 1]
    ]
  ]
]);

これで、statusというカラムに入ってるデータが1のユーザーだけをログインさせることができるようになります。



ただ、もっとカスタマイズしたい、WHERE句の条件だけじゃなくて、取得するカラムとか、アソシエーション先のデータも取りたいとか、そんな場合もあるかもしれない。

そういうときは、モデル側に独自のメソッドを作ってカスタマイズすることができます。

//コントローラー
$this->loadComponent('Auth', [ 
  'authenticate' => [
    'Form' => [
      'finder' => 'auth'
    ]
  ]
]);

//モデル(テーブル)
namespace App\Model\Table;
use Cake\ORM\Query;
class UsersTable extends Table {
  public findAuth(Query $query) {
    $query->where(['status' => 1]);//statusが1のユーザーのみ取得
    $query->select(['id', 'username', 'password']);//取得するカラムを絞る
    $query->contain(['Posts']);//アソシエーション(postsテーブルとアソシエーションしていると仮定)
    return $query;
  }
}

DBからデータを取ってくるfind()も結構書き方が変わっているんですが、これも今回は触れないです。また別の機会に。

カスタマイズするには、まずAuthComponentの設定のところで「finder」という項目を設定します。今回は「’finder’ => ‘auth’」としましたが、「auth」のところは何でも良いです。大事なのは、「’finder’ => ‘〇〇’」で書いた単語を使って、「find〇〇」というメソッドをテーブル側で作成するってことですね。findと〇〇をドッキングさせるわけです。パイルダーオン!!(CV:石丸博也)です。

これで「find〇〇」というメソッドを経由してログイン処理を行うようになります。「find〇〇」の〇〇は先頭が大文字になります。キャメル何ちゃら的な。

だから、もし「’finder’ => ‘mazingerz’」とかお茶目なことするんなら、テーブル側のメソッドも「findMazingerz()」とすれば正常に機能するはずです。試してないけど。



ログアウトしてみる

ログアウトは昔と変わらずです。

$this->Auth->logout();



パスワードのハッシュ化

ログイン処理を行うためには、そもそもログインするユーザーデータがDBに入っていなければ始まらない。

ユーザーデータを作成する場合、パスワードは暗号化された状態のものをDBに登録すると思います。

CakePHP2なら、パスワードのハッシュ化はこれでできました。

$password = $this->Auth->password('password');

CakePHP3の場合は、ちょっと違う。

use Cake\Auth\DefaultPasswordHasher;
class UsersController extends AppController {
  public function add() {
    $hasher = new DefaultPasswordHasher();
    $password = $hasher->hash('password');
  }
}

仮に「/users/add」というURLでユーザー作成画面を作るとするなら、こんな感じでパスワードをハッシュ化できます。やってることは、ハッシュ化用クラスのインスタンスを作って、ハッシュ化してるだけっす。

ハッシュ化に使うクラスは「DefaultPasswordHasher」以外にもいくつかあるような感じに見えるんですが、僕はまだこれしか使ったことがないです。

別にコントローラーじゃなくても、テーブルでもエンティティでも、どこでも同じように読み込んで使用することができます。



パスワードのチェック

ちょいおまけ的な。

もしかしたら、ログイン処理以外のところでも、パスワードの入力を求めたいことがあるかもしれない。ユーザーが自分のパスワードを別のものに変更したいときとかに、現在のパスワードを入力させたりとか、そういうシステム、たまにあるよね。

そのとき、すでにDBに入力されているパスワードと一致するかどうかを確かめることになると思うんですけど、上と同じやり方でハッシュ化しただけでは、実は一致しているかどうかが判別できない。

「$hasher->hash()」で実際やってみると分かるんですけど、同じ文字を入れても、ハッシュ化するたびに違う文字列になるんですよ。どういう仕組みなのかはよく分からないんだけど、とにかく、入力されたパスワードが一緒でも、ハッシュ化して比べると違う文字列になっちゃうから、一致しているかが分からないのです。

例えば、$userに現在のユーザーデータが入ってるとして、そこにパスワードがPOSTされてきたとする。

//入力されたパスワードをハッシュ化
$hasher = new DefaultPasswordHasher();
$password = $hasher->hash($this->request->data['password']);

//$user->passwordには現在のハッシュ化されたパスワードが入ってる
if($password == $user->password) {
  echo '一致しました';
}

こんな風に書いても、永遠に一致しない。5000回試しても10000回試しても、一致する日は来ない……と思う。

じゃあどうするかって言うと、こうする。

$hasher = new DefaultPasswordHasher();
$bool = $hasher->check($this->request->data['password'], $user->password);

if($bool) {
  echo '一致しました';
}

DefaultPasswordHasherクラスには「check()」というパスワードチェック用のメソッドがあるので、それ使って入力されたデータと現在のデータを比較すればオッケーです。一致していればtrueが返ってきます。

他のハッシュ化クラスを使った場合も同じかどうかまでは、検証してないです。すみません。






ふう……思ったより長くなってしまいました。いつものことだけど。

でもこれだけできれば、よっぽど複雑なことをしない限り、ログイン周りはおおむね大丈夫な気がします。

ああ……そういえば、あれ試してなかったな。複数のカラムでログインできるようにするやつ。ユーザーIDとメールアドレス、どっちでもログインできるみたいな。

CakePHP1.3や2のときは、自分でAuthコンポーネントをいじって実装したんですが……3だとどうなんだろ。やっぱり同じように自分でいじらないとダメなのかな。僕が見た限りでは、3も対応はしていないように見えたけど。

1.3や2のときは、こんな感じでやりました。

複数のカラムでログインできるようにしたいんですけど(CakePHPのAuthで)

これと似たようなことをやる感じになる気もするんですけど、もしCakePHP3でもやる機会があれば、それがこれと大きく違うものであれば、そんときは記事にしたいと思います。

※CakePHP3でもやってみました(2016/12/12)
CakePHP3を触ってみました 〜複数のカラムでログインできるようにしたいんですけど〜



それでは、本日はこの辺で。

次回は、createdとmodifiedについて紹介します(CV:石丸謙二郎)



その他のCakePHP3を触ってみましたの記事はこちら
まとめという名の箸休め
 もしかしたら何か関連しているかも? 
 質問や感想などお気軽にコメントしてください