背景
最近的一个项目用到了 Sanctum 然后访问API的时候就出现 csrf token的报错 CSRF token mismatch.
问题分析
更具文档需要在API路由中加入 EnsureFrontendRequestsAreStateful
这个中间件
'api' => [
\Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
...
],
查看源码 vendor/laravel/sanctum/src/Http/Middleware/EnsureFrontendRequestsAreStateful.php
不难发现里面有一个 fromFrontend
的判断,如果判断是同源就会加上csrf验证
<?php
class EnsureFrontendRequestsAreStateful
{
/**
* Handle the incoming requests.
*
* @param \Illuminate\Http\Request $request
* @param callable $next
* @return \Illuminate\Http\Response
*/
public function handle($request, $next)
{
$this->configureSecureCookieSessions();
return (new Pipeline(app()))->send($request)->through(static::fromFrontend($request) ? [
function ($request, $next) {
$request->attributes->set('sanctum', true);
return $next($request);
},
config('sanctum.middleware.encrypt_cookies', \Illuminate\Cookie\Middleware\EncryptCookies::class),
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
config('sanctum.middleware.verify_csrf_token', \Illuminate\Foundation\Http\Middleware\VerifyCsrfToken::class),
] : [])->then(function ($request) use ($next) {
return $next($request);
});
}
...
/**
* Determine if the given request is from the first-party application frontend.
*
* @param \Illuminate\Http\Request $request
* @return bool
*/
public static function fromFrontend($request)
{
$domain = $request->headers->get('referer') ?: $request->headers->get('origin');
if (is_null($domain)) {
return false;
}
$domain = Str::replaceFirst('https://', '', $domain);
$domain = Str::replaceFirst('http://', '', $domain);
$domain = Str::endsWith($domain, '/') ? $domain : "{$domain}/";
$stateful = array_filter(config('sanctum.stateful', []));
return Str::is(Collection::make($stateful)->map(function ($uri) {
return trim($uri).'/*';
})->all(), $domain);
}
}
结论
如果将前后端代码部署在同一个域下面,如 localhost:80,localhost:8080,localhost/api,localhost/web 这种情况下需要让前端带上 csrf token,要么就使用 api.xxx.xxx,web.xxx.xxx 的方式部署.这个中间件的设计可能是为了安全吧,本地开发还是直接注释掉方便些.