2019.01.12

Vue + Vue Router + Vuex + Laravelで写真共有アプリを作ろう (4) 認証API


UPDATED:2020.01.05
PHP 7.4 および Laravel 6 に対応しました 🎉

この連載記事では、フロントエンドに Vue.js + Vue Router + Vuex とサーバーサイドに Laravel を使用したシングルページ Web アプリケーションの開発方法を紹介します。実際に写真共有アプリを開発する手順を通して SPA 開発のエッセンスを学ぶことができます。

今回のチュートリアルで扱うツールなどのバージョンは以下の通りです。

Node npm Vue.js Vue Router Vuex PHP Laravel
10.15 6.4 2.6 3.1 3.1 7.4 6.9

この章から認証機能を実装していきます。

まず本章ではサーバサイドの API を実装します。
基本的には Laravel にデフォルトで備えられている認証機能をそのまま使います。

API 用のルート

API と画面のルート定義は別々のファイルに記述した方が分かりやすいので、API のルート定義は前回画面を返却するルートを定義した routes/web.php ではなく api.php に記載しましょう。そのためには少しデフォルトの定義を変更する必要があります。

app/Providers/RouteServiceProvider.php を以下の通りに編集してください。

RouteServiceProvider.php
protected function mapApiRoutes()
{
    Route::prefix('api')
         ->middleware('web') // ★ 'api' → 'web' に変更
         ->namespace($this->namespace)
         ->group(base_path('routes/api.php'));
}

RouteServiceProvider はアプリケーションの起動時にルート定義を読み込むためのクラスです。routes/api.php に記述したルート定義に適用されるミドルウェアグループは api なのですが、それを web に変更します。

ちなみにミドルウェアグループの定義は app/Http/Kernel.php に記述されています。
(こちらのファイルは参考で載せているので編集の必要はありません。)

Kernel.php
protected $middlewareGroups = [
    'web' => [
        \App\Http\Middleware\EncryptCookies::class,
        \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
        \Illuminate\Session\Middleware\StartSession::class,
        // \Illuminate\Session\Middleware\AuthenticateSession::class,
        \Illuminate\View\Middleware\ShareErrorsFromSession::class,
        \App\Http\Middleware\VerifyCsrfToken::class,
        \Illuminate\Routing\Middleware\SubstituteBindings::class,
    ],

    'api' => [
        'throttle:60,1',
        'bindings',
    ],
];

web とくらべて api はずいぶんすっきりしていますね。api ミドルウェアグループでは本来、外部のアプリケーションから呼び出されるようなステートレスな Web API(Twitter API や Google Map API などをイメージしてください)が想定されているので、セッションやクッキー、CSRF トークンを扱うミドルウェアが含まれていません。

今回実装する API は内部からしか呼ばれない上にクッキー認証を行うステートフルなものなので、ミドルウェアグループは画面と同じ web に設定します。


ではここから「会員登録」「ログイン」「ログアウト」の3本の API を実装します。
といっても認証機能に関してはデフォルト実装のカスタマイズで OK です。

テストの準備

実装してすぐにプログラムの動作を確認できるように、テストコードを書きながら開発しましょう。テストコードがないとフロントエンドを実装して API を呼び出すコードを作り終わるまでプログラムの正しさが分かりませんが、それでは不便ですよね。

ここではあくまで API の動作を確かめるためにテストコードを用います。そのため失敗するテストケースや入力値バリデーションを一つずつ確かめるテストケースなどは記述しません。必要だと思う方は各自でケースを追加していただければと思います。

では、テストコード作成前にいくつか準備を行います。

インメモリの SQLite を用いる

テスト実行時に開発用の PostgreSQL データベースとは別の SQLite データベースが用いるための設定を行います。インメモリの SQLite を利用するのでテスト実行が終わると消去され無駄なデータが残りません。

まず config/database.phpconnections に以下の接続情報を追加しましょう。

database.php
'sqlite_testing' => [
    'driver' => 'sqlite',
    'database' => ':memory:',
    'prefix' => '',
],

そして phpunit.xml に DB 接続の設定を追記します。

phpunit.xml
<php>
    <env name="APP_ENV" value="testing"/>
    <env name="DB_CONNECTION" value="sqlite_testing"/> <!-- ★ 追加 -->
    <!-- 以下略 -->

会員登録 API

I/O 設計

簡単に Input(リクエストデータ)と Output(レスポンス)を設計します。

リクエスト

デフォルトの通り name email password password_confirmation を受け取ります。

レスポンス

