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
に記述されています。
システム構成
含まれるパッケージを踏まえて、システム構成を図にしてみるとおおむね以下のようになります。
認証の際は、フロントエンドでは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
以上で、ブラウザにアクセスすると以下のようなデフォルトページが表示されます。
個々では、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
にアクセスすると以下のユーザー登録ページが表示されます
バックエンドのログイン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->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){ ... }
}
[/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を検証してみた結果になります。
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) => {
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
の値を
Firefoxのローカルストレージインスペクタで保存されたJWTを確認すると、satellizer_tokenにJWTが保存されていることを確認できます。
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->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) {
});
[/php]
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
マイグレーションに、name
とtopic
フィールドを追加します。
[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]
マイグレーションを適用します。
$ 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 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->name = $request->input('name');
$post->topic = $request->input('topic');
$post->save();
return response()->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("$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)
}
[/js]
ログイン後、/#/create-postを開き、適当に入力して[CREATE POST]をクリックするとAPIが呼ばれ、成功すると以下のように「Post added successfully」とToastが表示されます。
また、ローカルストレージから、satellizer_tokenを削除してPostの作成をためしてみると401が返ってくることが確認できます。
補足: ログアウトの実装
Gulp Watchしておきます。
$ gulp && gulp watch
artisanコマンドで、ログアウトボタンを生成します。
$ artisan ng:component logout-button
Component created successfully.
[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(() => {
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