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

この記事はだいぶ前に書かれたものなので情報が古いかもしれません
基本ログインしっぱなしだからめったに見ない画面だよね

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

Authコンポーネントを書き換えるぞい
カラムって言えば良い? フィールドって言えば良い?
今でしょ。倍返し。おもてなし。じぇじぇじぇ。
TwitterとかFacebookとか、他のサービスなんかでもよくありますけど、ログインフォームに入力するものが、ユーザーIDでもメールアドレスでもどっちでもOKみたいな場合があるじゃないですか。

冒頭の画像はTwitterのログインフォームなのですが『ユーザー名、またはメールアドレス』って、書いてありますよね。同じフォームに、ユーザー名かメールアドレス、どちらを入れてもログインが可能なわけです。

これを「CakePHPのAuthで実装したい。いつやるか、今でしょ」っていうのが今回のお話です。

ただし今回の記事は「こんなやり方でやれば良いんだぜ〜」というよりも「良いやり方が分からなかったらこうしてみたんだけど、どう?」的な話になります。

デフォルトではAuthコンポーネントにそういう機能はないような気がするんだけど……あるのかな?

まあ、とりあえず行ってみましょー。



結論を先に言っとく。岸部一徳

やったこととしては、複数のカラム……例えばメールアドレスとユーザーIDだったら、二回クエリを投げて判定しているだけです。最初にメールアドレスとパスワードでログイン判定を行なって、認証に失敗したら次はユーザーIDとパスワードでログイン判定して、それでも認証に失敗したら、ログインに失敗したと見なす。

もちろん、必ず二回判定するわけじゃなくて、最初のメールアドレスとパスワードの認証で成功したら、その時点でログインできます。

最初はメールアドレスとユーザーIDのOR検索みたいな感じにしようかとも思ったんだけど、OR検索だとインデックスが効かないので、ユーザー数が少ないときはよくても、段々とログイン処理が遅くなる恐れがあるし……ぶっちゃけ、複数のカラムで判定するからって、10個も20個も判定用のフィールドを使用することってないと思うのよね。基本的には2つか、あってもせいぜい3つってとこでしょうから、なら複数回インデックスの効いた検索をして、結果を返せば良いじゃないかと。倍返しすれば良いじゃないかと。

なので、その複数回判定できるように、Authコンポーネントの中身を書き換えるってのが、今回の作業になりやす。



Cake1.3の場合

Authコンポーネントファイル(auth.php)の中を見てみると「indentify」っていうメソッドがあるんですよ。データベースにアクセスして対象のデータがあるか判定しているのはここの中でやっているようなので、これをいじります。

まあ、難しいことはないです。ほんの数行書き換えるだけで済む話だから。

ファイルの中を見てみると、identifyメソッドの中にこんな記述があるんですよ。

function identify($user = null, $conditions = null) {

  〜 中略 〜

  $data = $model->find('first', array(
    'conditions' => array_merge($find, $conditions),
    'recursive' => 0
  ));
  if (empty($data) || empty($data[$model->alias])) {
    return null;
  } 

  〜 中略 〜

}

メソッドの後半の方です。半分よりちょい下くらい。findでデータベースにアクセスして、空のデータが返って来たらnullを返す。ここでnullが返ると、ログインに失敗します。そりゃそうだ。ユーザーデータが取れなかったんだから。

この『return null』ってところを、ちょちょいっと変更してみます。

function identify($user = null, $conditions = null) {

  〜 中略 〜

  $data = $model->find('first', array(
    'conditions' => array_merge($find, $conditions),
    'recursive' => 0
  ));
  if (empty($data) || empty($data[$model->alias])) {
    if(!empty($this->subFields)) {
      $username = $user[$model->alias . '.' . $this->fields['username']];
      unset($user[$model->alias . '.' . $this->fields['username']]);
      $this->fields['username'] = array_shift($this->subFields);
      $user[$model->alias . '.' . $this->fields['username']] = $username;
      $data[$model->alias] = $this->identify($user);
    }
  } 

  〜 中略 〜

}

今回は、『subFields』っていう変数を用意してみました。Authの設定をコントローラーとかに書くことがあると思うんですけど、そこでsubFieldsの設定をしておくわけですね。