デフォルトの挙動では登録成功後には定義されたページにリダイレクトするレスポンスを返しますが、今回は SPA なのでいつどのページに遷移するかはフロントエンドでコントロールします。

そのため、登録成功後は登録ユーザーの情報を返却させることにします。そうすれば返却されたユーザーデータをフロントエンドで保存しておいて認証状態のチェックなどに使えます。

テストコード

artisan コマンドでテストコードを作成します。

$ php artisan make:test RegisterApiTest

雛形 tests/Feature/RegisterApiTest.php が作成されるので以下の内容で編集してください。

RegisterApiTest.php
<?php

namespace Tests\Feature;

use App\User;
use Tests\TestCase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Foundation\Testing\RefreshDatabase;

class RegisterApiTest extends TestCase
{
    use RefreshDatabase;

    /**
     * @test
     */
    public function should_新しいユーザーを作成して返却する()
    {
        $data = [
            'name' => 'vuesplash user',
            'email' => 'dummy@email.com',
            'password' => 'test1234',
            'password_confirmation' => 'test1234',
        ];

        $response = $this->json('POST', route('register'), $data);

        $user = User::first();
        $this->assertEquals($data['name'], $user->name);

        $response
            ->assertStatus(201)
            ->assertJson(['name' => $user->name]);
    }
}

assertStatusassertJson は Laravel によって追加されたアサーションです。
詳しくはマニュアル(🇺🇸 公式 / 🇯🇵 日本語)を参照してください。

これ以降も、API を作成するときは事前にテストコードを書いてから実装を行います。ただしあまり詳しい説明は行いませんので、テストコードの部分が難しいと感じた方は、とりあえずテストは省略して読み進んでもアプリケーションは完成します。個人的には、テストで挫折するくらいならまずはテストの箇所は飛ばしてでも最後までクリアした方が学びになると思います。無理せず自分のレベルに合わせて判断してください。

ルート定義

routes/api.php にルート定義を記述します。

api.php
<?php

use Illuminate\Http\Request;

// 会員登録
Route::post('/register', 'Auth\RegisterController@register')->name('register');

コントローラー

app/Http/Controllers/Auth/RegisterController.php を以下の通り編集します。

RegisterController.php
use Illuminate\Http\Request; // ★ 追加

class RegisterController extends Controller
{
    /* 中略 */

    // ★ メソッド追加
    protected function registered(Request $request, $user)
    {
        return $user;
    }
}

なぜこのメソッドを追加するかを把握するために、register メソッド が定義されている Illuminate\Foundation\Auth\RegistersUsers トレイトを見てみましょう(RegistersUsers トレイトは RegisterController で追加されています)。

RegistersUsers.php
public function register(Request $request)
{
    /* 中略 */

    return $this->registered($request, $user)
                    ?: redirect($this->redirectPath());
}

protected function registered(Request $request, $user)
{
    //
}

register メソッドの最後の return 文で registered メソッドが呼ばれ、その戻り値が偽値だった場合には redirect 関数が呼ばれる仕組みになっています。そして registered メソッド自体は中身が実装されていません。つまりデフォルトでは redirect 関数が呼ばれるようになっていて、このレスポンスをカスタマイズしたい場合はトレイトを使用している RegisterControllerregistered メソッドの中身を実装して上書きしてやればよいわけです。

テスト実施

ここまでできたらテストを実行して API が期待通りに動作していることを確かめます。

$ ./vendor/bin/phpunit --testdox

ログイン API

I/O 設計

リクエスト

デフォルトの通り email password を受け取ります。

レスポンス

会員登録同様、デフォルトの挙動では認証成功後には定義されたページにリダイレクトするレスポンスを返しますが、今回は登録ユーザーの情報を返却させることにします。

テストコード

artisan コマンドでテストコードを作成します。

$ php artisan make:test LoginApiTest

雛形 tests/Feature/LoginApiTest.php が作成されるので以下の内容で編集してください。

LoginApiTest.php
<?php

namespace Tests\Feature;

use App\User;
use Tests\TestCase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Foundation\Testing\RefreshDatabase;

class LoginApiTest extends TestCase
{
    use RefreshDatabase;

    public function setUp(): void
    {
        parent::setUp();

        // テストユーザー作成
        $this->user = factory(User::class)->create();
    }

    /**
     * @test
     */
    public function should_登録済みのユーザーを認証して返却する()
    {
        $response = $this->json('POST', route('login'), [
            'email' => $this->user->email,
            'password' => 'password',
        ]);

        $response
            ->assertStatus(200)
            ->assertJson(['name' => $this->user->name]);

        $this->assertAuthenticatedAs($this->user);
    }
}

