PHP
api
laravel
laravel5.5

LaravelでセッションIDで認証状態をチェックするステートフルなAPIを使う

Laravelは標準でAPI用のルーティングなり設定が用意されています。
しかしこれらの設定はトークンを利用して認証状態をチェックするものでありステートレスであることが前提となっています。
サーバサイドでレンダリングして、一部分だけちょろっとAPI経由でデータを取得したいがために、ログインセッションを使って認証済みならデータを取得するAPIを作ろうとしたときにハマったのでメモ。

:smiley: 話すこと

LaravelにおいてセッションIDを利用してサクッとAPI作るやり方。

:expressionless: 話さないこと

ステートレスなAPIがベストなのかステートフルなAPIでもいいのかについて。
またAPIの設計のベストプラクティスについて。

TL;DR

ステートフルなAPIでも要件が許されるなら、ステートフルAPI用の設定を追加すればサクッと実装できます。
ステートフル用のAPIのルーティングファイルroutes/statefulApi.phpの追加、
Providers/RouteServiceProvider.phpで追加したルーティングファイルの読み込み、Http/Kernel.phpに専用のミドルウェアグループの定義、をそれぞれ行います。

環境

名前 バージョン
PHP 7.1.6
Laravel 5.5.14

修正前

以下のようなAPIのルーティング設定があります。

routes/api.php
<?php

use Illuminate\Http\Request;

Route::get('/post/index', 'Api\PostController@index');

コントローラは以下のとおりです。
トークンでの認証といった大仰なことは面倒ですしサクッとAPIを実装したいためcookieに保存したセッションIDで認証済みかチェックしたいので、middlewareはauthを指定しています。

Http/Controllers/Api/PostController.php
<?php

namespace App\Http\Controllers\Api;

use Illuminate\Http\Request;
use App\Post;
use App\Http\Controllers\Controller;

class PostController extends Controller
{
    /**
     * @return void
     */
    public function __construct()
    {
        $this->middleware('auth');
    }

    /**
     * @param  \Illuminate\Http\Request  $request
     * @return array
     */
    public function index(Request $request)
    {
        return Post::all();
    }
}

Kernel.phpは以下のとおりです。デフォルトのままです。

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.
     *
     * These middleware are run during every request to your application.
     *
     * @var array
     */
    protected $middleware = [
        \Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
        \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
        \App\Http\Middleware\TrimStrings::class,
        \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
    ];

    /**
     * The application's route middleware groups.
     *
     * @var array
     */
    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',
        ],
    ];

    /**
     * The application's route middleware.
     *
     * These middleware may be assigned to groups or used individually.
     *
     * @var array
     */
    protected $routeMiddleware = [
        'auth' => \Illuminate\Auth\Middleware\Authenticate::class,
        'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
        'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
        'can' => \Illuminate\Auth\Middleware\Authorize::class,
        'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
        'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
    ];
}

cookieに認証済みのセッションIDが保存されている状態で上記のAPIを実行すると401エラーになってしまいます。
それはなぜかというとwebのミドルウェアに\App\Http\Middleware\EncryptCookies::classがあるのですが、これはcookieの暗号化と復号を担っており、ブラウザのcookieに保存されているセッションIDはwebのミドルウェアで暗号化されたセッションIDで、ブラウザからAPIが実行されるときは暗号化された値がリクエストに付与されますが、apiのミドルウェアグループに\App\Http\Middleware\EncryptCookies::classがないため、セッションIDを復号できないために認証のチェックではじかれてしまって401エラーになります。
また、復号だけでなく\Illuminate\Session\Middleware\StartSession::classも必要です。

かといってapiのミドルウェアグループにcookieやsessionのミドルウェアを追加してしまうと、トークンを前提としたステートレスなAPIに余分なものがついてしまいます。
そのため既存のapiを汚さないために新しくステートフルなAPI用の設定を追加します。

ステートフルなAPI用の設定の追加

専用のルーティング設定ファイルを追加します。

routes/statefulApi.php
<?php

use Illuminate\Http\Request;

Route::get('/post/index', 'Api\PostController@index');

上記のルーティング設定ファイルを読み込むようにします。

Providers/RouteServiceProvider.php
<?php

namespace App\Providers;

use Illuminate\Support\Facades\Route;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;

class RouteServiceProvider extends ServiceProvider
{
    /**
     * 省略
     */

    /**
     * Define the routes for the application.
     *
     * @return void
     */
    public function map()
    {
        $this->mapApiRoutes();

        $this->mapWebRoutes();

        $this->mapStatefulApiRoutes();    // ★追加
    }

    /**
     * 省略
     */

// ★↓↓↓↓↓追加
    /**
     * Define the "statefulApi" routes for the application.
     *
     * These routes are typically stateless.
     *
     * @return void
     */
    protected function mapStatefulApiRoutes()
    {
        Route::prefix('api')
             ->middleware('stateful_api')
             ->namespace($this->namespace)
             ->group(base_path('routes/statefulApi.php'));
    }
// ★↑↑↑↑↑追加
}

Kernel.phpにステートフルAPI用の設定を追加します。
apiミドルウェアのものを基本としながらセッションIDで認証させるため
cookie関連のミドルウェアとStartSessionのミドルウェアを追加します。

Http/Kernel.php
    /**
     * The application's route middleware groups.
     *
     * @var array
     */
    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',
        ],

// ★↓↓↓↓↓追加
        'stateful_api' => [
            \App\Http\Middleware\EncryptCookies::class,
            \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
            \Illuminate\Session\Middleware\StartSession::class,
            \Illuminate\View\Middleware\ShareErrorsFromSession::class,
            'throttle:60,1',
            'bindings',
        ],
// ★↑↑↑↑↑追加
    ];

このようにすることによってセッションIDで認証済みかどうかをチェックするようになります。
アクセストークンを発行・管理するのは面倒ですしそこまで必要ではない場合、ステートフルなAPIを使ってみるのもいいかと思います。