====== Apricot ミドルウェア ====== --- //[[http://www.y2sunlight.com|y2sunlight]] 2020-07-29// [[apricot:usage:ja|Apricot ドキュメント に戻る]] 目次 * [[apricot:usage:ja:features|Apricot 特徴と概要]] * [[apricot:usage:ja:config|Apricot 配置と構成]] * [[apricot:usage:ja:errors-logging|Apricot ログとエラー処理]] * [[apricot:usage:ja:http|Apricot リクエストとレスポンス]] * [[apricot:usage:ja:frontend|Apricot フロントエンド]] * [[apricot:usage:ja:database|Apricot データベース]] * [[apricot:usage:ja:model|Apricot モデルとサービス]] * Apricot ミドルウェア * [[apricot:usage:ja:controller|Apricot コントローラ]] * [[apricot:usage:ja:validation|Apricot バリデーション]] * [[apricot:usage:ja:provider|Apricot サービスプロバイダー]] * [[apricot:usage:ja:authentication|Apricot ユーザ認証]] * [[apricot:usage:ja:utility|Apricot ユーティリティ]] ---- ===== ミドルウェアの構造 ===== ミドルウェアとはアクションを囲んでいる層のような存在で、利用者からのリクエストは何層もあるミドルウェアを通って最終的にアクションにたどり着きそこでレスポンスが生成されますが、途中でリクエストが中断され、ミドルウェアがレスポンスを生成することもあります。また、ミドルウェアとは途中でリクエストやレスポンスに介入してデータをモニタリング、フィルタリングまたは変換したりすることもできます。 === ミドルウェアパイプライン === {{:apricot:usage:ja:middleware:ext-fig01.svg?nolink&800}} 上図のような処理のネスト構造を ''パイプライン'' (pipeline) と呼び、特に多層になったミドルウェア構造を ''ミドルウェアパイプライン'' と呼ぶ事にします。 ミドルウェアパイプラインを含めたミドルウェアの仕組みはApricotのコアが提供しますが、具体的なミドルウェアの実装はアプリ側に任されています。 \\ ===== Middleware インターフェース ===== 全てのミドルウェアは以下の ''Middleware'' インタフェースを実装して作ります。 前項のミドルウェア構造の図から分かるように、ミドルウェアのインターフェースの役割は自分を処理の後に次のプロセッサーに制御を渡すことです。この為に、ミドルウェアパイプラインは、''process()'' のパラメータとして ''Invoker'' インターフェースを渡します。 ''Invoker'' の ''invoke()'' メソッドを呼び出すことによって次のプロセッサーに制御を渡します。ミドルウェアは任意タイミングで ''invoke()'' を 使うことができるので、前処理、後処理またはその両方ができます。また、クライアントの要求を自分だけで消費して Invoker を使うことなく自分のレスポンスを返すことも可能です。 \\ ===== Middlewareの実装 ===== Apricotのスケルトンで提供されているミドルウェアは、以下場所にの配置してあります。この配置は必須ではありません。適宜アプリケーションのルールで変更して下さい。 /your-project/app/Middleware; 以下は、ミドルウェアの典型的な実装例です。 {{fa>folder-open-o}} ** /your-project/app/Middleware ** invoke(); // Post-processing // ... return response } } === Middlewareの設定 === 新しく作成したミドルウェアは、アプリケーションの設定ファイル ''/your-project/config/app.php'' に登録します。 {{fa>folder-open-o}} ** /your-project/config ** [ \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のスケルトンでは以下のミドルウェアが初期実装されています。 * アクセスログ * フォーム入力変換 * CSRF対策 * ユーザ認証(基本認証 及び セッション認証) ここでは、ユーザ認証以外のミドルウェアについて説明します。ユーザ認証については後続の章をご覧ください。これらのミドルウェアを適用したくない場合は、アプリケーションの設定ファイル( config/app.php )を修正して下さい。 \\ ==== アクセスログ ==== アクセスログのミドルウェアは、リクエストをモニタして次のプロセスを発動するだけの簡単な構造をしています。アクセスログをカスタマイズするには、このミドルウェアを修正して下さい。 {{fa>folder-open-o}} ** /your-project/app/Middleware ** /** * 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'' シングルトンを使用しています。このシングルトンについてはユーザ認証を参照して下さい。 \\ ==== フォーム入力変換 ==== このミドルウェアの目的は、フォームの入力変数を変換することです。 * 入力値をトリミングします。 * 入力値が空の場合、その値をnullにします。 これらの変換を望まない変数については、そのキーを ''$exclude'' に登録して下さい。入力変数の変換を追加または変更するには、このミドルウェアを修正して下さい。 {{fa>folder-open-o}} ** /your-project/app/Middleware ** /** * 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'' をスローして[[apricot:usage:ja:errors-logging#集約例外ハンドラー]]に処理を委ねます。 CSR対策を望まないコントローラーについては、そのクラス名を ''$exclude'' に登録して下さい。 {{fa>folder-open-o}} ** /your-project/app/Middleware ** /** * 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トークンを入力変数として埋め込む方法については、「[[apricot:usage:ja:frontend#テンプレートの継承|フロントエンドのテンプレートの継承]]」のCSRF対策の項を参照して下さい。 アプリケーション設定( [[apricot:usage:ja:config#appphp_ファイル|config/app.php]] )の ''csrftrue.disposable'' が true の場合、CSRFトークンは使い捨てで、''verify()'' の後で削除され次の ''generate()'' で再生成されます。これが false の場合は、セッション中の間CSRFトークンの値は不変です。 \\