Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
42
Help us understand the problem. What is going on with this article?
@fluke8259

4. JWT Authentication - Laravel5 + AngularJS で作るSPA

More than 5 years have passed since last update.

4. JWT

既存のやり方はCookieからSessionを探して、
Sessionからユーザのログイン状態を確認したが、
JWTはTokenを持っていること自体がログインされている意味であって、
Tokenの有効だけを確認しログイン状態を判断する認証方式である。

これで得られるのは、Sessionを管理する必要がないので、コストも下がるし、
複数のサーバにSessionを共有する手間もなくなる。

もちろん、Laravelの場合簡単に具現できるが ☺

唯一短所はTokenを送るためRequestの量が多くなる問題があるが、
Cookie事態を使わないので、その分相殺されるかなと思う。

Backend

まず、会員登録は後で作るつもりなので、User一人をTinkerで簡単に追加させる。

ほかのやり方があればそれでいい。


$ php artisan tinker

Tinkerに次のコードを入力し会員を作る

>>> App\User::create(['email'=>'laravelfanatic@example.com', 'name'=>'laravelfanatic', 'password'=>bcrypt('secret')]);

2つのRouteを追加する。

app/Http/routes.php
app/Http/routes.php
<?php

Route::get('/', function(Illuminate\Foundation\Inspiring $inspiring){

    $message = $inspiring->quote();

    return response(compact('message'), 200);

});

// Check if user has valid token
Route::get('auth', ['middleware' => 'jwt.auth', function(){
    $user = JWTAuth::parseToken()->toUser();

    return Response::json(compact('user'));
}]);

// Request token with email and password
Route::post('auth', function(){
    // grab credentials from the request
    $credentials = Input::only('email', 'password');

    try {
        // attempt to verify the credentials and create a token for the user
        if (! $token = JWTAuth::attempt($credentials)) {
            return response()->json(['error' => 'invalid_credentials'], 401);
        }
    } catch (JWTException $e) {
        // something went wrong whilst attempting to encode the token
        return response()->json(['error' => 'could_not_create_token'], 500);
    }

    // all good so return the token
    return response()->json(compact('token'));

});

GET methodのRouteはTokenからユーザ情報を習得する。
(ユーザがログイン中であるのかを確認する部分)

POST methodのRouteはE-mailと暗証番号からTokenを習得する。
(既存のログイン機能と似ている)

次はMiddlewareを追加する。

app/Http/Kernel.php
<?php namespace App\Http;

use Illuminate\Foundation\Http\Kernel as HttpKernel;

class Kernel extends HttpKernel {

    /**
     * The application's global HTTP middleware stack.
     *
     * @var array
     */
    protected $middleware = [
        'Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode',
        'App\Http\Middleware\EnableCors'
    ];

    /**
     * The application's route middleware.
     *
     * @var array
     */
    protected $routeMiddleware = [
        'jwt.auth' => 'Tymon\JWTAuth\Middleware\GetUserFromToken',
        'jwt.refresh' => 'Tymon\JWTAuth\Middleware\RefreshToken'
    ];

}

Frontend

Sign in pageから追加する

src/signin/signin.js
angular.module('lrng.signin')
    .config(function($stateProvider){
        $stateProvider.state('signin', {
            url:'/signin',
            templateUrl:'signin/signin.tpl.html',
            controller:'SignInController',
            controllerAs:'vm',
            data:{
                guest:true
            }
        });
    })
    .controller('SignInController', function(Auth, $state){
        var vm = this;

        vm.signIn = function(){
            vm.error = null;
            Auth.attempt(vm.email, vm.password, function(){
                if(Auth.hasPendingState()){
                    var pending = Auth.releasePendingState();
                    $state.go(pending.state, pending.params);
                    return;
                }
                $state.go('home');
            }, function(data){
                vm.error = data.error;
            });
        };
    });
src/signin/signin.tpl.html
<div class="container">
    <div class="row">
        <div class="col-sm-6 col-sm-offset-3">
            <h1>Sign in</h1>
            <div ng-show="vm.error" class="alert alert-danger">
                <strong>Oops...</strong>
                <span ng-bind="vm.error"></span>
            </div>
            <form class="" ng-submit="vm.signIn()" method="post">
                <div class="form-group">
                    <label for="email">E-mail</label>
                    <input ng-model="vm.email" name="email" id="email" class="form-control" type="email" required>
                </div>
                <div class="form-group">
                    <label for="password">Password</label>
                    <input ng-model="vm.password" name="password" id="password" class="form-control" type="password" required>
                </div>
                <div class="form-group">
                    <button class="btn btn-primary form-control">Sign In</button>
                </div>
            </form>
        </div>
    </div>

