Laravel Augnlar Material Starterは、フロントエンドにAngular Material、バックエンドAPIにLaravelを利用したシングルページアプリケーションを開発するためのスターターパッケージです。

Laravelは、Laravel ElixirというJavascriptやCSSなどのアセットをビルドするためのgulpタスクがフレームワークに予め含まれていますが、Laravel Angular Material Starterは、AngularJSとAngularJSベースのUIコンポーネントフレームワーク Angular MaterialをビルドするためにLaravel Elixirが予め構成されたLaravelプロジェクトと言った構成になっています。
また、フロントエンドのAngularJSから呼び出されるLaravel側のAPIに対する認証はJWTを利用します。

この記事では、主にフロントエンドのAPIの呼び出しと、バックエンドのJWT生成、検証に利用されるパッケージの構成について調べたことをまとめていきます。

含まれるパッケージ

Laravel Angular Material Starterには、2016.7時点でLaravel 5.2とAngularJS 1.5をベースに以下のパッケージが含まれています。

公式ドキュメントでは「Libraries involved」に載っています。

Composerパッケージ

Laravel側のパッケージは、バックエンドAPIおよび認証をサポートする以下のようなパッケージを含んでいます。

  • dingo/api: Laravel、LumenでRestful APIを管理するミドルウェアを提供するパッケージ
  • tymon/jwt-auth: Laravel、Lumen用のJWTの生成、認証の仕組みを提供するパッケージ
  • barryvdh/laravel-cors: Laravel、Lumen用のCross-Origin Resource Sharingを提供するパッケージ

Bowerパッケージ

フロントエンドのパッケージ管理はBowerを利用しています。

  • AngularJS (~1.5)
  • Angular Material (~1.1)
  • Satellizer (^0.14.0): クライアント側のOAuth認証処理をラップするサービスを提供します
  • Restangular (~1.5.2): Laravelで実装したREST APIへのアクセスに利用します

npmパッケージ

npmは、テスト・ビルド関連のパッケージを含みます。

AngularJSのビルドに使われる主なパッケージは以下のとおりです。

  • webpack (^1.12.11″): webpackはJSのビルドのみに使用しています
  • babel (^4.0.2): ES6 (ES2015) のコンパイラー、webpackから呼ばれます
  • browserify (~8.1.3): webpackに移行してから使われていないと思われます
  • eslint (^1.10.3): JSの構文解析ツールはeslintを使用
  • gulp-uglify (^1): プロダクション用JSファイルの圧縮に使用
  • gulp-sourcemaps (^1): プロダクション用JSファイルののソースマップ生成に使用

AngularJSのビルド用gulpタスクは、tasks/angular.task.jsに記述されています。

システム構成

含まれるパッケージを踏まえて、システム構成を図にしてみるとおおむね以下のようになります。

laravelangularmaterialstarter

認証の際は、フロントエンドではSatellizerを利用してバックエンドの認証APIを呼び出します。

Laravel側のバックエンドAPIは、すべてdingo/apiのミドルウェアを通ります。
dingo/apiのミドルウェアは、バージョンの管理や認証などの処理を行います。

ログインAPIは、tymon/jwt-authを利用して認証を行いJWTをクライアントに返します。
Satellizerは、JWTを受け取るとローカルストレージに保存します。

認証後のAPI呼び出しについては、フロントエンドはRestangularを利用してAPIを呼び出します。
Restangularは、デフォルトでSatellizerが保存したJWTをローカルストレージから取得してAuthorizationヘッダにセットするように構成されています。

Laravel側は、dingo/apiのミドルウェアからtymon/jwt-authに設定された認証プロバイダによってJWTが検証され、該当するバージョンのRest APIが呼び出されます。

以上が、APIの呼び出し周りの主な流れです。

以降、プロジェクトの動作を実際に確認しつつ、詳細を追っていきます。

プロジェクトの作成と初期動作確認

Node.jsの環境も必要なので、ローカルの開発環境はLaravel Homesteadが便利です。

以下、ドキュメントのInstallをHomesteadで試したコマンドになります。

