271
275

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

AngularJS でログインのフローを作る

Last updated at Posted at 2014-03-28

AngularJS で、よくあるログインのようなフローを作ってみました。

  • 未ログインならログインページを表示する
    • 他の任意のURLはログインページへリダイレクトする
  • ログイン済みでログインページを表示しようとしたときはトップページにリダイレクトする
  • ナビゲーションの表示をログイン状態に応じて変化させる

index.html

最低限の見た目のために Bootstrap を使っていますが、Bootstrap の CSS で !importantdisplay:block が指定されているところがあり、ngCloak や ngShow が効かなくなることがあったのでそれらを上書きするための CSS を html 文中に書いています。

index.html
<!doctype html>
<html ng-app="App">
<head>
<meta charset="utf-8">
<title>AngularJS Example</title>
<link rel="stylesheet" href="lib/bootstrap/dist/css/bootstrap.css"/>
<style>
#body [ng-cloak],
#body .ng-hide {
    display: none !important;
}
</style>
</head>
<body id="body">
<nav class="navbar navbar-default" ng-controller="NavCtrl">
    <div class="container-fluid">
        <div class="navbar-header">
            <a class="navbar-brand" href="#/">AngularJS Example</a>
        </div>
        <div class="collapse navbar-collapse" ng-cloak ng-show="user">
            <ul class="nav navbar-nav">
                <li><a href="#/">Home</a></li>
                <li><a href="#/page1">Page1</a></li>
                <li><a href="#/page2">Page2</a></li>
            </ul>
            <form class="navbar-form navbar-right">
                <button type="button" class="btn btn-default" ng-click="logout()">Logout</button>
            </form>
            <div class="navbar-text navbar-right">
                {{user.username}}
            </div>
        </div>
    </div>
</nav>
<div class="container-fluid">
    <div ng-view></div>
</div>
<script src="lib/jquery/dist/jquery.js"></script>
<script src="lib/angular/angular.js"></script>
<script src="lib/angular-route/angular-route.js"></script>
<script src="js/app.js"></script>
</body>
</html>

body にはまず最初にナビゲーションがあります。
ナビゲーションはログインの前後で表示する内容が異なるため NavCtrl コントローラーで制御します。
ログイン後であれば、各ページヘのリンク、ユーザー名、ログアウトボタン、などを表示します。
未ログインであればそれらが表示されないように ng-show を利用しています。

ナビゲーションの次に ngView があります。
ここには未ログインであればログインページを表示し、ログイン済みならなにかしらのコンテンツを表示します。

login.html

ログインページのテンプレートです。

login.html
<form class="form-inline" ng-submit="login()">
    <input type="text" class="form-control" placeholder="username" ng-model="username">
    <input type="password" class="form-control" placeholder="password" ng-model="password">
    <button type="submit" class="btn btn-default" ng-disabled="disabled">Login</button>
    <span class="text-danger" ng-show="alert">{{alert.msg}}</span>
</form>

ルーティング

js でルーティングを定義します。otherwise を除いて全部で4つのルートを定義しています。

/login がログインページのルート、その他の3つはログイン後のページです。

ログインページ以外のルートはコントローラーやテンプレートを別に用意するのが面倒なので手抜きです。

app.js
var app = angular.module('App', ['ngRoute']);

app.config(function($routeProvider){
    $routeProvider.when('/', {template: '<h1>home</h1>', controller: function(){}});
    $routeProvider.when('/page1', {template: '<h1>page1</h1>', controller: function(){}});
    $routeProvider.when('/page2', {template: '<h1>page2</h1>', controller: function(){}});
    $routeProvider.when('/login', {templateUrl: 'view/login.html', controller: 'LoginCtrl'});
    $routeProvider.otherwise({redirectTo: '/'});
});

認証

認証のためのサービスを定義します。

認証はユーザー名とパスワードに同じ値を入れれば通ります。というか、なにも入力しなくても通ります。

本当に実装するときは非同期の問合せになるので、500 ミリ秒遅延する Promise を返しています。

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 であるかどうか、ログイン済みであるかどうか、を元に必要に応じて適切なルートにリダイレクトします。

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 を返すので、成功時はトップにリダイレクト、失敗時はメッセージを表示します。

さらに失敗/成功に関わらずフォームをクリアしていますが、よく考えると成功時はこの処理必要ありませんでした。

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 で監視してナビコントローラーのスコープに設定します。

また、ログアウトでログインページにリダイレクトします。

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 をごっそり削除)して、ルーティングを次のようにする方法も考えられます。

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: '<h1>home</h1>',
        controller: function(){},
        resolve: requireAuth
    });
    $routeProvider.when('/page1', {
        template: '<h1>page1</h1>',
        controller: function(){},
        resolve: requireAuth
    });
    $routeProvider.when('/page2', {
        template: '<h1>page2</h1>',
        controller: function(){},
        resolve: requireAuth
    });
    $routeProvider.when('/login', {
        templateUrl: 'view/login.html',
        controller: 'LoginCtrl',
        resolve: skipLogin
    });
    $routeProvider.otherwise({
        redirectTo: '/'
    });
});

ログイン時にルーティングを再設定する実装

あるいは、ログアウトは実際にブラウザをリロードするという前提で、ログイン後にルーティングを再設定する方法も考えられます。

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: '<h1>home</h1>', controller: function(){}
                    });
                    $routeProvider.when('/page1', {
                        template: '<h1>page1</h1>', controller: function(){}
                    });
                    $routeProvider.when('/page2', {
                        template: '<h1>page2</h1>', controller: function(){}
                    });
                    $routeProvider.otherwise({
                        redirectTo: '/'
                    });
                    $route.reload();
                }
            }
        }
    });
});
// 不要
// app.run();
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;
            })
        ;
    };
});
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 を表示することが出来ます。

271
275
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
271
275

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?