目次

Apricot ミドルウェア

y2sunlight 2020-07-29

Apricot ドキュメント に戻る

目次


ミドルウェアの構造

ミドルウェアとはアクションを囲んでいる層のような存在で、利用者からのリクエストは何層もあるミドルウェアを通って最終的にアクションにたどり着きそこでレスポンスが生成されますが、途中でリクエストが中断され、ミドルウェアがレスポンスを生成することもあります。また、ミドルウェアとは途中でリクエストやレスポンスに介入してデータをモニタリング、フィルタリングまたは変換したりすることもできます。

ミドルウェアパイプライン

上図のような処理のネスト構造を パイプライン (pipeline) と呼び、特に多層になったミドルウェア構造を ミドルウェアパイプライン と呼ぶ事にします。

ミドルウェアパイプラインを含めたミドルウェアの仕組みはApricotのコアが提供しますが、具体的なミドルウェアの実装はアプリ側に任されています。


Middleware インターフェース

全てのミドルウェアは以下の Middleware インタフェースを実装して作ります。

Middleware.php
<?php
namespace Apricot\Foundation\Middleware;
 
use Apricot\Foundation\Invoker;
use Apricot\Foundation\Response;
 
/**
 * Middleware Interface
 */
interface Middleware
{
    /**
     * Processes an incoming request and produces a response.
     *
     * @param Invoker $next Next invoker.
     * @return \Apricot\Foundation\Response if return a response within this method, then don't call the next action.
     */
    public function process(Invoker $next) :Response;
}

前項のミドルウェア構造の図から分かるように、ミドルウェアのインターフェースの役割は自分を処理の後に次のプロセッサーに制御を渡すことです。この為に、ミドルウェアパイプラインは、process() のパラメータとして Invoker インターフェースを渡します。

Invoker.php
<?php
namespace Apricot\Foundation;
 
/**
 * Invoker Interface
 */
interface Invoker
{
    /**
     * Invokes an incoming request processor
     *
     * @return \Apricot\Foundation\Response
     */
    public function invoke() : Response;
}

Invokerinvoke() メソッドを呼び出すことによって次のプロセッサーに制御を渡します。ミドルウェアは任意タイミングで invoke() を 使うことができるので、前処理、後処理またはその両方ができます。また、クライアントの要求を自分だけで消費して Invoker を使うことなく自分のレスポンスを返すことも可能です。


Middlewareの実装

Apricotのスケルトンで提供されているミドルウェアは、以下場所にの配置してあります。この配置は必須ではありません。適宜アプリケーションのルールで変更して下さい。

/your-project/app/Middleware;

以下は、ミドルウェアの典型的な実装例です。

/your-project/app/Middleware

Invoker.php
<?php
namespace App\Middleware;
 
use Apricot\Foundation\Response;
use Apricot\Foundation\Invoker;
use Apricot\Foundation\Middleware\Middleware;
 
/**
 * Middleware
 */
class MyMiddleware implements Middleware
{
    /**
     * {@inheritDoc}
     * @see \Apricot\Foundation\Middleware\Middleware::invoke()
     */
    public function process(Invoker $next): Response
    {
        // Pre-processing
        // ...
 
        // Calls the next Invoker.
        $response = $next->invoke();
 
        // Post-processing
        // ...
 
        return response 
    }
}

Middlewareの設定

新しく作成したミドルウェアは、アプリケーションの設定ファイル /your-project/config/app.php に登録します。

/your-project/config

app.php
<?php
/**
 * This file contains application settings.
 */
return
[
    'middleware' =>[
        \App\Middleware\AccessLog::class,        /* Access log */
        \App\Middleware\VerifyCsrfToken::class,  /* Verify CSRF Token */
        \App\Middleware\Auth\SessionAuth::class, /* Session authentication */
        \App\Middleware\InputConverter::class,   /* Input Converter */
        \App\Middleware\MyMiddleware ::class,    /* New MyMiddleware */
    ],
];

これらのミドルウェアは、パイプラインによって登録した順に発動されます。


ミドルウエアの実例

Apricotのスケルトンでは以下のミドルウェアが初期実装されています。

ここでは、ユーザ認証以外のミドルウェアについて説明します。ユーザ認証については後続の章をご覧ください。これらのミドルウェアを適用したくない場合は、アプリケーションの設定ファイル( config/app.php )を修正して下さい。