$ composer create-project jadjoubran/laravel5-angular-material-starter --prefer-dist
$ cd laravel5-angular-material-starter

gulp bowerをグローバルインストールして、必要なパッケージをそれぞれインストール

$ sudo npm install -g gulp bower
$ npm install
$ bower install

.envは、HomesteamのMySQLでそのまま利用可能です。
マイグレーションします。

$ php artisan migrate
Migration table created successfully.
Migrated: 2014_10_12_000000_create_users_table
Migrated: 2014_10_12_100000_create_password_resets_table

デフォルトのLaravel5プロジェクトと同じユーザーテーブルが作成されました。

gulpを実行するとangularディレクトリ以下のアセットがビルドされます。

$ gulp

以上で、ブラウザにアクセスすると以下のようなデフォルトページが表示されます。
laravel-angular-material-starter-index-page
個々では、laravel5angularmaterial.appをHomesteadのホスト名に設定しています。

プロジェクトのディレクトリ構成

プロジェクトの構成については、ドキュメントの「Folder Structure」に記述されています。

Laravel Angular Material Starterで追加されているディレクトリ、ファイルをハイライトしてみると以下のようになります。

.
├── angular
│   ├── app
│   ├── config
│   ├── dialogs
│   ├── directives
│   ├── filters
│   ├── index.components.js
│   ├── index.config.js
│   ├── index.directives.js
│   ├── index.filters.js
│   ├── index.main.js
│   ├── index.modules.js
│   ├── index.run.js
│   ├── index.services.js
│   ├── material
│   ├── run
│   └── services
├── app
├── artisan
├── bootstrap
├── bower.json
├── composer.json
├── composer.lock
├── config
├── database
├── elixir.json
├── gulpfile.js
├── karma.conf.js
├── package.json
├── phpunit.xml
├── public
│   ├── build
│   ├── css
│   ├── favicon.ico
│   ├── img
│   ├── index.php
│   ├── js
│   ├── robots.txt
│   └── web.config
├── readme.md
├── resources
│   ├── lang
│   └── views
├── server.php
├── storage
│   ├── app
│   ├── framework
│   └── logs
├── tasks
│   ├── angular.task.js
│   ├── bower.task.js
│   └── ngHtml2Js.task.js
├── tests
│   ├── AngularGeneratorCommandsTest.php
│   ├── JwtAuthTest.php
│   ├── LaravelRoutesTest.php
│   ├── PasswordResetTest.php
│   ├── TestCase.php
│   └── angular
└── webpack.config.js

AngularJSプロジェクトは、angularディレクトリにまとめられています。
ビルドタスクは、laravel-elixirのgulpfile.jsに、tasksディレクトリの各タスクが読み込まれるように設定されています。
tasks/angular.task.jsは、webpack.config.jsを読み込みwebpackを使って、JSやCSSをビルドするように構成されています。
テストについては、tests/angularディレクトリに置かれた*.spec.jsが対称となるようにkarma.conf.jsに設定されいます。

ログインの実装

Laravel Angular Material Starterには、ユーザー登録・ログイン APIと対応するページがすでに用意されています。
それらをアプリケーションに組み込むには、ユーザー登録・ログインページヘリンクするボタンを配置するだけです。

ここでは、ログインの仕組みについてデフォルトの実装を確認してみます。

ユーザー登録・ログインの動作確認

まずは、デフォルトのログインページを確認してみます。

Angularのルーティングは、AngularRouterを利用しており、ルーティングの設定はangular/config/routes.config.jsにあります。
以下、ログインページとユーザー登録ページのルーティングが確認できます。

export function RoutesConfig($stateProvider, $urlRouterProvider) {
  'ngInject';

  let getView = (viewName) => {
    return `./views/app/pages/${viewName}/${viewName}.page.html`;
  };

  $urlRouterProvider.otherwise('/');

  $stateProvider
        ...
        .state('app.login', {
          url: '/login',
          views: {
            'main@': {
              templateUrl: getView('login')
            }
          }
        })
        .state('app.register', {
            url: '/register',
            views: {
                'main@': {
                    templateUrl: getView('register')
                }
            }
        })
        ...
}