ルート定義

routes/api.php にルート定義を記述します。

api.php
// ログイン
Route::post('/login', 'Auth\LoginController@login')->name('login');

コントローラー

app/Http/Controllers/Auth/LoginController.php を以下の通り編集します。

LoginController.php
use Illuminate\Http\Request; // ★ 追加

class LoginController extends Controller
{
    /* 中略 */

    // ★ メソッド追加
    protected function authenticated(Request $request, $user)
    {
        return $user;
    }
}

authenticated メソッドを追加する理由も会員登録のときと同じパターンです。LoginController が使用している Illuminate\Foundation\Auth\AuthenticatesUsers トレイトで login メソッドの実装を見てみましょう。

AuthenticatesUsers.php
public function login(Request $request)
{
    /* 中略 */

    if ($this->attemptLogin($request)) {
        return $this->sendLoginResponse($request);
    }

    /* 中略 */
}

protected function sendLoginResponse(Request $request)
{
    /* 中略 */

    return $this->authenticated($request, $this->guard()->user())
            ?: redirect()->intended($this->redirectPath());
}

protected function authenticated(Request $request, $user)
{
    //
}

login メソッドでログイン成功時に sendLoginResponse メソッドが呼ばれ、sendLoginResponse メソッドの return 文でレスポンスが決まっています。return 文では実装が空の authenticated メソッドが呼ばれて、戻り値が偽であった場合にリダイレクトレスポンスが返されています。

つまり authenticated メソッドをオーバーライドすればレスポンスをカスタマイズできますね。

テスト実施

ここまでできたらテストを実行して API が期待通りに動作していることを確かめます。

$ ./vendor/bin/phpunit --testdox

ログアウト API

I/O 設計

リクエスト

ログアウトもリクエストデータはデフォルトの通りです。
つまり何も受け取りません。

レスポンス

会員登録とログイン同様、デフォルトの挙動では認証成功後には定義されたページにリダイレクトするレスポンスを返します。今回はログアウト処理が成功したことだけが分かればよいのでステータスコード 200 番だけを返却させることにします。

テストコード

artisan コマンドでテストコードを作成します。

$ php artisan make:test LogoutApiTest

雛形 tests/Feature/LogoutApiTest.php が作成されるので以下の内容で編集してください。

LogoutApiTest.php
<?php

namespace Tests\Feature;

use App\User;
use Tests\TestCase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Foundation\Testing\RefreshDatabase;

class LogoutApiTest extends TestCase
{
    use RefreshDatabase;

    public function setUp(): void
    {
        parent::setUp();

        // テストユーザー作成
        $this->user = factory(User::class)->create();
    }

    /**
     * @test
     */
    public function should_認証済みのユーザーをログアウトさせる()
    {
        $response = $this->actingAs($this->user)
                         ->json('POST', route('logout'));

        $response->assertStatus(200);
        $this->assertGuest();
    }
}

ルート定義

routes/api.php にルート定義を記述します。

api.php
// ログアウト
Route::post('/logout', 'Auth\LoginController@logout')->name('logout');

コントローラー

LoginControllerloggedOut メソッドを追加します。

LoginController.php
protected function loggedOut(Request $request)
{
    // セッションを再生成する
    $request->session()->regenerate();

    return response()->json();
}

loggedOut メソッドを追加する理由も会員登録・ログインと同じパターンです。ログインと同様に AuthenticatesUsers トレイトの logout メソッドを見てみましょう。

AuthenticatesUsers.php
public function logout(Request $request)
{
    /* 中略 */

    return $this->loggedOut($request) ?: redirect('/');
}

protected function loggedOut(Request $request)
{
    //
}

同じパターンの繰り返しなのでもう説明は不要ですね。
loggedOut メソッドをオーバーライドすることでレスポンスをカスタマイズできます。

Laravel では認証機能が最初から備わっている上で、デフォルト実装のカスタマイズもしやすいように作られています。

テスト実施

ここまでできたらテストを実行して API が期待通りに動作していることを確かめます。

$ ./vendor/bin/phpunit --testdox

認証機能に必要な3本の API が完成しました。

今回はテストコードを使用して API の動作確認を行いましたが、Postman などのソフトウェアを使っても確認できます。興味がある方はチェックしてみてください。

👾 👾 👾

この章はこれでおしまいです。
本章までのソースコードはリポジトリの ch-4 ブランチに置いてあります。

次の章ではログイン画面のユーザーインターフェースを作成します。

関連記事

連載記事(全16回)

Vue + Vue Router + Vuex + Laravelで写真共有アプリを作ろう

その他