アクセスログ

アクセスログのミドルウェアは、リクエストをモニタして次のプロセスを発動するだけの簡単な構造をしています。アクセスログをカスタマイズするには、このミドルウェアを修正して下さい。

/your-project/app/Middleware

AccessLog.php
/**
 * Access Log - Middleware
 */
class AccessLog implements Middleware
{
    /**
     * {@inheritDoc}
     * @see \Apricot\Foundation\Middleware\Middleware::invoke()
     */
    public function process(Invoker $next): Response
    {
        // Logs a message.
        $message = session_id().' '.$_SERVER['REQUEST_METHOD'].' '.$_SERVER['REQUEST_URI'];
 
        // Logs context data.
        $data = [
            'remote_addr' => $_SERVER['REMOTE_ADDR'],
            'remote_user' => AuthUser::check() ? AuthUser::getUser()->account : null,
            'user_agent' =>  $_SERVER['HTTP_USER_AGENT'],
            'input' => json_encode(Input::all()),
        ];
        Log::info($message,$data);
 
        // Calls the next Invoker.
        return $next->invoke();
    }
}

収集したログデータはinfoレベルで出力します。ログインユーザのアカウントを取得する為に、AuthUser シングルトンを使用しています。このシングルトンについてはユーザ認証を参照して下さい。


フォーム入力変換

このミドルウェアの目的は、フォームの入力変数を変換することです。

これらの変換を望まない変数については、そのキーを $exclude に登録して下さい。入力変数の変換を追加または変更するには、このミドルウェアを修正して下さい。

/your-project/app/Middleware

InputConverter.php
/**
 * Input Converter - Middleware
 */
class InputConverter implements Middleware
{
    /**
     * @var array List of input variables to exclude
     */
    private $exclude = [
        'password',
        'password_confirmation',
    ];
 
    /**
     * {@inheritDoc}
     * @see \Apricot\Foundation\Middleware\Middleware::invoke()
     */
    public function process(Invoker $next): Response
    {
        $inputs = Input::all();
        foreach($inputs as $key=>$value)
        {
            if (in_array($key, $this->exclude)) continue;
            if (is_string($value))
            {
                $value = trim($value); // Trims a string value.
                if($value === '') $value = null; // Converts an empty string value to null.
                Input::set($key, $value);
            }
        }
        return $next->invoke();
    }
}

CSRF対策

このミドルウェアの目的は、CSRF対策です。発行しているCSRFトークンの検証に失敗した場合は、TokenMismatchException をスローして集約例外ハンドラーに処理を委ねます。

CSR対策を望まないコントローラーについては、そのクラス名を $exclude に登録して下さい。

/your-project/app/Middleware

VerifyCsrfToken.php
/**
 * CSRF token verification - Middleware
 */
class VerifyCsrfToken implements Middleware
{
    /**
     * @var array List of controllers to exclude
     */
    private $exclude = [
        'HogeHogeController', // For example: Web API controller etc.
    ];
 
    /**
     * {@inheritDoc}
     * @see \Apricot\Foundation\Middleware\Middleware::invoke()
     */
    public function process(Invoker $next): Response
    {
        if (!in_array(controllerName(), $this->exclude))
        {
            // Verifies CSRF tokens.
            if (!CsrfToken::verify())
            {
                throw new \Apricot\Exceptions\TokenMismatchException('VerifyCsrfToken Error');
            }
        }
 
        // Generates a CSRF token.
        CsrfToken::generate();
 
        return $next->invoke();
    }
}

CsrfToken はApricotコアのクラスで次の静的メソッドを持ちます。

メソッド機能
generate()セッション内のCSRFトークンが未生成の場合、それを生成してセッションに格納します。
verify():boolフォームの入力変数とセッションに格納されているCSRFトークンを比較して同じならtrueを返します。これはHTTPメソッドがPOSTの場合のみ有効で、GETの場合は常にtrueを返します。

フォーム内にCSRFトークンを入力変数として埋め込む方法については、「フロントエンドのテンプレートの継承」のCSRF対策の項を参照して下さい。

アプリケーション設定( config/app.php )の csrftrue.disposable が true の場合、CSRFトークンは使い捨てで、verify() の後で削除され次の generate() で再生成されます。これが false の場合は、セッション中の間CSRFトークンの値は不変です。