ブラウザで、/#/registerにアクセスすると以下のユーザー登録ページが表示されます
laravel-angular-material-starter-register-page

ログインページは、/#/loginを開きます。
laravel-angular-material-starter-login-page

バックエンドのログインAPI実装

Laravel側のログインAPIを見てみます。

ログインAPIはapp/Http/Controllers/Auth/AuthController.phpに実装されています。

<?php

namespace App\Http\Controllers\Auth;

use Auth;
use JWTAuth;
use App\User;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;

class AuthController extends Controller
{
    public function login(Request $request)
    {
        $this->validate($request, [
            'email'    => 'required|email',
            'password' => 'required|min:8',
        ]);

        $credentials = $request->only('email', 'password');

        try {
            // verify the credentials and create a token for the user
            if (! $token = JWTAuth::attempt($credentials)) {
                return response()->error('Invalid credentials', 401);
            }
        } catch (\JWTException $e) {
            return response()->error('Could not create token', 500);
        }

        $user = Auth::user();

        return response()->success(compact('user', 'token'));
    }


    public function register(Request $request){ ... }
}

認証処理は、tymon/jwt-authが提供するJWTAuthファサードのattempt呼び出します。
標準のAuth::attemptは、認証が成功したかどうかの真偽値を返しますが、JWTAuthは、JWTを返します。

認証に成功した場合は、ユーザー情報とJWTをレスポンスとして返します。

{"errors":false,"data":{"user":{"id":1,"name":"Suzuki","email":"suzuki@example.com","created_at":"2016-07-18 10:27:09","updated_at":"2016-07-18 10:27:09"},"token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOjEsImlzcyI6Imh0dHA6XC9cL2xhcmF2ZWw1YW5ndWxhcm1hdGVyaWFsLmFwcFwvYXBpXC9hdXRoXC9sb2dpbiIsImlhdCI6MTQ2ODg0ODAwMiwiZXhwIjoxNDY4ODUxNjAyLCJuYmYiOjE0Njg4NDgwMDIsImp0aSI6IjYyMWIwMDhkZjQzZmZjZmZiOWY3YzIzNGIxODliNTY3In0.8CHZb-jyYsH7-RmrXmmjiVe5E2YGWl-EsLLR1b4O3Z0"}}

補足: 生成されたJWTの内容を確認するにはjwt.ioのDebuggerが便利です。
以下、上記のJWTを検証してみた結果になります。
laravel-angular-material-starter-decode-jwt

Satellizerによる認証APIの呼び出し

Laravelに実装された認証APIをAngularJSから呼び出す処理については、Satellizerを利用して実装しています。

Satellizerの設定は、angular/config/satellizer.config.jsにあります。

export function SatellizerConfig($authProvider) {
	'ngInject';

	$authProvider.httpInterceptor = function() {
		return true;
	}

	$authProvider.loginUrl = '/api/auth/login';
	$authProvider.signupUrl = '/api/auth/register';
	$authProvider.tokenRoot = 'data';//compensates success response macro

}
  • loginUrl: ログインAPIのパス
  • signupUrl: ユーザー登録APIのパス
  • tokenRoot: ログインAPIまたはユーザー登録APIのレスポンスJSONに含まれるJWTのキー

ログイン画面で、[Login]ボタンを押すと、コントローラのloginメソッドが呼ばれ、$authでインジェクションされるSatellizerのloginメソッドを実行すると、ログインAPIが呼び出されます。

class LoginFormController {
	constructor($auth, ToastService) {
		'ngInject';

		this.$auth = $auth;
		this.ToastService = ToastService;
	}
	...
	login() {
		let user = {
			email: this.email,
			password: this.password
		};

		this.$auth.login(user)
			.then((response) => {
				this.$auth.setToken(response.data);

				this.ToastService.show('Logged in successfully.');
			})
			.catch(this.failedLogin.bind(this));
	}
	...
}
...

