Skip to main content

路由中间件之 VerifyCsrfToken

简介

[
'App\Http\Middleware\EncryptCookies',
'Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse',
'Illuminate\Session\Middleware\StartSession',
'Illuminate\View\Middleware\ShareErrorsFromSession',
'App\Http\Middleware\VerifyCsrfToken', // 本章内容
'Illuminate\Routing\Middleware\SubstituteBindings',
'App\Http\Middleware\RedirectIfAuthenticated',
]

本章,我们围绕 VerifyCsrfToken 中间件进行源码分析。从字面意思我们知道,这个中间件就是实现 CSRF 令牌验证的中间件。如果我们在调试 web 路由时,被 CSRF 影响,可以注释掉此中间件,即可消除 CSRF 令牌的影响。如下:

Laravel

四种情况下,将不会抛出 CSRF 异常

public function handle($request, Closure $next)
{
if (
$this->isReading($request) ||
$this->runningUnitTests() ||
$this->inExceptArray($request) ||
$this->tokensMatch($request)
) {
return $this->addCookieToResponse($request, $next($request));
}

throw new TokenMismatchException;
}

大家看上面的源码,内容与逻辑一目了然。当 isReadingrunningUnitTestsinExceptArraytokensMatch 有一个为真,那么就通过 CSRF 令牌验证,否则抛出异常。

isReading

protected function isReading($request)
{
return in_array($request->method(), ['HEAD', 'GET', 'OPTIONS']);
}

从源码我们知道:isReading 意思是当请求方法是 HEAD、GET、OPTIONS 中的一个时,直接通过 CSRF 验证。

runningUnitTests

protected function runningUnitTests()
{
return $this->app->runningInConsole() && $this->app->runningUnitTests();
}

runningUnitTests 是指当 php 在控制台运行且是运行环境为测试环境时,通过 CSRF 请求

inExceptArray

protected function inExceptArray($request)
{
foreach ($this->except as $except) {
if ($except !== '/') {
$except = trim($except, '/');
}

if ($request->fullUrlIs($except) || $request->is($except)) {
return true;
}
}

return false;
}

inExceptArray 指排除掉在 except 属性中指定的路由,不对其进行 CSRF 验证。

except 属性指定位置如下:

Laravel

tokensMatch

protected function tokensMatch($request)
{
$token = $this->getTokenFromRequest($request);

return is_string($request->session()->token()) &&
is_string($token) &&
hash_equals($request->session()->token(), $token);
}

tokensMatch 即请求过来的 token 与服务端存储的 token 进行防时序 hash 验证,看看是否相等,如果相等通过 CSRF 验证,如果不相等则抛出异常。

那么如何获取请求过来的 token 呢

protected function getTokenFromRequest($request)
{
// 重点在这里,获取 token 的两种方式:在请求体中加入 _token 字段,在请求头中加入 X-CSRF-TOKEN 字段
$token = $request->input('_token') ?: $request->header('X-CSRF-TOKEN');

if (! $token && $header = $request->header('X-XSRF-TOKEN')) {
// 第三种 token 获取方式,从请求头 X-XSRF-TOKEN 字段解密获取请求过来 token,这种更加麻烦与安全
$token = $this->encrypter->decrypt($header, static::serialized());
}

return $token;
}

关于官方 CSRF 的内容 --> 传送门