Laravelのsession/csrfまわりのハンドリング何しているかのコードリーディングメモ。
Session
Illuminate\Session\Middleware\StartSession
ミドルウェアでセッションのハンドリングをしています。
public function handle($request, Closure $next)
{
if (! $this->sessionConfigured()) {
return $next($request);
}
$session = $this->getSession($request);
if ($this->manager->shouldBlock() ||
($request->route() instanceof Route && $request->route()->locksFor())) {
return $this->handleRequestWhileBlocking($request, $session, $next);
}
return $this->handleStatefulRequest($request, $session, $next);
}
getSession()
ではクッキーからセッションIDを取得し、 Illuminate\Session\Store::setId()
で取得したセッションIDをセットします。
public function getSession(Request $request)
{
return tap($this->manager->driver(), function ($session) use ($request) {
$session->setId($request->cookies->get($session->getName()));
});
}
setId()
は指定したIDを設定するか、新しくセッションIDを発行します。
public function setId($id)
{
$this->id = $this->isValidId($id) ? $id : $this->generateSessionId();
}
https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Session/Store.php#L649-L652
セッションIDを設定したあとは、 handleStatefulRequest()
を呼び出します。
protected function handleStatefulRequest(Request $request, $session, Closure $next)
{
$request->setLaravelSession(
$this->startSession($request, $session)
);
// ...(省略)...
$response = $next($request);
$this->storeCurrentUrl($request, $session);
$this->addCookieToResponse($response, $session);
$this->saveSession($request);
return $response;
}
setLaravelSession()
では $request->session()
などのリクエストクラスからのsession呼び出しができるようにしています。
startSession()
では Store::start()
=> Store::loadSession()
という感じで呼び出していて、セッションIDを使ってセッションストレージからセッションを取り出して $attributes
プロパティにセットしています。
public function start()
{
$this->loadSession();
if (! $this->has('_token')) {
$this->regenerateToken();
}
return $this->started = true;
}
https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Session/Middleware/StartSession.php#L142-L149 https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Session/Store.php#L83-L92
またCSRFトークンである _token
がセッション内に存在しない場合は再生成しています。
public function regenerateToken()
{
$this->put('_token', Str::random(40));
}
https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Session/Store.php#L703-L706
$next($request)
で本処理が完了したら、storeCurrentUrl()
でセッションの _previous.url
にリクエストURLをセットします。
protected function storeCurrentUrl(Request $request, $session)
{
if ($request->isMethod('GET') &&
$request->route() instanceof Route &&
! $request->ajax() &&
! $request->prefetch() &&
! $request->isPrecognitive()) {
$session->setPreviousUrl($request->fullUrl());
}
}
addCookieToResponse()
ではSet-CookieでセッションIDをセットするようにレスポンスヘッダの操作をしています。
protected function addCookieToResponse(Response $response, Session $session)
{
if ($this->sessionIsPersistent($config = $this->manager->getSessionConfig())) {
$response->headers->setCookie(new Cookie(
$session->getName(),
$session->getId(),
$this->getCookieExpirationDate(),
$config['path'],
$config['domain'],
$config['secure'] ?? false,
$config['http_only'] ?? true,
false,
$config['same_site'] ?? null,
$config['partitioned'] ?? false
));
}
}
最後に saveSession()
でセッションをセッションストレージに保存します。
protected function saveSession($request)
{
if (! $request->isPrecognitive()) {
$this->manager->driver()->save();
}
}
CSRF
Illuminate\Foundation\Http\Middleware\VerifyCsrfToken
で処理をしています。
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.');
}
- HEAD, GET, OPTIONSは通す:
isReading()
- テスト実行中は通す:
runningUnitTests()
- 特定のリクエストパスは通す:
inExceptArray()
- トークンマッチしていたら通す:
tokensMatch()
となっている。
tokensMatch()
は getTokenFromRequest()
で取ってきたトークンがセッションに含まれるトークン(上述の _token
)と合致しているかをチェックしています。
protected function tokensMatch($request)
{
$token = $this->getTokenFromRequest($request);
return is_string($request->session()->token()) &&
is_string($token) &&
hash_equals($request->session()->token(), $token);
}
getTokenFromRequest()
はformデータやHTTPヘッダからCSRFトークンを取得してます。
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;
}
Bladeの csrf_field()
はヘルパーで定義されており、input hiddenを作成しています。
function csrf_field()
{
return new HtmlString('<input type="hidden" name="_token" value="'.csrf_token().'" autocomplete="off">');
}
https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Foundation/helpers.php#L324-L327
csrf_token()
はセッションからトークンデータを取り出しています。
function csrf_token()
{
$session = app('session');
if (isset($session)) {
return $session->token();
}
throw new RuntimeException('Application session store not set.');
}
https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Foundation/helpers.php#L338-L347