ログイン成功したら応答を受け取ったら、$auth.login メソッドでログインし、レスポンスのdataのJWTを$auth.setTokenでローカルストレージに保存します(Satellizerのloginメソッドの中(20行目)でsetTokenが実行されるので、不要な気がする)。

satellizer.config.jsでJWTを含むオブジェクトのキーをdataに指定し、また、tokenNameのデフォルトはtokenなので、レスポンスのJSONから、data.tokenの値を_というキー(デフォルト設定では、satellizer_token)でローカルストレージに保存します。

Firefoxのローカルストレージインスペクタで保存されたJWTを確認すると、satellizer_tokenにJWTが保存されていることを確認できます。
laravel-angular-material-starter-show-jwt-in-local-storage

APIの認証

Laravel Angular Material StarterのバックエンドAPIは、dingo/apiを利用して実装しています。

dingo/apiは、LaravelでAPIを実装するために以下のような機能を提供しています。

  • Content Negotiation: HttpのContent Negotiationの仕組みを提供します
  • Multiple Authentication Adapters: 認証プロバイダBasic、JWT、OAuth2およびカスタムが用意されており、それらをルーティングのミドルウェア設定で組み合わせることもできます
  • API Versioning: APIのバージョンを管理、バージョンごとのルーティングにも対応可能 (Creating API Endpoints)
  • Rate Limiting: ユーザーやIPアドレスごとのスロットリングができます
  • Response Transformers and Formatters: API応答の変換、フォーマット機能を提供します
  • Error and Exception Handling: エラー発生時の例外処理と対応するHTTPレスポンスを生成してくれるヘルパー
  • Internal Requests: Laravelのコントローラーなどの内部コードからAPIを呼び出せる機能
  • API Blueprint Documentation: アノテーションを記述しておくとartisan api:docsコマンドを利用してAPIドキュメント(Markdown形式)を生成してれる機能

と、APIを実装するにあたって必要十分な機能が揃っています。

JWT認証の設定

dingo/apiのデフォルトの認証プロバイダはBasicなので、JWT認証プロバイダを利用する場合は設定が必要になります。

dingo/apiの設定ファイルは、config/api.phpです。
config/api.phpのauth.jwtを以下のようにセットします。

    'auth' => [
        'jwt' => 'Dingo\Api\Auth\Provider\JWT',
    ],

JWTの検証は、tymon/jwt-authが内部で利用されます。

APIにJWT認証を設定

dingo/apiに指定した認証をAPIにかける場合、ミドルウェアはapi.authを指定します。

以下、はデフォルトのルーティングですが、対象のAPIはまだありませんが、37行目に雛形として準備されています。

<?php

/*
|--------------------------------------------------------------------------
| Application Routes
|--------------------------------------------------------------------------
|
| Here is where you can register all of the routes for an application.
| It's a breeze. Simply tell Laravel the URIs it should respond to
| and give it the controller to call when that URI is requested.
|
*/

Route::group(['middleware' => ['web']], function () {

    Route::get('/', 'AngularController@serveApp');

    Route::get('/unsupported-browser', 'AngularController@unsupported');

});

//public API routes
$api->group(['middleware' => ['api']], function ($api) {

    // Authentication Routes...
    $api->post('auth/login', 'Auth\AuthController@login');
    $api->post('auth/register', 'Auth\AuthController@register');

    // Password Reset Routes...
    $api->post('auth/password/email', 'Auth\PasswordResetController@sendResetLinkEmail');
    $api->get('auth/password/verify', 'Auth\PasswordResetController@verify');
    $api->post('auth/password/reset', 'Auth\PasswordResetController@reset');

});

//protected API routes with JWT (must be logged in)
$api->group(['middleware' => ['api', 'api.auth']], function ($api) {

});

APIのサンプル実装と動作確認

最後にAPIを追加して、API呼び出し周りの動作と仕組みを確認していきます。

追加するAPIは、TutorialのPostを作成するAPIを追加します。

Postモデルの作成

--migrationオプションを指定してPostモデルを生成します。

$ php artisan make:model Post --migration
Model created successfully.
Created Migration: 2016_08_01_072721_create_posts_table