//コントローラー
function beforeFilter() {
  $this->Auth->fields = array('username' => 'email', 'password' => 'password');
  $this->Auth->subFields = array('user_id');
}

この変数に値が入ってたら、最初のログイン判定に失敗したとき、判定するフィールドを変えて、再帰的にidentifyメソッドを呼びだす、みたいな感じ。ここではデータベースのカラム名が、メールアドレスは『email』、ユーザーIDは『user_id』にしてあります。

もっとたくさんのカラムで判定したければ、subFieldsにカラム名を追加すれば良い。例えば……何だろ。メールアドレスとユーザーIDに加えて、生年月日(カラム名:birthday)と出席番号(カラム名:number)でもログインできるようにしてみる? ああ、もちろんこの場合、生年月日も出席番号もユニークな値になっているっていう想定でね。

//コントローラー
function beforeFilter() {
  $this->Auth->fields = array('username' => 'email', 'password' => 'password');
  $this->Auth->subFields = array('user_id', 'birthday', 'number');
}

これで、認証に失敗し続けている限りは、再帰的にsubFieldsで設定したフィールドで判定を続けます。全部に失敗して、初めてログイン失敗になる。



全くの余談なんだけど、フィールドとカラムって、どっちが正式名称なんすかね? ってか、両者は定義が違うもんなのか? さっきから両方を混在して書いちゃってるけど、それは僕の中では両者は同じものだからです。「だったらどっちかに統一しろよ。何で混在させてんねん」って思うかもしれないけど、何でかってーと、どっちで検索されても大丈夫なようにするためです。僕と同じような人は「Auth 複数フィールド ログイン」でも「Auth 複数カラム ログイン」でも検索する可能性があるからね。実際僕もそうだったし。だから僕なりの、検索する人へのおもてなし……の、つもりです。

文章的にはややこしくてごめんちゃい。



Cake2の場合

今回はバージョン2.3の場合で話しますが、どうやら2系になってから、Auth周りもいろいろ改修が入ったのか、ファイルが分割されてるんですよね。んなもんだから、今まで通りAuth.phpのidentifyメソッドを見ても、そこでデータベースへのアクセスは行なってないのよ。いや、行なってはいるんだけど、行なうために別ファイルのメソッドを呼び出してる。

なので、1.3のときとは変更する箇所が異なります。やることは一緒なんだけどね。上と同じ、subFieldsってのを用意して、それを使って複数判定を行なう。

今回はフォームからログインするところだけを改修するんで、修正するファイルはこれです。

・FormAuthenticate.php

コアライブラリの中を見ると、コンポーネントファイルがあるところに、さらに『Auth』っていうディレクトリがあって、その中にこのファイルが入ってます。なので、それをapp側に持ってくる。ファイルの置き方としては『/app/Controller/Component/Auth/FormAuthenticate.php』だね。

FormAuthenticate.phpの中を見ると、『authenticate』っていうメソッドがありまして、そこで認証のためにデータベースにアクセスしています。いや、まあ正確には、そのメソッドの中でさらに別ファイルのメソッドを呼び出して、そのメソッドの中でfindしてるんだけどね。

public function authenticate(CakeRequest $request, CakeResponse $response) {
  $userModel = $this->settings['userModel'];
  list(, $model) = pluginSplit($userModel);

  $fields = $this->settings['fields'];
  if (!$this->_checkFields($request, $model, $fields)) {
    return false;
  }
  return $this->_findUser(
    $request->data[$model][$fields['username']],
    $request->data[$model][$fields['password']]
  );
}

これがauthenticateの中身。これだけ見ると、1.3の頃に比べてシンプルで分かりやすいな〜とも思うのですが、僕の場合は1.3に慣れていたせいで、このメソッドに辿り着くまでが大変だったわ。Auth.phpを見たとき「じぇじぇじぇ! どこを書き換えたら良いのか分がらん」ってなったわ。まあそんなこたぁどーでもいい。

このメソッドを、1.3のときと同じ要領で、subFieldsを使って複数回判定できるようにします。

