--- title: AngularJS でログインのフローを作る tags: Angular:1.2.15 author: ngyuki slide: false --- AngularJS で、よくあるログインのようなフローを作ってみました。 - 未ログインならログインページを表示する - 他の任意のURLはログインページへリダイレクトする - ログイン済みでログインページを表示しようとしたときはトップページにリダイレクトする - ナビゲーションの表示をログイン状態に応じて変化させる ## index.html 最低限の見た目のために Bootstrap を使っていますが、Bootstrap の CSS で `!important` な `display:block` が指定されているところがあり、ngCloak や ngShow が効かなくなることがあったのでそれらを上書きするための CSS を html 文中に書いています。 ```html:index.html AngularJS Example
``` body にはまず最初にナビゲーションがあります。 ナビゲーションはログインの前後で表示する内容が異なるため NavCtrl コントローラーで制御します。 ログイン後であれば、各ページヘのリンク、ユーザー名、ログアウトボタン、などを表示します。 未ログインであればそれらが表示されないように `ng-show` を利用しています。 ナビゲーションの次に ngView があります。 ここには未ログインであればログインページを表示し、ログイン済みならなにかしらのコンテンツを表示します。 ## login.html ログインページのテンプレートです。 ```html:login.html
{{alert.msg}}
``` ## ルーティング js でルーティングを定義します。otherwise を除いて全部で4つのルートを定義しています。 /login がログインページのルート、その他の3つはログイン後のページです。 ログインページ以外のルートはコントローラーやテンプレートを別に用意するのが面倒なので手抜きです。 ```js:app.js var app = angular.module('App', ['ngRoute']); app.config(function($routeProvider){ $routeProvider.when('/', {template: '

home

', controller: function(){}}); $routeProvider.when('/page1', {template: '

page1

', controller: function(){}}); $routeProvider.when('/page2', {template: '

page2

', controller: function(){}}); $routeProvider.when('/login', {templateUrl: 'view/login.html', controller: 'LoginCtrl'}); $routeProvider.otherwise({redirectTo: '/'}); }); ``` ## 認証 認証のためのサービスを定義します。 認証はユーザー名とパスワードに同じ値を入れれば通ります。というか、なにも入力しなくても通ります。 本当に実装するときは非同期の問合せになるので、500 ミリ秒遅延する Promise を返しています。 ```js:app.js app.factory('AuthService', function($q, $timeout){ var _user = null; return { isLogged: function(){ return !!_user; }, getUser: function(){ return _user; }, login: function(username, password){ var deferred = $q.defer(); $timeout(function(){ if (username == password) { _user = {username: username}; deferred.resolve(); } else { deferred.reject(); } }, 500); return deferred.promise; }, logout: function(){ _user = null; return $q.all(); } }; }); ``` ## \$routeChangeStart イベント 次にモジュールの run で `$rootScope` に `$routeChangeStart` イベントをバインドします。 このイベントは ngRoute のルーティングの開始時に呼ばれます。 これから表示しようとしているルートのコントローラーが LoginCtrl であるかどうか、ログイン済みであるかどうか、を元に必要に応じて適切なルートにリダイレクトします。 ```js:app.js app.run(function($rootScope, $location, $route, AuthService){ $rootScope.$on('$routeChangeStart', function(ev, next, current){ if (next.controller == 'LoginCtrl') { if (AuthService.isLogged()) { $location.path('/'); $route.reload(); } } else { if (AuthService.isLogged() == false) { $location.path('/login'); $route.reload(); } } }); }); ``` ## ログインコントローラー ログインコントローラーを作ります。 AuthService.login は Promise を返すので、成功時はトップにリダイレクト、失敗時はメッセージを表示します。 さらに失敗/成功に関わらずフォームをクリアしていますが、よく考えると成功時はこの処理必要ありませんでした。 ```js:app.js app.controller('LoginCtrl', function($scope, $location, AuthService){ $scope.login = function(){ $scope.disabled = true; AuthService.login($scope.username, $scope.password) .then(function(){ $location.path('/'); }) .catch(function(){ $scope.alert = {msg: "Login failed"}; }) .finally(function(){ $scope.username = ""; $scope.password = ""; $scope.disabled = false; }) ; }; }); ``` ## ナビコントローラー 最後にナビコントローラーを作ります。 ログイン状態は AuthService サービスが持っているので `$watch` で監視してナビコントローラーのスコープに設定します。 また、ログアウトでログインページにリダイレクトします。 ```js:app.js app.controller('NavCtrl', function($scope, $location, AuthService) { $scope.$watch( function(){ return AuthService.getUser() }, function(newVal, oldVal){ $scope.user = newVal } ); $scope.logout = function () { AuthService.logout().finally(function(){ $location.path('/login') }); }; }); ``` ## 問題点? "/" のルートに home.html などのテンプレートを使うと判るのですが、一番最初にサイトに訪れてログインページが表示されるとき、home.html がダウンロードされています。 `$routeChangeStart` イベントが呼ばれる時点ではまだテンプレートのダウンロードは開始されていないのですが、そのタイミングで `$location.path()` で遷移させてもテンプレートのダウンロードは止められないようです。 特に何が困るわけでもありませんが、なんとなく気持ち悪いです。 ## \$routeProvider の resolve での実装 \$routeChangeStart イベントは使わずに(app.run をごっそり削除)して、ルーティングを次のようにする方法も考えられます。 ```js app.config(function($routeProvider){ var requireAuth = { login: function($q, $location, AuthService){ if (AuthService.isLogged() == false) { $location.path('/login'); return $q.reject(); } } }; var skipLogin = { login: function($q, $location, AuthService){ if (AuthService.isLogged()) { $location.path('/'); return $q.reject(); } } }; $routeProvider.when('/', { template: '

home

', controller: function(){}, resolve: requireAuth }); $routeProvider.when('/page1', { template: '

page1

', controller: function(){}, resolve: requireAuth }); $routeProvider.when('/page2', { template: '

page2

', controller: function(){}, resolve: requireAuth }); $routeProvider.when('/login', { templateUrl: 'view/login.html', controller: 'LoginCtrl', resolve: skipLogin }); $routeProvider.otherwise({ redirectTo: '/' }); }); ``` ## ログイン時にルーティングを再設定する実装 あるいは、ログアウトは実際にブラウザをリロードするという前提で、ログイン後にルーティングを再設定する方法も考えられます。 ```js app.config(function($routeProvider){ // 最初は otherwise な LoginCtrl のルートしかない $routeProvider.otherwise({ templateUrl: 'view/login.html', controller: 'LoginCtrl', resolve: { login: function($route, AuthService){ if (AuthService.isLogged()) { // ログインに成功したらルートを追加&上書きする $routeProvider.when('/', { template: '

home

', controller: function(){} }); $routeProvider.when('/page1', { template: '

page1

', controller: function(){} }); $routeProvider.when('/page2', { template: '

page2

', controller: function(){} }); $routeProvider.otherwise({ redirectTo: '/' }); $route.reload(); } } } }); }); ``` ```js // 不要 // app.run(); ``` ```js app.controller('LoginCtrl', function($scope, $route, AuthService){ $scope.login = function(){ $scope.disabled = true; AuthService.login($scope.username, $scope.password) .then(function(){ // ログインに成功したらルーティングを呼ぶ $route.reload(); }) .catch(function(){ $scope.alert = {msg: "Login failed"}; }) .finally(function(){ $scope.username = ""; $scope.password = ""; $scope.disabled = false; }) ; }; }); ``` ```js app.controller('NavCtrl', function($scope, $window, AuthService) { $scope.$watch( function(){ return AuthService.getUser() }, function(newVal, oldVal){ $scope.user = newVal } ); $scope.logout = function () { AuthService.logout().finally(function(){ // ログアウトはブラウザのリロード // ($routeProvider に追加したルートをクリアする他の方法がわからない) $window.location.reload(); }); }; }); ``` この方法なら `/page1` とかをダイレクトに開いた場合でも、ログインページを挟んだあとに `/page1` を表示することが出来ます。 - デモ - [http://jsbin.com/dojoq/4/](http://jsbin.com/dojoq/4/)