【Laravel】VerifyCsrfTokenの中身を調べてみた
Laravel内部でCSRFトークンをどのように使っているのかが気になったので、VerifyCsrfTokenの中身を調べてみました。
結論
- VerifyCsrfToken.php内のhandleメソッド内でtokenの判定が行われている
- postの場合はtokenがマッチするかを見ている
- その他の場合にはcookieにトークンをセットしている
環境
Laravel 10
VerifyCsrfTokenとは
csrfトークンを用いてXSRF対策を行うミドルウェアです。
Kernel.phpの中を見るとwebの時のミドルウェアとして登録されています。
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class, // これです
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
];
VerifyCsrfTokenの中身
App内のVerifyCsrfToken
先ほどのVerifyCsrfTokenのパスをよく見るとAppの中になっています。
その中身が以下です。
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
class VerifyCsrfToken extends Middleware
{
/**
* The URIs that should be excluded from CSRF verification.
*
* @var array<int, string>
*/
protected $except = [
//
];
}
$exceptという配列がありますが、この配列に入っているパスはcsrfトークンによる検証をスキップします。
このクラスが継承している、パッケージのMiddlewareが今回見ていくクラスです。
パッケージ内のVerifyCsrfToken
ここからはIlluminate/Foundation/Http/Middleware/VerifyCsrfToken.phpの中を見ていきます。
handleメソッド
public function handle($request, Closure $next)
{
if (
$this->isReading($request) ||
$this->runningUnitTests() ||
$this->inExceptArray($request) ||
$this->tokensMatch($request)
) {
return tap($next($request), function ($response) use ($request) {
if ($this->shouldAddXsrfTokenCookie()) {
$this->addCookieToResponse($request, $response);
}
});
}
throw new TokenMismatchException('CSRF token mismatch.');
}
handleメソッドでは4つの条件のどれかに当てはまればtap関数をreturnします。
- isReading
- メソッドがhead,get,optionであるか
- runningUnitTests
- ユニットテストを実行しているか
- inExceptArray
- 前述の
$expect配列に入っている
- 前述の
-
tokenMatch
- tokenが一致するか
tap関数が受け取るクロージャでは新しいtokenをcookieにセットしています
次にtokenが一致するかを検証しているtokenMatchメソッドについてみていきます
tokenMatchメソッド
protected function tokensMatch($request)
{
$token = $this->getTokenFromRequest($request);
return is_string($request->session()->token()) &&
is_string($token) &&
hash_equals($request->session()->token(), $token);
}
getTokenFromRequestでtokenを取得し、 hash_equals関数 でsessionに保存したtokenとrequestで送られてきたtokenが一致するかを検証しています。
tokenが一致せずにエラーになる場合は、$tokenの中身と$request->session()->token()の中身がどうなっているかを確認するといいと思います。
次にgetTokenFromRequestでのtoken取得方法をみていきます。
getTokenFromRequestメソッド
protected function getTokenFromRequest($request)
{
$token = $request->input('_token') ?: $request->header('X-CSRF-TOKEN');
if (! $token && $header = $request->header('X-XSRF-TOKEN')) {
try {
$token = CookieValuePrefix::remove($this->encrypter->decrypt($header, static::serialized()));
} catch (DecryptException) {
$token = '';
}
}
return $token;
}
ここではrequestに含まれる_tokenというパラメータか、ヘッダに含まれるX-CSRF-TOKENの内容をtokenとして使用しています。
それでもtokenが取れなければX-XSRF-TOKENをヘッダから取得して復号化し、tokenとしてセットしています。
Discussion