この記事を三行にまとめると
Authコンポーネントを書き換えるぞいカラムって言えば良い? フィールドって言えば良い?
今でしょ。倍返し。おもてなし。じぇじぇじぇ。
TwitterとかFacebookとか、他のサービスなんかでもよくありますけど、ログインフォームに入力するものが、ユーザーIDでもメールアドレスでもどっちでもOKみたいな場合があるじゃないですか。
冒頭の画像はTwitterのログインフォームなのですが『ユーザー名、またはメールアドレス』って、書いてありますよね。同じフォームに、ユーザー名かメールアドレス、どちらを入れてもログインが可能なわけです。
これを「CakePHPのAuthで実装したい。いつやるか、今でしょ」っていうのが今回のお話です。
ただし今回の記事は「こんなやり方でやれば良いんだぜ〜」というよりも「良いやり方が分からなかったらこうしてみたんだけど、どう?」的な話になります。
デフォルトではAuthコンポーネントにそういう機能はないような気がするんだけど……あるのかな?
まあ、とりあえず行ってみましょー。
もちろん、必ず二回判定するわけじゃなくて、最初のメールアドレスとパスワードの認証で成功したら、その時点でログインできます。
最初はメールアドレスとユーザーIDのOR検索みたいな感じにしようかとも思ったんだけど、OR検索だとインデックスが効かないので、ユーザー数が少ないときはよくても、段々とログイン処理が遅くなる恐れがあるし……ぶっちゃけ、複数のカラムで判定するからって、10個も20個も判定用のフィールドを使用することってないと思うのよね。基本的には2つか、あってもせいぜい3つってとこでしょうから、なら複数回インデックスの効いた検索をして、結果を返せば良いじゃないかと。倍返しすれば良いじゃないかと。
なので、その複数回判定できるように、Authコンポーネントの中身を書き換えるってのが、今回の作業になりやす。
まあ、難しいことはないです。ほんの数行書き換えるだけで済む話だから。
ファイルの中を見てみると、identifyメソッドの中にこんな記述があるんですよ。
メソッドの後半の方です。半分よりちょい下くらい。findでデータベースにアクセスして、空のデータが返って来たらnullを返す。ここでnullが返ると、ログインに失敗します。そりゃそうだ。ユーザーデータが取れなかったんだから。
この『return null』ってところを、ちょちょいっと変更してみます。
今回は、『subFields』っていう変数を用意してみました。Authの設定をコントローラーとかに書くことがあると思うんですけど、そこでsubFieldsの設定をしておくわけですね。
この変数に値が入ってたら、最初のログイン判定に失敗したとき、判定するフィールドを変えて、再帰的にidentifyメソッドを呼びだす、みたいな感じ。ここではデータベースのカラム名が、メールアドレスは『email』、ユーザーIDは『user_id』にしてあります。
もっとたくさんのカラムで判定したければ、subFieldsにカラム名を追加すれば良い。例えば……何だろ。メールアドレスとユーザーIDに加えて、生年月日(カラム名:birthday)と出席番号(カラム名:number)でもログインできるようにしてみる? ああ、もちろんこの場合、生年月日も出席番号もユニークな値になっているっていう想定でね。
これで、認証に失敗し続けている限りは、再帰的にsubFieldsで設定したフィールドで判定を続けます。全部に失敗して、初めてログイン失敗になる。
全くの余談なんだけど、フィールドとカラムって、どっちが正式名称なんすかね? ってか、両者は定義が違うもんなのか? さっきから両方を混在して書いちゃってるけど、それは僕の中では両者は同じものだからです。「だったらどっちかに統一しろよ。何で混在させてんねん」って思うかもしれないけど、何でかってーと、どっちで検索されても大丈夫なようにするためです。僕と同じような人は「Auth 複数フィールド ログイン」でも「Auth 複数カラム ログイン」でも検索する可能性があるからね。実際僕もそうだったし。だから僕なりの、検索する人へのおもてなし……の、つもりです。
文章的にはややこしくてごめんちゃい。
なので、1.3のときとは変更する箇所が異なります。やることは一緒なんだけどね。上と同じ、subFieldsってのを用意して、それを使って複数判定を行なう。
今回はフォームからログインするところだけを改修するんで、修正するファイルはこれです。
・FormAuthenticate.php
コアライブラリの中を見ると、コンポーネントファイルがあるところに、さらに『Auth』っていうディレクトリがあって、その中にこのファイルが入ってます。なので、それをapp側に持ってくる。ファイルの置き方としては『/app/Controller/Component/Auth/FormAuthenticate.php』だね。
FormAuthenticate.phpの中を見ると、『authenticate』っていうメソッドがありまして、そこで認証のためにデータベースにアクセスしています。いや、まあ正確には、そのメソッドの中でさらに別ファイルのメソッドを呼び出して、そのメソッドの中でfindしてるんだけどね。
これがauthenticateの中身。これだけ見ると、1.3の頃に比べてシンプルで分かりやすいな〜とも思うのですが、僕の場合は1.3に慣れていたせいで、このメソッドに辿り着くまでが大変だったわ。Auth.phpを見たとき「じぇじぇじぇ! どこを書き換えたら良いのか分がらん」ってなったわ。まあそんなこたぁどーでもいい。
このメソッドを、1.3のときと同じ要領で、subFieldsを使って複数回判定できるようにします。
2系の場合、コントローラーでのAuthの設定はこんな風に書きます。
1.3の頃に「$this->Auth->〇〇」と書いていたやつを「authenticate」の中にまとめるようなイメージですかね。
ここに「subFields」を追加する。
authenticateに関する詳しい書き方はこの辺りを参照。
Cookbook
これで、一応複数のカラム(フィールド)でのログイン判定は、できるようになります。
ただ、冒頭でも言ったけど、これがベストなやり方なのかは分からない。
てゆーか、Twitterとかって、実際にどうやってこの辺の処理をやってるんですかね? 案外こんな風に、シンプルに複数回判定しているんですかね?
それとも何か、画期的なやり方が他にあるのだろうか……?
ついでに、今回は無理矢理、2013年の流行語大賞を全部詰め込んでみたんだけど、もうちょっと上手く使いたかったな……もっと何か、画期的な使い方をしたかった。
CakePHP3でも複数カラムの設定をやってみました。
CakePHP3を触ってみました 〜複数のカラムでログインできるようにしたいんですけど〜
冒頭の画像は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を触ってみました 〜複数のカラムでログインできるようにしたいんですけど〜