Edited at

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

More than 5 years have passed since last update.

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 を表示することが出来ます。