public function authenticate(CakeRequest $request, CakeResponse $response) {
  $userModel = $this->settings['userModel'];
  list(, $model) = pluginSplit($userModel);

  $fields = $this->settings['fields'];
  if (!$this->_checkFields($request, $model, $fields)) {
    return false;
  }

  //複数のフィールドでログイン判定できるようにする
  $fieldArray[] = $this->settings['fields']['username'];

  if(!empty($this->settings['subFields'])) {
    $fieldArray = am($fieldArray, $this->settings['subFields']);
  }

  //データが取得できるまで判定を繰り返す
  foreach($fieldArray as $array) {
    $this->settings['fields']['username'] = $array;

    $user = $this->_findUser(
      $request->data[$model][$fields['username']],
      $request->data[$model][$fields['password']]
    );

    //データの照合に成功したら処理終了
    if(!empty($user)) {
      return $user;
    }
  }

  return false;
}

2系の場合、コントローラーでのAuthの設定はこんな風に書きます。

//コントローラー
function beforeFilter() {
  $this->Auth->authenticate = array(
    'Form' => array(
      'fields' => array('username' => 'email', 'password' => 'password'),
     ),
  );	
}

1.3の頃に「$this->Auth->〇〇」と書いていたやつを「authenticate」の中にまとめるようなイメージですかね。

ここに「subFields」を追加する。

//コントローラー
function beforeFilter() {
  $this->Auth->authenticate = array(
    'Form' => array(
      'fields' => array('username' => 'email', 'password' => 'password'),
      'subFields' => array('user_id'),
     ),
  );	
}

authenticateに関する詳しい書き方はこの辺りを参照。
Cookbook






これで、一応複数のカラム(フィールド)でのログイン判定は、できるようになります。

ただ、冒頭でも言ったけど、これがベストなやり方なのかは分からない。

てゆーか、Twitterとかって、実際にどうやってこの辺の処理をやってるんですかね? 案外こんな風に、シンプルに複数回判定しているんですかね?

それとも何か、画期的なやり方が他にあるのだろうか……?

ついでに、今回は無理矢理、2013年の流行語大賞を全部詰め込んでみたんだけど、もうちょっと上手く使いたかったな……もっと何か、画期的な使い方をしたかった。



CakePHP3でも複数カラムの設定をやってみました。
CakePHP3を触ってみました 〜複数のカラムでログインできるようにしたいんですけど〜
 もしかしたら何か関連しているかも? 
 みんなからのコメント 
2014年02月25日 20:04:37
twatwa
複数カラムのログインで困っていたところ、こちらの記事を参考にさせて頂きました!ありがとうございます!
2014年02月26日 12:30:37
まっち~(管理人)
>twatwaさん
いえいえ〜。お役に立てたみたいで良かったです〜。
2017年11月14日 20:47:01
murata
まさに探していた情報!なんですが・・・

AppControllers内のbeforeFilterで$this->Auth->settings['fields']や$this->Auth->settings['subFields']を設定しても反映されないです。

何かヒントをいただけませんでしょうか。
2017年11月14日 23:28:39
まっち~(管理人)
>murataさん
Cakeのバージョンは2系ですかね?
すみません。2系の方は上記の書き方だと正常に動かないですね…何でこう書いたんだろう。

beforeFilterのところは以下のように書き直してみてください。

$this->Auth->authenticate = array(
 'Form' => array(
  'fields' => array('username' => 'email', 'password' => 'password'),
  'subFields' => array('user_id'),
 ),
);

これで目的の動きになるはずです。
あとで記事本文の方も修正しておきますね。申し訳ないです。
2017年11月20日 09:05:50
murata
返信ありがとうございます。
ご察しの通り、2系(2.10.4)を使用しております。
頂いたコードでemail,user_idカラムを見に行くようになったのですが、
認証に失敗してしまいます。

従来、emailで成功していた認証が、
email, user_idのいずれでも失敗するといった状態です。

なんとなく方向性は見えてきました。
過去の記事へのコメントに返信いただき、ありがとうございました。
2017年11月20日 10:51:40
まっち~(管理人)
>murata
むぅ……なるほど。2.3や2.5ならこの書き方で大丈夫なはずなのですが、もしかしたらバージョンが上がって少し仕様が変わってしまったのかもしれませんね。お力になれずすみません。

カラムを見にいくようになったのであれば、発行されているSQL自体は問題なさそうなのでしょうか? そうなるとBaseAuthenticate.phpの_findUser()の中身も少し改造が必要なのかも。

2.10.4は触ったことないもので、僕も中身を確認してみます。