マイグレーションに、nametopicフィールドを追加します。

<?php

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreatePostsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->increments('id');
            $table->string('name');
            $table->string('topic');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::drop('posts');
    }
}

マイグレーションを適用します。

$ php artisan migrate
Migrated: 2016_08_01_072721_create_posts_table

コントローラーとルーティングの作成

ここで実装するのはcreateメソッドのみですが、他のCRUD処理も実装することを想定して、--resourceオプションを指定してPostControllerを作成してみます。

$ php artisan make:controller PostController --resource
Controller created successfully.

App\Postをインポートし、Laravel Controllerのコードのcreateメソッドの内容をコピペします。

<?php

namespace App\Http\Controllers;

use App\Post;
use App\Http\Requests;
use Illuminate\Http\Request;

class PostController extends Controller
{
    ...
    /**
     * Show the form for creating a new resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function create()
    {
      $this->validate($request, [
        'name'  => 'required',
        'topic' => 'required',
      ]);
  
      $post = new Post;
      $post->name = $request->input('name');
      $post->topic = $request->input('topic');
      $post->save();
  
      return response()->success(compact('post'));
    }
    ...
}

追加したAPIを、認証の対象としてルーティングを追加します。
dingo/apiの認証プロバイダを呼び出すミドルウェアapi.authが指定されている箇所に追加します。

$api->group(['middleware' => ['api', 'api.auth']], function ($api) {
    $api->resource('posts', 'PostController');
});

Post作成フォームの実装

Angular ComponentおよびAngular Pageに沿って、AngularJS側に、Post追加用のフォームを追加します。

フロントエンドのルーティング

フロントエンドのルーティングは、angular/config/routes.config.jsに設定します。
追加したページヘのルーティングを、Front-end routesのコードをコピーして追加します。

また、/create-postを認証済みの場合にのみ表示したい場合、Authenticated routes (front-end)を参考にdata.auth = trueを指定します。

        .state('app.create_post', {
          url: '/create-post',
          data: {
            auth: true
          },
          views: {
            'main@': {
              templateUrl: getView('create_post')
            }
          }
        });

この処理は、angular/run/routes.run.jsに記述されています。

以下のように、ui-routerのState Change Eventsイベント発生時に、$authでインジェクションされるSatellizerのisAuthenticatedメソッドで認証済みかどうかチェックし、認証していなければログインページを表示しています。

export function RoutesRun($rootScope, $state, $auth) {
    'ngInject';


    let deregisterationCallback =  $rootScope.$on("$stateChangeStart", function(event, toState) {

        if (toState.data && toState.data.auth) {
            /*Cancel going to the authenticated state and go back to the login page*/
            if (!$auth.isAuthenticated()) {
                event.preventDefault();
                return $state.go('app.login');
            }
        }

    });
    $rootScope.$on('$destroy', deregisterationCallback)
}

ログイン後、/#/create-postを開き、適当に入力して[CREATE POST]をクリックするとAPIが呼ばれ、成功すると以下のように「Post added successfully」とToastが表示されます。
laravel-angular-starter-tutorial-create-pot-form

また、ローカルストレージから、satellizer_tokenを削除してPostの作成をためしてみると401が返ってくることが確認できます。

補足: ログアウトの実装

Gulp Watchしておきます。

$ gulp && gulp watch

artisanコマンドで、ログアウトボタンを生成します。

$ artisan ng:component logout-button
Component created successfully.
class LogoutButtonController{
    constructor($auth, ToastService) {
        'ngInject';

        this.$auth = $auth;
        this.ToastService = ToastService;
    }

    $onInit(){
    }

    logout() {
      this.$auth.logout().then(() => {
        this.ToastService.show('You have successfully logout.');
      })
    }
}

export const LogoutButtonComponent = {
    templateUrl: './views/app/components/logout-button/logout-button.component.html',
    controller: LogoutButtonController,
    controllerAs: 'vm',
    bindings: {}
}

参考

LARAVEL 5.2 API USING JWT AUTHENTICATION TUTORIAL FROM SCRATCH