</div>

State設定のときdata項目は自由にデータを入れることができるので、
ここにどうFilteringさせるのかを決めた。
このStateはログイン済みのユーザが入ったらRedirectしてあげようとする。

Auth.attemptの中身で、3番目の引数は認証成功のときのCallbackであり、4番目の引数は認証失敗のときのCallbackである。
そして、3番ものPendingStateはmypageにログインせずに入ろうとしたときに、
signinにRedirectされるけど、
成功的にログインができたら、PendingStateにまた戻すためのコードである。

ログインが必要であるmypageというページも追加する。

src/mypage/mypage.js
angular.module('lrng.mypage')
    .config(function($stateProvider){
        $stateProvider.state('mypage', {
            url:'/mypage',
            templateUrl:'mypage/mypage.tpl.html',
            controller:'MyPageController',
            controllerAs:'vm',
            data:{
                auth:true
            }
        });
    })
    .controller('MyPageController', function(){
        var vm = this;
    });
src/mypage/mypage.tpl.html
<h1>MyPage</h1>

次は、index.htmlで行った設定をほかのファイルに分離した

src/core/config.js
angular.module('lrng.config')
    .config(function($urlRouterProvider, jwtInterceptorProvider, $httpProvider){

        $urlRouterProvider
            .when('', '/')
            .otherwise('/notfound');

        $urlRouterProvider.deferIntercept();

        jwtInterceptorProvider.tokenGetter = [function() {
            return localStorage.getItem('id_token');
        }];

        $httpProvider.interceptors.push('jwtInterceptor');
    })
    .factory('Config', function(){
        var rootUrl = 'http://localhost:8000/';

        return {
            rootUrl:rootUrl
        };
    });

$urlRouterProvider.deferIntercept()でUi-routerを止める(ログインが必要な場合ログインした後にページを遷移したいので)

jwtInterceptorProviderでTokenを保存するところを決める。

$httpProvider.interceptors.push('jwtInterceptor');はほかの設定がない限り、$httpを使うたびにに自動的にTokenを含めて送ってくれる。

src/core/auth.js
angular.module('lrng.auth')
    .factory('Auth', function($http, Config, $rootScope, $state){

        /**
         * Auth state
         * states [idle, auth, user, guest]
         * @type {String}
         */
        var state = 'idle';

        var currentUser = null;

        var authenticateToken = function(cbSuccess, cbError){
            console.log('authenticating token');
            state = 'auth';
            $rootScope.$broadcast('StartAuthenticating');

            var url = Config.rootUrl + 'auth';

            return $http.get(url).success(function(data){
                console.log(data.user);
                state = 'user';
                currentUser = data.user;

                $rootScope.$broadcast('EndAuthenticating', currentUser);
                angular.isFunction(cbSuccess)?cbSuccess(data):null;
            }).error(function(data){
                console.log(data.error);
                currentUser = null;
                state = 'guest';

                $rootScope.$broadcast('EndAuthenticating', null);
                angular.isFunction(cbError)?cbError(data):null;
            });
        };

        var attempt = function(email, password, cbSuccess, cbError){
            var url = Config.rootUrl + 'auth';

            return $http.post(url, {
                email:email,
                password:password
            })
            .success(function(data){
                console.log('Success');
                localStorage.setItem('id_token', data.token);
                authenticateToken(cbSuccess, cbError);
            })
            .error(function(data, status){
                console.log('Error occurs :' + status);
                angular.isFunction(cbError)?cbError(data):null;
            });
        };

        var signOut = function(){
            console.log('Sign out');
            localStorage.removeItem('id_token');
            currentUser = null;
            state = 'guest';
            $rootScope.$broadcast('EndAuthenticating', null);
            $state.go('home');
        };

        var isPending = false;
        var pendingState = null;
        var pendingParams = null;

        var setPendingState = function(state, params){
            isPending = true;
            pendingState = state;
            pendingParams = params;
        };
        var releasePendingState = function(){
            var result = {
                state:pendingState,
                params:pendingParams
            };

            isPending = false;
            pendingState = null;
            pendingParams = null;

            return result;
        };
        var hasPendingState = function(){
            return isPending;
        };

        return {
            authenticateToken:authenticateToken,
            attempt:attempt,
            signOut:signOut,
            getAuthState:function(){
                return state;
            },
            getCurrentUser:function(){
                return currentUser;
            },
            setPendingState:setPendingState,
            releasePendingState:releasePendingState,
            hasPendingState:hasPendingState
        };
    });

