Laravel Angular Material Starter API認証周りの構成についてのメモ

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で試したコマンドになります。

[bash]
$ composer create-project jadjoubran/laravel5-angular-material-starter –prefer-dist
$ cd laravel5-angular-material-starter
[/bash]

gulp bowerをグローバルインストールして、必要なパッケージをそれぞれインストール
[bash]
$ sudo npm install -g gulp bower
$ npm install
$ bower install
[/bash]

.envは、HomesteamのMySQLでそのまま利用可能です。
マイグレーションします。
[bash]
$ php artisan migrate
Migration table created successfully.
Migrated: 2014_10_12_000000_create_users_table
Migrated: 2014_10_12_100000_create_password_resets_table
[/bash]
デフォルトのLaravel5プロジェクトと同じユーザーテーブルが作成されました。

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

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

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

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

Laravel Angular Material Starterで追加されているディレクトリ、ファイルをハイライトしてみると以下のようになります。
[bash highlight=”2-18, 22, 27, 29, 50-53, 55, 60, 61″]
.
├── 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
[/bash]
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にあります。
以下、ログインページとユーザー登録ページのルーティングが確認できます。
[js title=”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’)
}
}
})

}
[/js]

ブラウザで、/#/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 title=”app/Http/Controllers/Auth/AuthController.php” highlight=”24″]
<?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-&gt;only('email', 'password');

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

    $user = Auth::user();

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


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

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

認証に成功した場合は、ユーザー情報とJWTをレスポンスとして返します。
[text]
{"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"}}
[/text]

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

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

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

Satellizerの設定は、angular/config/satellizer.config.jsにあります。
[js title=”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

}
[/js]

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

ログイン画面で、[Login]ボタンを押すと、コントローラのloginメソッドが呼ばれ、$authでインジェクションされるSatellizerのloginメソッドを実行すると、ログインAPIが呼び出されます。
[js title=”angular/app/components/login-form/login-form.component.js”]
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) =&gt; {
            this.$auth.setToken(response.data);

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

}

[/js]
ログイン成功したら応答を受け取ったら、$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を以下のようにセットします。
[php title=”config/api.php” firstline=”157″]
‘auth’ => [
‘jwt’ => ‘Dingo\Api\Auth\Provider\JWT’,
],
[/php]
JWTの検証は、tymon/jwt-authが内部で利用されます。

APIにJWT認証を設定

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

以下、はデフォルトのルーティングですが、対象のAPIはまだありませんが、37行目に雛形として準備されています。
[php title=”app/Http/routes.php” highlight=”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-&gt;post('auth/login', 'Auth\AuthController@login');
$api-&gt;post('auth/register', 'Auth\AuthController@register');

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

});

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

});
[/php]

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

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

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

Postモデルの作成

--migrationオプションを指定してPostモデルを生成します。
[bash]
$ php artisan make:model Post –migration
Model created successfully.
Created Migration: 2016_08_01_072721_create_posts_table
[/bash]

マイグレーションに、nametopicフィールドを追加します。
[php title=”database/migrations/2016_08_01_072721_create_posts_table.php” highlight=”17-18″]
<?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]
マイグレーションを適用します。
[bash]
$ php artisan migrate
Migrated: 2016_08_01_072721_create_posts_table
[/bash]

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

ここで実装するのはcreateメソッドのみですが、他のCRUD処理も実装することを想定して、--resourceオプションを指定してPostControllerを作成してみます。
[bash]
$ php artisan make:controller PostController –resource
Controller created successfully.
[/bash]
App\Postをインポートし、Laravel Controllerのコードのcreateメソッドの内容をコピペします。
[php title=”app/Http/Controllers/PostController.php” gutter=”false” highlight=”5, 19-29″]
<?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-&gt;name = $request-&gt;input('name');
  $post-&gt;topic = $request-&gt;input('topic');
  $post-&gt;save();

  return response()-&gt;success(compact('post'));
}
...

}
[/php]

追加したAPIを、認証の対象としてルーティングを追加します。
dingo/apiの認証プロバイダを呼び出すミドルウェアapi.authが指定されている箇所に追加します。
[php title=”app/Http/routes.php” firstline=”37″]
$api->group([‘middleware’ => [‘api’, ‘api.auth’]], function ($api) {
$api->resource(‘posts’, ‘PostController’);
});
[/php]

Post作成フォームの実装

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

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

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

また、/create-postを認証済みの場合にのみ表示したい場合、Authenticated routes (front-end)を参考にdata.auth = trueを指定します。
[js title=”angular/config/routes.config.js” firstline=”64″ highlight=”66-68″]
.state(‘app.create_post’, {
url: ‘/create-post’,
data: {
auth: true
},
views: {
‘main@’: {
templateUrl: getView(‘create_post’)
}
}
});
[/js]

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

以下のように、ui-routerのState Change Eventsイベント発生時に、$authでインジェクションされるSatellizerのisAuthenticatedメソッドで認証済みかどうかチェックし、認証していなければログインページを表示しています。
[js title=”angular/run/routes.run.js” highlight=”9″]
export function RoutesRun($rootScope, $state, $auth) {
‘ngInject’;

let deregisterationCallback =  $rootScope.$on(&quot;$stateChangeStart&quot;, function(event, toState) {

    if (toState.data &amp;&amp; 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)

}
[/js]

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

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

補足: ログアウトの実装

Gulp Watchしておきます。
[bash]
$ gulp && gulp watch
[/bash]

artisanコマンドで、ログアウトボタンを生成します。
[bash]
$ artisan ng:component logout-button
Component created successfully.
[/bash]

[js title=”angular/app/components/logout-button/logout-button.component.js”]
class LogoutButtonController{
constructor($auth, ToastService) {
‘ngInject’;

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

$onInit(){
}

logout() {
  this.$auth.logout().then(() =&gt; {
    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: {}
}
[/js]

参考

LARAVEL 5.2 API USING JWT AUTHENTICATION TUTORIAL FROM SCRATCH