LaravelとAxiosにおけるより良いCSRFリフレッシュ


この記事は使用の文脈で書かれているInertia.js , しかし、概念とコードのスニペットは、あなたがaxios あなたのLaravelプロジェクトのパッケージ.
私は大ファンですInertia.js Laravelを使用したアプリケーションのビルド時.あなたがそれに慣れていないならば、彼らのサイトに行って、それを読んでください.
一般的な問題は、スパのようなアプリケーションを行うときに、慣性を使用するように、あなたはCSRFの不整合の例外に実行されますhere ). 慣性mentions a way ユーザーがフレンドリーな方法で意識するようにするには、まだ私はまだ良いユーザー体験として不足していることがわかります.
ユーザーは、CSRFトークンの概念を持っていないか、またはアプリケーションでいくつかの要求をするために必要です.彼らがアイドル状態になった後に彼らのセッションに戻るならば、トークンは期限切れになります.彼らが何かをして、保存しようとしているとき、「ページが期限切れになった」と言うことはイライラするでしょう、そして、彼らが問題を解決するためにページを更新する必要があるということは非常に直感的でありません.
私は、新しいトークンを作成するために単純なaxiosインターセプターと終点を加えることによって、それがユーザーのために継ぎ目がなくて、まだそのセキュリティ目的に役立つことができるとわかりました.

エンドポイントの追加
まず、新しいトークンを得るためのエンドポイントを追加しましょう.
php artisan make:controller RefreshCsrfTokenController --invokable
このコマンドはinvokable (single-action) controller .
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class RefreshCsrfTokenController extends Controller
{
    /**
     * Handle the incoming request.
     *
     * @param  \Illuminate\Http\Request $request
     * @return \Illuminate\Http\Response
     */
    public function __invoke(Request $request)
    {
        //
    }
}
我々は新鮮なトークンを生成するために必要な機能は1行だけです!
$request->session()->regenerateToken();
これは、新しいトークンを作成し、私たちのセッションでそれを格納します.新しいトークンを再生した後に、JSON応答を返すことができます.完了したコントローラ関数は以下のようになります.
public function __invoke(Request $request)
{
    $request->session()->regenerateToken();

    return response()->json();
}
最後に、エンドポイントをroutes/web.php .
Route::get('/csrf-token', \App\Http\Controllers\RefreshCsrfTokenController::class);
バックエンドが準備ができたので、フロントエンドを扱いましょう.

axiosインターセプターの追加
慣性は、要求をするために使用する人気のライブラリであるinterceptors これにより、レスポンスを引き継ぐ前にレスポンスを「即座に」インターセプトすることができます.
私は、この行動を組織化するのが好きですplugins ディレクトリの私のVUEアプリケーションで、メインでそれを含めるapp.js ファイル.ファイルを作成しましょうresources/js/plugins 呼ばれるhttps.js .
我々は、インポートする必要がありますaxios ライブラリとインターセプターを設定します.
// resources/js/plugins/http.js
import axios from 'axios'

axios.interceptors.response.use()