authenticateTokenは持っているTokenからユーザ情報を習得してくれる。最初の立ち上げとログイン直後に呼ばれる。

attemptはログインフォームからもらったEmailとPasswordを送ってTokenを確保する役割をする。

そして、ログインをするときにStartAuthenticationEndAuthenticationを投げるようにした。

次は、立ち上げた最初に起動するコードである。

src/core/init.js
angular.module('lrng.init')
    .run(function(Auth, $urlRouter, $rootScope, $state){
        Auth.authenticateToken(function(){
            $urlRouter.sync();
        },function(){
            $urlRouter.sync();
        });

        $rootScope.$on('$stateChangeStart', function(event, toState, toParams){

            var go = function(state, params){
                event.preventDefault();
                $state.go(state, params);
            };

            // return if target state need no filtering
            if(!angular.isObject(toState.data) || (!toState.data.auth && !toState.data.guest)) return;

            var authState = Auth.getAuthState();

            // if on attempting, ignore state change
            if(authState=='auth') {
                event.preventDefault();
                return;
            }

            // only for authenticated user
            if(angular.isObject(toState.data) && toState.data.auth){
                switch(authState){
                    case 'user':
                        return;
                    case 'guest':
                    case 'init':
                        console.log('redirect signin');
                        Auth.setPendingState(toState.name, toParams);
                        go('signin');
                        return;
                }
            }

            // only for guest
            if(angular.isObject(toState.data) && toState.data.guest){
                switch(authState){
                    case 'user':
                        console.log('redirect home');
                        go('home');
                        return;
                }
            }

        });
        $rootScope.$on('$stateChangeSuccess', function(event, toState){
            if(Auth.hasPendingState() && toState.name!=='signin') Auth.releasePendingState();
        });

        $urlRouter.listen();
    });

まず、先書いたauthenticateTokenを起動しておいて、ログイン情報が確保できてから$urlRouter.sync()でURL routerを起動させる。

そして、$stateChangeStartEvent handlerを追加して、ログイン状況に合わせてStateをFilteringするようにした。
$stateChangeSuccessの場合、PendingStateを持っている状況でログインせずにほかのページに飛んだときにPendingStateを放すために書いておいた。

$urlRouter.listen()で復活させる。ただし、最初は$urlRouter.sync()でRefreshさせる必要がある。

次はTop-Navをいじる。

src/directives/top-nav/top-nav.js
angular.module('lrng.top-nav')
    .directive('topNav', function(){
        return {
            restrict:'E',
            templateUrl:'directives/top-nav/top-nav.tpl.html',
            controller:'TopNavController',
            controllerAs:'vm',
            scope:{}
        }
    })
    .controller('TopNavController', function(Auth, $scope){
        var vm = this;

        vm.collapsed = true;
        vm.currentUser = null;
        vm.onAuthenticating = true;

        $scope.$on('StartAuthenticating', function(){
            vm.onAuthenticating = true;
        });

        $scope.$on('EndAuthenticating', function(event, user){
            vm.onAuthenticating = false;
            vm.currentUser = user;
        });

        vm.signOut = function(){
            Auth.signOut();
        };

    });
