はじめに
本投稿は、2015/5/22に行われた、CakePHPで実装するログイン機能 - connpassの内容についてまとめた資料です。
今後の予定
AWS上で構築するRESTfulアプリ勉強会~Web開発ワークショップ~ - connpass
今回は、ログイン機能を追加します。
第2回でBackbone.jsを使用して画面を作りましたが、それ以来の機能追加です!
2015/5/25追記 - 1
phpMyAdminにログインできない、という方が何名かいらっしゃいました。
User study
Group apache
であるべきなんですが、なぜか、
User study
Group wheel
と、Groupがwheelになっていました。
rootでログインして、そこを直してapache再起動(service httpd restart
)すればOKでした。
※第1回のQiita記事作成と前後して、開発環境の元とするAMIを直前に修正したりしていたので、そのへんのタイミングでまちがった設定のAMIを一時公開してしまっていた可能性があります。
申し訳ございません!
2015/5/25追記 - 2
ユーザ情報は、パスワードをハッシュ化してから保存しますので、phpMyAdminでデータ追加してもログイン時にエラーになります。ご注意下さい。
ログインとは?
由来はこういうことらしいです。
それはさておき、ログイン機能を実装する際にやるべきことは次の2つです。
- 認証 - Authentication
- 承認(認可) - Authorization
認証 - Authentication とは?
認証とは、「本人確認」と言っていいでしょう。
今回作成するログイン機能では、シンプルなフォーム認証を使用します。
つまり、「ユーザID」と「パスワード」を使用し、正しければ本人であるとみなします。
「ログイン」といった場合通常はこの「認証」を指しますが、「承認」処理とセットで実装することになります。
承認(認可) - Authorization とは?
「認証」されたユーザに対し、特定のリソースやサービスへのアクセスを許可することを言います。
今回は、認証されたユーザには全リソースへのアクセスを許可することとします。
今回のログイン処理実装の特殊なところ
この勉強会の大前提は「REST」、「SinglePageApplication(SPA)」です。
SPAでもRESTでもない、通常のCakePHPのアプリケーション(サーバサイドでページ生成する普通のWebアプリ)だと、未認証状態で認証が必要なページにアクセスした場合、ログインページにリダイレクトされます。
ですが、SPAの場合はサーバへのアクセスは全てRestAPIを通じて行われるので、基本的にリダイレクトは使用しません。単にHTTPステータスコード401(Unauthorized)をクライアントに返し、クライアント側プログラムが自身でログイン画面を表示します。
未認証の状態での全てのAPIリクエストは、HTTPステータスコード401を返す可能性があるので、クライアント側はそれに対応する必要があります。
今回の内容
今回は、以下の機能を実現します。けっこう多いので頑張りましょう!
機能リスト
- ユーザ登録
- ユーザ名と氏名、パスワードを入力し、新規ユーザを登録します
- 成功すればメッセージ表示
- ユーザ名と氏名、パスワードを入力し、新規ユーザを登録します
- ログイン
- ユーザ名、パスワードを入力し、ログインボタンをクリック
- 正しければTODO一覧画面に遷移
- 間違っていればエラーを表示
- 既に他のユーザがログイン済みの場合その旨表示
- 自分自身が既にログイン済みであればTODO一覧画面に遷移
- ユーザ名、パスワードを入力し、ログインボタンをクリック
- ログアウト
- ログインした状態でログアウトボタンをクリック
- ログイン画面に遷移
- ログインした状態でログアウトボタンをクリック
- ヘッダ表示
- ログイン中は画面上部(ヘッダ)にユーザ名とログアウトボタンを常に表示する。
- 認証済みチェック
- 未認証ユーザによる、ログイン画面以外へのアクセスは全てログイン画面に遷移させる
画面はこんな感じです
- ログインとユーザ登録
- 画面上半分がログイン画面、下半分がユーザ登録画面です。
- ユーザ登録では、ユーザ名と氏名、パスワードを登録します。
-
登録には「パスワードの再入力」欄を用意するのがセオリーですが、今回はシンプルにする為あえて用意していません。
- 余力があれば付けてみましょう!
- TODO一覧画面
- 画面上部にヘッダが表示されます。ユーザ名と、カッコ内に氏名、ログアウトボタンが表示されます。
- TODO詳細画面
- 同じくヘッダが表示されます。
事前準備
事前準備は毎回同じなので、別エントリにまとめました。
全12回の勉強会でやっているGitの使い方 - AWS上で構築するRESTfulアプリ勉強会~Web開発ワークショップ~ - Qiitaを参照してください。
やることは、
- gitのブランチを整えて今回用ブランチ
vol/05
を作成する
です。まずこれをやりましょう。今回はさらに、
- ユーザ登録用のテーブル作成
も行います。
ユーザ登録用のテーブル作成
作成するのは、users
テーブルです。
まず、phpMyAdminにログインしましょう。
URLは、http://(PublicIP)/phpMyAdmin/
です。
変更していなければ、ユーザ名study
、パスワードstudy
でログインできます。
下図の通り、左側ペインの[study]-[New]をクリックし、テーブルを作成して下さい。
下記の通り設定して下さい。
テーブル名
users
各列の設定
名前 | データ型 | 長さ/値 | デフォルト値 | 照合順序 | 属性 | NULL | インデックス | A_I(AutoIncrement) | コメント |
---|---|---|---|---|---|---|---|---|---|
id | INT | 10 | なし | UNSIGNED | PRIMARY | レ | id | ||
username | VARCHAR | 20 | なし | utf8_general_ci | UNIQUE | ユーザ名 | |||
password | VARCHAR | 100 | なし | utf8_general_ci | パスワード | ||||
name | VARCHAR | 50 | なし | utf8_general_ci | 氏名 |
その他の設定
- テーブルのコメント:
ユーザ
- ストレージエンジン:
InnoDB
- 照合順序:
utf8_general_ci
- パーティションの定義:空白
「保存する」クリックでテーブル作成!
準備
では、準備を始めましょう!
-
Gitのブランチを整えて、
vol/05
ブランチを作成。 - ユーザ登録用テーブルを作成。
準備ができたら、Lesson1です!
Lesson1 サーバサイド
Lesson1では、サーバサイドのプログラムを実装します。
プログラムのテストには、第1回でインストールした「POSTMAN」を使用してAPIの動作確認を行います。
ポイント
SPAでないアプリケーションと、WebAPIを使用したSPAアプリケーションの実装の仕方の違いを抑えておきましょう。
未ログイン状態でTODO一覧を表示しようとした場合
SPAでない通常のWebアプリケーション
こういうシーケンスです。
- TODO一覧画面をサーバに要求
- サーバは、未ログインのユーザからの要求なので、ステータスコード302を返し、ログイン画面にリダイレクトさせる。
- 302を受けたブラウザが、
Location
ヘッダで指定されたURLを要求 - サーバはログイン画面のHTMLを返す
SPAの場合
こうなります。
- TODO一覧画面をサーバに要求
- サーバは、未ログインのユーザからの要求なので、ステータスコード401を返し、要求が許可されないことを応答する。
- 401を受けるとJavascriptが自分でログイン画面を表示します。
この違いを抑えておきましょう!
作成するAPI
URL | Http Method | 処理 | Controller | Action |
---|---|---|---|---|
/rest-study/users/login.json | POST | ログインする | UsersController | login |
/rest-study/users/logout.json | POST | ログアウトする | UsersController | logout |
/rest-study/users/loggedin.json | GET | ログインユーザ情報を取得(ログイン中かどうかのチェック) | UsersController | loggedIn |
/rest-study/users/signup.json | POST | ユーザを登録する | UsersController | signUp |
編集するファイル一覧
編集 | file | 編集概要 |
---|---|---|
修正 | app/Config/routes.php | 追加するAPIへのルート追加 |
修正 | app/Controller/AppController.php | Authコンポーネントの設定 |
新規 | app/Controller/UsersController.php | API実装 |
新規 | app/Model/User.php | ユーザテーブルへのアクセス |
routes.php
追加したAPIへのルートを追加します。
〜略〜
/*
* API
*/
+
+// ログイン
+Router::connect('/users/login', array (
+ 'controller' => 'users',
+ 'action' => 'login',
+ 'method' => array (
+ 'POST'
+ )
+));
+
+// ログアウト
+Router::connect('/users/logout', array (
+ 'controller' => 'users',
+ 'action' => 'logout',
+ 'method' => 'POST'
+));
+
+// ログインチェック(ログイン情報取得)
+Router::connect('/users/loggedin', array (
+ 'controller' => 'users',
+ 'action' => 'loggedIn',
+ 'method' => 'GET'
+));
+
+// サインアップ
+Router::connect('/users/signup', array (
+ 'controller' => 'users',
+ 'action' => 'signUp',
+ 'method' => array (
+ 'POST'
+ )
+));
+
+
Router::mapResources(array (
'todo_lists',
));
Router::parseExtensions('json');
〜略〜
単純なCRUDを実行するAPIであれば、
Router::mapResources()
関数でControllerを追加してあげれば
index
, view
, add
, edit
, delete
のアクションへのルートが設定されますが、ログインのURLはこうではないので、個別に追加しています。
Router::connect()
関数を使用して下記項目を全て設定しています。
- URL
- 実行するcontroller
- 実行するaction
- 対象とするhttpメソッド
ログインの場合、
- URL ->
/users/login
- 実行するcontroller ->
users
- 実行するaction ->
login
- 対象とするhttpメソッド ->
POST
なので、上記コードのように設定します。
AppController.php
コンポーネント、承認処理を追加。
class AppController extends Controller {
- public $components = array(
- 'RequestHandler'
- );
+ public $components = array (
+ 'RequestHandler',
+ 'Auth' => array (
+ 'authenticate' => array (
+ 'Form' => array (
+ 'passwordHasher' => 'Blowfish'
+ )
+ )
+ )
+ );
+
+ public function beforeFilter() {
+ $user = $this->Auth->user();
+ if ($user === null
+ && $this->request->params['controller'] !== 'users'
+ && $this->request->params['action'] !== 'login'
+ && $this->request->params['action'] !== 'logout'
+ && $this->request->params['action'] !== 'loggedin'
+ && $this->request->params['action'] !== 'signup') {
+ throw new UnauthorizedException();
+ }
+ //全アクションを許可
+ $this->Auth->allow();
+ }
+
}
$components
変数
認証処理は、CakePHPが用意してくれているAuth
コンポーネントを使用します。
フォーム認証を使用し、パスワードのハッシュアルゴリズムにはBlowfish
を使用することを指定しています。
beforeFilter
関数
全てのアクションの前に実行される関数がこのbeforeFilter関数です。
ここで、アクセスしてきたユーザが認証済みかどうかチェックしています。
認証されていない場合、ログイン(login)、ユーザ登録(signup)、ログイン済みチェック(loggedin)以外は実行出来ないようにしています。
-
$this->Auth->user()
でログインユーザ情報を取得 - ログインユーザ情報が取得できなかった場合、
users
コントローラの下記アクションを除き、UnauthorizedException
をthrowして401エラーを発生させます。- login
- loggedin
- signup
UsersController.php
ログイン、ログイン済みチェック、ログアウト、ユーザ登録の実装です。
<?php
App::uses('AppController', 'Controller');
App::uses('BlowfishPasswordHasher', 'Controller/Component/Auth');
class UsersController extends AppController {
public function login() {
$user = $this->Auth->user();
$res = array();
if ($user) {
if ($user['username'] === $this->request->data['username']) {
$res['User'] = $user;
$res['message'] = "ログイン済みです";
} else {
$res['message'] = "別のユーザがログイン済みです";
}
} else {
$this->request->data['User']['username'] = $this->request->data['username'];
$this->request->data['User']['password'] = $this->request->data['password'];
if ($this->Auth->login()) {
$user = $this->Auth->user();
$res['User'] = $user;
$res['message'] = "ログイン成功";
} else {
$res['message'] = "ユーザ名またはパスワードが違います";
}
}
$this->set(compact('res'));
$this->set('_serialize', 'res');
}
public function loggedIn(){
$user = $this->Auth->user();
if ($user) {
$res['User'] = $user;
$res['message'] = "ログイン済みです";
$this->set(compact('res'));
$this->set('_serialize', 'res');
}else{
throw new UnauthorizedException();
}
}
public function logout() {
$user = $this->Auth->user();
$res = array();
if ($user !== null) {
$this->Auth->logout();
$res['message'] = "ログアウトしました。";
} else {
$res['message'] = "ログインしていません。";
}
$this->set(compact('res'));
$this->set('_serialize', 'res');
}
public function signUp() {
$data = $this->request->data;
if (isset($data['password'])) {
$passwordHasher = new BlowfishPasswordHasher();
$data['password'] = $passwordHasher->hash($data['password']);
}
$res = $this->User->save($data);
if($res){
unset($res['User']['password']);
$res['message'] = "登録しました。ログインできます!";
}else{
$res['message'] = "登録に失敗しました。";
}
$this->set(compact('res'));
$this->set('_serialize', 'res');
}
}
login
関数
ログイン処理を実行します。結果として、下記の4つのいずれかになります。
- ログイン成功
- ログイン失敗(ユーザ名orパスワードが違う)
- 自分が既にログイン済み
- 他人がログイン済み
ログイン処理は、
-
$this->request->data['User']['username']
にユーザ名、 -
$this->request->data['User']['password']
にパスワードを設定し、 -
$this->Auth->login()
を実行します。
クライアントに返すJSONの項目として、下記を設定しています。
-
User
-> ユーザ情報(id, username, name) -
message
-> 処理結果を表すメッセージ
{
"User": {
"id": "1",
"username": "test",
"name": "テスト"
},
"message": "ログイン成功"
}
loggedIn
関数
ユーザが既にログイン済みかどうか調べます。
-
$this->Auth->user()
でログインユーザ情報を取得し、null
でなければログイン済みと判断します。 - 未ログインの場合、
UnauthorizedException
をthrowして401エラーを発生させます。 - クライアントに返すJSONは
login
関数と同じく、user
とmessage
を設定します。
logout
関数
ログアウト処理を実行します。
-
$this->Auth->user()
でログイン済みか判断し、ログイン済みなら$this->Auth->logout()
でログアウト処理を実行します。 - 未ログインの場合はログアウト処理は実行せず、JSONに
message
をセットします。
signUp
関数
User
モデルを使用してユーザ情報をデータベースに登録します。
- パスワードは、
BlowfishPasswordHasher
クラスを使用してハッシュ化した文字列を保存します。 - 保存が成功した場合JSONにユーザ情報を設定しますが、パスワードは除いておきます(
unset($res['User']['password']);
)。
User.php
usersテーブルにデータを登録するためのmodelです。
<?php
App::uses('AppModel', 'Model');
class User extends AppModel {
}
CakePHPのmodelの標準機能以外何も実装していません。
ちゃんとしたアプリケーションなら、ここに入力値のバリデーション等を実装しなければなりませんが、今回のテーマでは無いので実装しないことにしました。
### 動作確認
POSTMANで各APIをテストします。
各APIの実行例を載せておきます。参考にしてやってみましょう!
ユーザ登録時のパスワードは、siginin
実行時にハッシュ化してから保存します。
なので、phpMyAdminでレコード追加したデータではログインに失敗しますのでご注意下さい。
下記、siginup
APIからテストするといいと思います。
- 例
URL | リクエストJSON | レスポンスコード | レスポンスJSON |
---|---|---|---|
/rest-study/users/signup.json | 200 | { "username": "test2", "password": "test2", "name": "テスト2" } |
{ "User": { "username": "test2", "name": "テスト2", "id": "65" }, "message": "登録しました。ログインできます!" } |
/rest-study/users/login.json | { "username":"test", "password":"test" } |
200 | { "User": { "id": "1", "username": "test", "name": "テスト" }, "message": "ログイン成功" } |
/rest-study/users/login.json | { "username":"test", "password":"hoge" } |
200 | { "message": "ユーザ名またはパスワードが違います" } |
/rest-study/users/logout.json | - | 200 | { "message": "ログアウトしました。" } |
/rest-study/users/loggedin.json | - | 200 | { "User": { "id": "1", "username": "test", "name": "テスト" }, "message": "ログイン済みです" } |
- エラー時の例1(未ログイン状態でログインチェック - 401が返却)
URL | リクエストJSON | レスポンスコード | レスポンスJSON |
---|---|---|---|
/rest-study/users/loggedin.json | - | 401 | { "name": "Unauthorized", "message": "Unauthorized", "url": "/rest-study/users/loggedIn.json" } |
- エラー時の例2(未ログイン状態でTODO一覧を取得 - 401が返却)
URL | リクエストJSON | レスポンスコード | レスポンスJSON |
---|---|---|---|
/rest-study/users/todo_lists.json | - | 401 | { "name": "Unauthorized", "message": "Unauthorized", "url": "/rest-study/users/todo_lists.json" } |
実装
では、実際に修正してAPIの動作確認をしてみましょう。
-
app/Config/routes.php
を上記の通り修正。 -
app/Controller/AppController.php
を上記の通り修正。 -
app/Controller/UsersController.php
を上記の通り新規作成。 -
app/Model/User.php
を上記の通り新規作成。 - 動作確認!
- Gitにコミット
GitHubでのdiff表示へのリンク
第5回 Lesson1 サーバーサイド · suzukishouten-study/rest-study@59c29b0
Lesson2へ!
Lesson2 クライアントサイド
Lesson2では、クライアントサイドのプログラムを実装します。
ポイント
ちょっとハマりポイントがあるのでチェックしておきましょう。
サーバとの通信はAjaxであり、非同期です。そのことを抑えておけば大丈夫です!
ブラウザでTODO一覧画面のURLを叩いた後の動作シーケンスについて次に説明します。
起動時に、まずサーバにユーザ情報を取りに行って、取れたらログイン済みと判断します。
(ヘッダに表示するユーザ情報の取得を兼ねています)
正しい動作
うまく動くシーケンスは下図のようになります。
- ログイン中かどうかを確認する処理を開始
- XHR(XmlHttpRequest)を使用し、サーバと通信します。この時、サーバからのレスポンス受信時に実行するコールバック関数をXHRに教えておきます。
- サーバからレスポンスを受信後、XHRは2で指定されたコールバック関数を実行します。
- コールバック関数では、サーバからの応答をみて、ユーザ情報(
ログイン済み
or未ログイン
の判断に利用)を記憶します。 - ルーティングを有効にします(
Backbone.history.start()
関数)。 - コントローラが実行され、TODO一覧画面表示関数が実行を開始
- 4で記憶したユーザ情報を確認
- ユーザ情報が取得済み(ログイン済み)なら、TODO一覧表示画面を表示
- ユーザ情報未取得(未ログイン)なら、ログイン画面を表示
うまく動かないパターン
前回までのプログラムでは、上記の5でやっているルーティングの有効化(Backbone.history.start()
関数)を起動時にすぐやっています。
このままだと、下記のようなシーケンスになってしまいます。
- ルーティングを有効にします(
Backbone.history.start()
関数)。 - ログイン中かどうかを確認する処理を開始
- XHR(XmlHttpRequest)を使用し、サーバと通信します。この時、サーバからのレスポンス受信時に実行するコールバック関数をXHRに教えておきます。
- 1でルーティングは既に有効なので、サーバからの応答を受ける前にコントローラが実行され、TODO一覧画面表示関数が実行を開始
- サーバからの応答後記録される予定のユーザ情報を確認。
- しかしまだ応答がないのでここではユーザ情報が未取得。よってログイン画面を表示します。
- サーバからレスポンスを受信後、XHRは2で指定されたコールバック関数を実行します。
- コールバック関数では、サーバからの応答をみて、ユーザ情報を記憶します。が、既にログイン画面を表示した後です。
上記から、ログイン中であったとしてもTODO一覧画面は表示されません。
こういった、「サーバからの応答が返る前に他の処理が実行されている」という動きを抑えておきましょう。
編集するファイル一覧
編集 | file | 編集概要 |
---|---|---|
修正 | app/View/Layouts/default.ctp | テンプレートの追加、修正 |
修正 | app/webroot/js/main.js | application.start()のタイミング変更 |
修正 | app/webroot/js/app.js | ログインチェック等 |
修正 | app/webroot/js/routers/router.js | ルート追加 |
修正 | app/webroot/js/routers/controller.js | ログイン関連の画面遷移制御 |
新規 | app/webroot/js/models/user-model.js | APIの実行 |
新規 | app/webroot/js/views/login-layout-view.js | ログイン画面表示用ビュー |
新規 | app/webroot/js/views/header-view.js | ヘッダ表示用ビュー |
default.ctp
テンプレートを追加。
<title>シンプルTODOアプリ</title>
</head>
<body>
+ <!-- ヘッダ -->
+ <div id="header"></div>
<!-- コンテンツ -->
<div id="main"></div>
+
+ <!-- ヘッダのテンプレート -->
+ <script type="text/template" id="header-template">
+ <p>ユーザ:<%- username %>(<%- name %>) <input type="button" id="logout" value="ログアウト"></input></p>
+ <hr>
+ </script>
<!-- TODO一覧表示のレイアウトテンプレート -->
〜略〜
</script>
<!-- 詳細画面のレイアウトテンプレート -->
<script type="text/template" id="todo-detail-layout-template">
〜略〜
</script>
<!-- 詳細画面の表示内容テンプレート -->
<script type="text/template" id="todo-detail-item-template">
〜略〜
</script>
+ <!-- ログイン画面テンプレート -->
+ <script type="text/template" id="login-layout-template">
+ <h2>ログイン</h2>
+ <div>
+ <p>ユーザ名 :<input type="text" id="username" placeholder="username" autofocus></input></p>
+ <p>パスワード:<input type="password" id="password" placeholder="password"></input></p>
+ <input type="button" id="login" value="ログイン"></input>
+ </div>
+ <hr>
+ <h2>ユーザ登録</h2>
+ <div>
+ <p>ユーザ名 :<input type="text" id="signup-username" placeholder="username"></input></p>
+ <p>氏名 :<input type="text" id="signup-name" placeholder="name"></input></p>
+ <p>パスワード:<input type="password" id="signup-password" placeholder="password"></input></p>
+ <input type="button" id="signup" value="登録"></input>
+ </div>
+ </script>
+
<!-- require -->
<script type="text/javascript" src="js/require-config.js"></script>
<script type="text/javascript" src="js/lib/require.js" data-main="main.js"></script>
</body>
</html>
下記を追加。
- ヘッダ情報を表示するRegionをするdivタグ
- ヘッダ情報表示ビュー用テンプレート
- ユーザ名、氏名、ログアウトボタンを配置
- ログイン画面用テンプレート
- ログイン用要素を配置
- ユーザ名
- パスワード
- ログインボタン
- ユーザ登録用要素を配置
- ユーザ名
- 氏名
- パスワード
- 登録ボタン
- ログイン用要素を配置
main.js
require(['app'], function(Application){
console.log('run main2');
window.application = new Application();
- window.application.start();
- console.log('app start');
});
});
application.start()
を削除。
これは、先程述べたハマりポイントの対応です。
後述するapplication(app.js
)内でのログインチェック(ログイン中かどうか確認)の処理が完了してからapplication.start()を実行するためです。application.start()
を削除した場合(期待する動作)と、削除しなかった場合の動作の違いを、今度はコードをベースに説明します。
application.start()
を削除した場合(期待する動作)
-
/rest-study/
にアクセス - main.js実行。 applicaionオブジェクトは生成するのみ
- app.jsの
initialize
関数内でログインチェックを実行(loggedin
APIのAjax通信開始) -
loggedin
APIからレスポンスを受取り、- ログイン中ならユーザ情報保持
- 未ログインならユーザ情報クリア
- application.start()を実行
-
onStart()
が実行され、Backbone.history.start()
が実行、ルーティングが開始される。 - controllerが実行され、
- ユーザ情報を保持している場合、TODO一覧画面に遷移
- ユーザ情報がクリアされている場合、
Backbone.history.navigate()
関数を実行してログイン画面(/#login
)にルーティング
-
application.start()
を削除しなかった場合
-
/rest-study/
にアクセス - main.js実行。 applicaion.start()を実行
- app.jsの
initialize
関数内でログインチェックを実行(loggedin
APIのAjax通信開始) -
loggedin
APIからレスポンスを受取る前に、app.jsのonStart()
が実行され、Backbone.history.start()
が実行、ルーティングが開始される。- controllerが実行され、
-
loggedin
APIのAjax通信のレスポンスをまだ受け取っていないため、ユーザ情報がクリアされている状態(初期値)なので、Backbone.history.navigate()
関数を実行してログイン画面(/#login
)にルーティング(実際にログインされているか否かにかかわらず)。
-
- controllerが実行され、
-
loggedin
APIからレスポンスを受取り、下記のように設定する- ログイン中ならユーザ情報保持
- 未ログインならユーザ情報クリア
- が、既にcontrollerの処理は終わっているので、ログイン画面が表示される...
app.js
ログイン関連の各処理を追加。
define(function(require){
console.log('run app');
var Router = require('routers/router');
+ var UserModel = require('models/user-model');
var Application = Marionette.Application.extend({
initialize : function(){
console.log('app.initialize');
+ // Ajaxのエラーの共通ハンドラを設定
+ $(document).ajaxError(function ( e, xhr, options , message ) {
+ window.application.ajaxErrorHandler( e, xhr, options , message );
+ });
new Router();
+ //ログインユーザ情報をサーバから取得
+ this.loginUser = new UserModel();
+ this.getLoginUser();
},
onStart : function(){
Backbone.history.start();
},
regions : {
+ headerRegion : '#header',
mainRegion : '#main'
- }
+ },
+
+ //ログインユーザ情報格納用
+ loginUser : null,
+
+ //ログインユーザ情報取得
+ getLoginUser : function(){
+ this.loginUser.getLoginUser(
+ this.onLoggedIn,
+ this.onNotLoggedIn
+ );
+ },
+
+ //ログインユーザ情報取得:ログイン済みの場合
+ onLoggedIn : function(){
+ window.application.start(); // applicaiton.start()はログインチェックの後
+ },
+
+ //ログインユーザ情報取得:未ログインの場合
+ onNotLoggedIn : function(){
+ window.application.clearLoginUser(); //ログイン情報をクリアしておく
+ window.application.start(); // applicaiton.start()はログインチェックの後
+ },
+
+ //ログイン済みか判定
+ isLoggedIn : function(){
+ return this.loginUser.isLoggedIn();
+ },
+
+ //ログインユーザ情報のクリア(ログアウト時)
+ clearLoginUser : function(){
+ this.loginUser.clear();
+ },
+
+ // ajaxのエラーを全てここでハンドリング
+ ajaxErrorHandler : function(e, xhr, options , message){
+ if( xhr.status === 401 ){
+ this.clearLoginUser();
+ // 未認証の場合ログイン画面に飛ばす
+ Backbone.history.navigate('#login', {trigger : true, replace : true});
+ }else if(xhr.status >= 400 && xhr.status < 500){
+ //ClientErrorの場合はメッセージ表示
+ alert(message);
+ }else if(xhr.status >= 500 && xhr.status < 600){
+ //ServerErrorの場合はメッセージ表示
+ alert(message);
+ }
+ },
});
return Application;
主な修正点。
追加require
- ログイン関連のAPIを実行する為、
models/user-model.js
をrequire。
initialize
関数
- Ajaxのエラーの共通ハンドラ(
ajaxErrorHandler
関数)を設定 - requireした
user-model
を生成し、ログインチェック(loggedin
API)を実行。- APIは
getLoginUser
関数内で実行- ログイン済みの場合、
onLoggedIn
関数が実行される。 - 未ログインの場合、
onNotLoggedIn
関数が実行される。
- ログイン済みの場合、
- APIは
ajaxErrorHandler
全てのAjax通信のエラー時に実行されるハンドラ関数。
- http statusが
401(Unauthorized)
(未ログイン)の場合、Backbone.history.navigate()
関数を実行してログイン画面(/#login
)にルーティング - それ以外のhttp statusの場合はalert表示。
router.js
〜略〜
//ルーティング設定
appRoutes : {
+ 'login' : 'login',
'' : 'todoLists',
'todo-lists' : 'todoLists',
'todo-lists/:id' : 'todoDetail'
},
〜略〜
ログイン画面へのルートを追加。
controller.js
//controller
console.log('load controller');
define(function() {
console.log('run controller');
var TodoController = Marionette.Controller.extend({
+ login : function(){
+ //ログイン画面
+ this.nextView('views/login-layout-view', null, true);
+ },
todoLists : function() {
//Todoレイアウト用ビューにルーティング
this.nextView('views/todo-layout-view');
},
todoDetail : function(id) {
this.nextView('views/todo-detail-layout-view', {modelId : id});
},
- nextView : function(viewPath, option) {
+ nextView : function(viewPath, option, tryShowLoginScreen) {
+ if(window.application.isLoggedIn()){
+ //ログイン済み
+ if(tryShowLoginScreen){
+ //ログイン済みなのにログイン画面に遷移しようとしている場合は
+ //TODOリスト画面にルーティング
+ Backbone.history.navigate('#todo-lists', {trigger: true, replace: true});
+ return;
+ }
+ }else{
+ //未ログイン
+ if(!tryShowLoginScreen){
+ //未ログイン状態でログイン画面以外に遷移しようとしている場合は
+ //ログイン画面にルーティング
+ Backbone.history.navigate('#login', {trigger: true, replace: true});
+ return;
+ }
+ }
+ //ヘッダ表示
+ this.showHeaderRegion(tryShowLoginScreen);
+ //コンテンツ表示
require([viewPath], function(View){
window.application.mainRegion.show(new View(option));
});
},
-
+
+ showHeaderRegion : function(tryShowLoginScreen){
+ if(tryShowLoginScreen){
+ //ログイン画面遷移時はヘッダ非表示
+ window.application.headerRegion.empty();
+ }else if(!window.application.headerRegion.hasView()){
+ //ログイン画面以外遷移時、かつヘッダ未表示の場合ヘッダ表示
+ require(['views/header-view'], function(View){
+ window.application.headerRegion.show(new View({
+ model : window.application.loginUser
+ }));
+ });
+ }
+ }
+
});
return TodoController;
});
login
関数追加
- nextView関数に追加した
tryShowLoginScreen
パラメータ(後述)をtrueで実行
nextView
関数
-
nextView
関数で、ログイン時とみログイン時の判断ロジック追加- ロジックはソース中のコメント通り。下記の2パターンの動作を制御している(ログイン画面に遷移しようとしているかどうかは
tryShowLoginScreen
パラメータで判定)。- 既にログインしているのにログイン画面に遷移しようとした場合はTODO一覧画面にルーティング
- 未ログイン状態でTODO一覧やTODO詳細画面(ログイン画面以外)に遷移しようとした場合はログイン画面にルーティング
- ロジックはソース中のコメント通り。下記の2パターンの動作を制御している(ログイン画面に遷移しようとしているかどうかは
- コンテンツ(TODO一覧や詳細画面)のviewを表示する前に、ヘッダを表示。
showHeaderRegion
関数
- ログイン画面表示時はヘッダ用viewを非表示にする。
- コンテンツ画面表示時は、ヘッダ用viewを表示する。
user-model.js
追加したAPIを呼び出す。
//ログイン用モデル
define(function() {
var LoginModel = Backbone.Model.extend({
parse : function(response) {
if(response.message){
this.loginMessage = response.message;
}
return response.User;
},
loginMessage : null,
//ログイン済みかどうか判定
isLoggedIn : function(){
return this.get('id') ? true : false;
},
//ログイン中のユーザ情報取得
getLoginUser : function(onLoggedIn, onNotLoggedIn){
this.urlRoot = '/rest-study/users/loggedin';
this.fetch(
{
wait : true,
success : function(){
onLoggedIn();
},
error : function(){
onNotLoggedIn();
},
}
);
},
//ログインする
login : function(username, password, onLoginSuccess, onLoginError){
this.urlRoot = '/rest-study/users/login';
this.save(
{
username : username,
password : password
}, {
success : function(model){
if(model.get('id')){
onLoginSuccess(model.loginMessage);
}else{
onLoginError(model.loginMessage);
}
},
}
);
},
//ログアウトする
logout : function(onLogoutSuccess){
this.urlRoot = '/rest-study/users/logout';
this.save(
{},
{
success : function(model){
onLogoutSuccess(model.loginMessage);
},
}
);
},
//登録(サインアップ)する
signup : function(username, password, name, onSignUpSuccess, onSignUpError){
this.urlRoot = '/rest-study/users/signup';
this.save(
{
username : username,
password : password,
name : name
}, {
success : function(model){
if(model.get('id')){
onSignUpSuccess(model.loginMessage);
}else{
onSignUpError(model.loginMessage);
}
},
}
);
},
});
return LoginModel;
});
parse
関数
サーバからのレスポンスを下記の通り処理しています。
-
message
をloginMessage
変数に保持。 -
User
(ユーザ情報)を取り出してreturn。
各APIの実行方法
- GETメソッドで実行するもの(
loggedin
API)はfetch
関数を実行。 - POSTメソッドで実行するもの(
login
、logout
、signup
の各APIはsave
関数を実行。 - 各APIのURLは、各関数実行時に
urlRoot
変数を変更することで指定。 - APIからのレスポンス受取り後は、view側の関数をコールバックすることでviewの処理を実行。
- エラー時は、
app.js
で設定したajaxErrorHandler
関数が実行されるので、ここでは何もしない。
login-layout-view.js
Marionette.LayoutViewを継承して実装。
//ログイン画面用レイアウトビュー
define(function(require) {
var LoginModel = require('models/user-model');
var LoginLayoutView = Marionette.LayoutView.extend({
//テンプレート
template : '#login-layout-template',
//UIパーツ
ui : {
username : '#username', // ログイン時のユーザ名
password : '#password', // ログイン時のパスワード
loginButton : '#login', // ログインボタン
signupUsername : '#signup-username', // 登録時のユーザ名
signupPassword : '#signup-password', // 登録時のパスワード
name : '#signup-name', // 登録時の氏名
signupButton : '#signup' // 登録ボタン
},
//イベント
events : {
//ログインボタンクリック時
'click @ui.loginButton' : 'onLoginClick',
//登録ボタンクリック時
'click @ui.signupButton' : 'onsignupClick',
},
//ログインボタンクリック時
onLoginClick : function(){
//テキストボックスから文字を取得
var username = this.ui.username.val(); //ユーザ名
var password = this.ui.password.val(); //パスワード
window.application.loginUser.login(
username,
password,
this.onLoginSuccess,
this.onLoginError);
},
//ログイン処理成功時のコールバック
onLoginSuccess : function(message){
Backbone.history.navigate('todo-lists', {trigger: true, replace: true});
console.log(message);
},
//ログイン処理失敗時のコールバック
onLoginError : function(message){
alert(message);
},
//登録ボタンクリック時
onsignupClick : function(){
//テキストボックスから文字を取得
var username = this.ui.signupUsername.val(); //ユーザ名
var password = this.ui.signupPassword.val(); //パスワード
var name = this.ui.name.val(); //氏名
var userModel = new LoginModel();
userModel.signup(
username,
password,
name,
this.onsignupSuccess,
this.onsignupError);
},
//登録成功時のコールバック
onsignupSuccess : function(message){
alert(message);
},
//登録失敗時のコールバック
onsignupError : function(message){
alert(message);
},
});
return LoginLayoutView;
});
オーソドックスなビューの実装です。
-
ui
変数で各入力項目やボタンのhtmlエレメントを設定 -
event
変数でボタンのClickイベントとハンドラ関数を紐付け - 各Clickイベントハンドラ関数では
user-model
を使用してAPIを実行- ログイン時は、
app.js
内に保持しているuser-model
を使用(返ってきたユーザ情報をそのままapp.js
内に保持するため)。 - ユーザ登録時は、
user-model
を新しく生成して使用(返ってきたユーザ情報は使用しないため)
- ログイン時は、
- APIからのレスポンス時にコールバック実行、画面遷移やメッセージ表示を行う
- ログイン成功時のコールバック関数内で、TODO一覧画面に遷移させている
header-view.js
Marionette.ItemViewを継承して実装。
//ヘッダ用ビュー
define(function(require){
var UserModel = require('models/user-model');
var HeaderView = Marionette.ItemView.extend({
//テンプレート
template : '#header-template',
ui : {
logoutButton : '#logout',
},
//DOMイベントハンドラ設定
events : {
//ログアウトボタンクリック時
'click @ui.logoutButton' : 'onLogoutClick',
},
onLogoutClick : function(){
var userModel = new UserModel();
userModel.logout(this.onLogoutSuccess);
},
onLogoutSuccess : function(message){
window.application.clearLoginUser();
Backbone.history.navigate('#login', {trigger : true, replace : true});
console.log(message);
},
});
return HeaderView;
});
これもオーソドックスなビューの実装です。
-
ui
変数でログアウトボタンを設定 -
event
変数でonLogoutClick
関数を紐付け -
onLogoutClick
関数ではuser-model
を使用してAPIを実行 - APIからのレスポンス時に
onLogoutSuccess
関数実行、ログイン画面に遷移
実装
では、実際に修正して画面の動作確認をしてみましょう。
-
app/View/Layouts/default.ctp
を上記の通り修正。 -
app/webroot/js/main.js
を上記の通り修正。 -
app/webroot/js/app.js
を上記の通り修正。 -
app/webroot/js/routers/router.js
を上記の通り修正。 -
app/webroot/js/routers/controller.js
を上記の通り修正。 -
app/webroot/js/models/user-model.js
を上記の通り新規作成。 -
app/webroot/js/views/login-layout-view.js
を上記の通り新規作成。 -
app/webroot/js/views/header-view.js
を上記の通り新規作成。 - 動作確認!
- Gitにコミット
GitHubでのdiff表示へのリンク
第5回 Lesson2 クライアントサイド · suzukishouten-study/rest-study@d4ae8a4
以上です!
次回予告
次回テーマは、CakePHPでデータ操作〜担当者アサイン機能の実装です。
今回でユーザが識別できるように成ったので、各ユーザにTODOをアサインする機能などをつけていきます。
またぜひご参加ください!
コメント/フィードバックお待ちしております。
参加者の方も、そうでない方もお気づきの点があればお願い致します。