// Add this line in resources/js/app.js
import './plugins/http'
The axios.interceptors.response.use 関数は2つの引き数を受け入れる2xx ステータスハンドラ)レスポンスハンドラとエラー( non -2xx ステータスコード)ハンドラ.成功した反応のために、我々は何もする必要はありません.
axios.interceptors.response.use(response => response)
これは応答を正常に返します.
エラーハンドラについては、419 , これはlaravelがIlluminate\Session\TokenMismatchException . 私は使用するつもりですlodash/get ( lodash 我々が望むすべての特性を持っていないかもしれないオブジェクトから変数を得るきれいな方法のために、デフォルトLaravelプロジェクトとともに来ます.
import axios from 'axios'
import get from 'lodash/get'

axios.interceptors.response.use(response => response, err => {
  const status = get(err, 'response.status')

  if (status === 419) {
    // Do something
  }

  return Promise.reject(err)
})
ステータスが419でないならば、我々は通常通り約束を拒絶します.
エラー応答で、Axiosは初期リクエストを作成するために使用された設定を与えます.基本的には、この点で2つのことをする必要があります.
  • 新しいトークンを/csrf-token 我々が始めにした終点.
  • 再生トークンを持つ元のリクエストを再試行します.
  • コールバックを作るつもりですasync それで、我々は我々を呼ぶことができます/csrf-token エンドポイントawait . 完全な実装です.
    import axios from 'axios'
    import get from 'lodash/get'
    
    axios.interceptors.response.use(response => response, async err => {
      const status = get(err, 'response.status')
    
      if (status === 419) {
        // Refresh our session token
        await axios.get('/csrf-token')
    
        // Return a new request using the original request's configuration
        return axios(err.response.config)
      }
    
      return Promise.reject(err)
    })
    
    この時点で、実際には、エンドユーザーのシームレスな体験をするために、2行のコードを追加する必要がありました.

    テスト
    シンプルを作ろうpost エンドポイント閉鎖機能を使用してください.インroutes/web.php
    Route::post('/test', fn () => response()->json(['status' => 'ok']));
    
    今、我々は少し混乱を加える必要がありますapp/Http/Middleware/VerifyCsrfToken.php 期限切れのトークンをシミュレートするミドルウェアデフォルトでは、このミドルウェアはIlluminate\Foundation\Http\Middleware\VerifyCsrfToken 既に機能を含むクラスhandle 関数.チェックする前にトークンを再生することで少し騒乱を起こすことがあります.
    public function handle($request, Closure $next)
    {
        if (random_int(0, 1)) {
            $request->session()->regenerateToken();
        }
    
        return parent::handle($request, $next);
    }
    
    The random_int(0, 1) 乱数を生成する0 or 1 , そしてもし1 それから、それは我々のセッションで新しいトークンを生成します.以下に、
  • ミドルウェアが処理されると、リクエストで渡されたトークンが送信したトークンに一致するかどうかをチェックします.
  • 我々はランダムにトークンを再生成する追加されたスニペット.それが再生されるならば、トークンは一致しません.
  • 彼らがマッチしないとき、それはTokenMismatchException , ナカ419 ステータスコード.
  • 私たちのインターセプターは/csrf-token , そしてそれは再びトークンを再生成します.
  • 最終的には、ミドルウェアはトークンを変更せず、要求は正しく解決されます.
  • VUE 3を慣性で使っているので、このリクエストをするためのコンポーネントを作ります.
    <template>
      <button type="button" @click.prevent="sendTest">Test</button>
    </template>
    
    <script>
    import { defineComponent } from 'vue'
    import axios from 'axios'
    
    export default defineComponent({
      setup () {
        const sendTest = async () => {
          const { data } = await axios.post('/test')
          console.log(data)
        }
    
        return {
          sendTest
        }
      }
    })
    </script>
    
    クリックするだけで簡単なボタンがpost 我々の要求/test エンドポイントとログの結果は、この場合は{ "status": "ok" } 私たちの/test エンドポイント.
    ボタンをクリックするとコンソールに表示されます.
  • 初めての投稿/test , それは失敗する419 , これはTokenMismatchException .
  • 次にリクエストを送信する/csrf-token , それは、それが我々の迎撃犯によって捕えられたことを意味します.
  • 原作/test リクエストは再試行され、今回は成功しました(ミドルウェアはリクエストを処理する前にトークンを再生成しませんでした).
  • ボタンを複数回クリックすると、失敗しない場合があります.これはランダムトークンの不一致をシミュレートしているためです.

    掃除
    テストを終えたら、ミドルウェアのhandle 私たちがLaravelがこの特定のミドルウェアを管理したいので、完全に機能してください.また、削除します/test ルートroutes/web.php .

    概要
    全体的に、我々のcodebaseへの影響はかなり最小限です、しかし、ユーザー経験は大いに改善されます.ユーザーは自分のセッションを残すことができますし、自分のCSRFトークンを期限切れにすることができます.戻って、要求をすることは彼らの望ましいワークフローを中断しません.