src/directives/top-nav/top-nav.tpl.html
<nav class="navbar navbar-default">
    <div class="container-fluid">

        <div class="navbar-header">

            <button type="button" class="navbar-toggle collapsed"
            ng-click="topNav.collapsed = !topNav.collapsed">
                <span class="sr-only">Toggle navigation</span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
            </button>

            <a class="navbar-brand" href="#">LrNg</a>

        </div>

        <div class="collapse navbar-collapse" collapse="topNav.collapsed">
            <ul class="nav navbar-nav">

                <li ui-sref-active="active"><a  ui-sref="home">Home</a></li>

                <li ui-sref-active="active"><a ui-sref="about">About</a></li>

                <li ui-sref-active="active" ng-show="vm.currentUser"><a ui-sref="mypage">My page</a></li>

            </ul>
            <ul ng-show="vm.onAuthenticating" class="nav navbar-nav navbar-right">
                <li><a>Authenticating...</a></li>
            </ul>
            <ul ng-hide="vm.onAuthenticating" class="nav navbar-nav navbar-right">
                <li ng-hide="vm.currentUser" ui-sref-active="active"><a ui-sref="signin">Sign In</a></li>
                <li ng-show="vm.currentUser" dropdown is-open="status.isopen">
                    <a dropdown-toggle ng-bind="vm.currentUser.name"></a>
                    <ul class="dropdown-menu" role="menu">
                      <li><a href="#">Action</a></li>
                      <li><a href="#">Another action</a></li>
                      <li><a href="#">Something else here</a></li>
                      <li class="divider"></li>
                      <li><a ng-click="vm.signOut()">Sign Out</a></li>
                    </ul>
                </li>
            </ul>
        </div>
    </div>
</nav>

StartAuthenticatingEndAuthenticatingイベントを利用することで、内容を変えることができる。

最終的にすべてのModuleを片づける。

src/index.html
<!doctype html>
<html ng-app="lrng">
<head>
    <meta charset="utf-8">
    <title>Yolo!</title>
    <link rel="stylesheet" href="vendor/css/font-awesome.css">
    <link rel="stylesheet" href="all.css" media="screen" title="no title" charset="utf-8">
</head>
<body>
    <top-nav></top-nav>
    <section ui-view></section>

    <script src="vendor/angular.js"></script>
    <script src="vendor/angular-ui-router.js" charset="utf-8"></script>
    <script src="vendor/ui-bootstrap-tpls.js" charset="utf-8"></script>
    <script src="vendor/angular-jwt.js" charset="utf-8"></script>
    <script charset="utf-8">
    // app
    angular.module('lrng', ['templates', 'angular-jwt', 'ui.bootstrap', 'ui.router', 'lrng.states', 'lrng.directives', 'lrng.core']);

    // core
    angular.module('lrng.core', [
        'lrng.init',
        'lrng.config',
        'lrng.auth'
        ]);
    angular.module('lrng.init', ['lrng.auth', 'ui.router']);
    angular.module('lrng.config', ['angular-jwt', 'ui.router']);
    angular.module('lrng.auth', ['lrng.config', 'ui.router']);

    // directives
    angular.module('lrng.directives', [
        'lrng.top-nav'
        ]);
    angular.module('lrng.top-nav', ['ui.router']);

    // states
    angular.module('lrng.states', [
        'lrng.home',
        'lrng.about',
        'lrng.mypage',
        'lrng.signin',
        'lrng.errors'
        ]);
    angular.module('lrng.home', ['ui.router', 'lrng.inspiring']);
    angular.module('lrng.about', ['ui.router']);
    angular.module('lrng.mypage', ['ui.router']);

    angular.module('lrng.signin', ['ui.router', 'lrng.auth']);

    // error states
    angular.module('lrng.errors', [
        'lrng.errors.notfound'
    ]);
    angular.module('lrng.errors.notfound', ['ui.router']);

    // services
    angular.module('lrng.inspiring', ['lrng.config']);

    // templates
    angular.module('templates', []);

    </script>
    <% scripts.forEach(function(script){%>
    <script src="<%=script%>" charset="utf-8"></script>
    <% });%>
</body>
</html>

書いてみて分かったけど、Laravelを使うときは基本的に入っている機能が入っていなくて、かなり長く書いてしまったと思う。
時間があれば、LaravelのAPIをそのまま映した感じのAngularLibraryを作ったらどうかなと思ったけど、時間的にどうなるのかがわからないので、必要に合わせて少しずつ追加しようと思っている

42
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
fluke8259
九州大学 工学部機械航空学科 航空宇宙コース 学部3年 / MAISIN&CO. CTO兼取締役 / Dick Choi / サイ / No Food, No Sleep, Just Coding / Boost開発者 / JS, Python主に使います / 彼女ほしいです

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
42
Help us understand the problem. What is going on with this article?