Laravelの認証周りのコードリーディングメモ。
↓こんな感じなパターンを見ていく
Route::get('/resources', function () {
$user = Auth::user();
// ...
})->middleware('auth');
Route::post('/login', function ($request) {
if (Auth::attempt($request->all()) {
// ...
}
});
認証チェック
Illuminate\Auth\Middleware\Authenticate
ミドルウェアでチェックしています。
public function handle($request, Closure $next, ...$guards)
{
$this->authenticate($request, $guards);
return $next($request);
}
protected function authenticate($request, array $guards)
{
if (empty($guards)) {
$guards = [null];
}
foreach ($guards as $guard) {
if ($this->auth->guard($guard)->check()) {
return $this->auth->shouldUse($guard);
}
}
$this->unauthenticated($request, $guards);
}
https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Auth/Middleware/Authenticate.php#L60-L65 https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Auth/Middleware/Authenticate.php#L76-L89
Illuminate\Auth\AuthManager::guard($guard)->check()
でチェックして認証が通っていなければ unauthenticated()
メソッド経由で AuthenticationException
をスローします。
guard()
は引数がnullの場合は getDefaultDriver()
でdriver名を取得して resolve()
した値を取得します。
public function guard($name = null)
{
$name = $name ?: $this->getDefaultDriver();
return $this->guards[$name] ?? $this->guards[$name] = $this->resolve($name);
}
https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Auth/AuthManager.php#L66-L71
resolve()
はconfigから取得してdriverを取得して各driverメソッドを呼び出します。
protected function resolve($name)
{
$config = $this->getConfig($name);
if (is_null($config)) {
throw new InvalidArgumentException("Auth guard [{$name}] is not defined.");
}
if (isset($this->customCreators[$config['driver']])) {
return $this->callCustomCreator($name, $config);
}
$driverMethod = 'create'.ucfirst($config['driver']).'Driver';
if (method_exists($this, $driverMethod)) {
return $this->{$driverMethod}($name, $config);
}
throw new InvalidArgumentException(
"Auth driver [{$config['driver']}] for guard [{$name}] is not defined."
);
}
https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Auth/AuthManager.php#L81-L102
デフォルトの値は session
が利用されるので createSessionDriver()
で Illuminate\Auth\SessionGuard
を取得します。
SessionGuard::check()
は user()
がnullかどうかで判定しています。
public function check()
{
return ! is_null($this->user());
}
https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Auth/GuardHelpers.php#L54-L57
user()
ではセッションからユーザIDを取得し、provider経由でユーザのModelインスタンスを取得します。
public function user()
{
if ($this->loggedOut) {
return;
}
if (! is_null($this->user)) {
return $this->user;
}
$id = $this->session->get($this->getName());
if (! is_null($id) && $this->user = $this->provider->retrieveById($id)) {
$this->fireAuthenticatedEvent($this->user);
}
// ...(省略)...
return $this->user;
}
https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Auth/SessionGuard.php#L151-L187
providerは AuthManager::createUserProvider()
で生成しています。
public function createUserProvider($provider = null)
{
if (is_null($config = $this->getProviderConfiguration($provider))) {
return;
}
if (isset($this->customProviderCreators[$driver = ($config['driver'] ?? null)])) {
return call_user_func(
$this->customProviderCreators[$driver], $this->app, $config
);
}
return match ($driver) {
'database' => $this->createDatabaseProvider($config),
'eloquent' => $this->createEloquentProvider($config),
default => throw new InvalidArgumentException(
"Authentication user provider [{$driver}] is not defined."
),
};
}
https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Auth/AuthManager.php#L125 https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Auth/CreatesUserProviders.php#L24-L43
getProviderConfiguration()
で auth.providers.{provider_name}
のconfigを取得し、driverがeloquentであれば createEloquentProvider()
を呼び出し、 EloquentUserProvider
を返します。
protected function createEloquentProvider($config)
{
return new EloquentUserProvider($this->app['hash'], $config['model']);
}
EloquentUserProvider::retrieveById()
は createModel()
でconfigで指定したモデルをインスタンス化し、 Model::newModelQuery()
経由でクエリを叩き、DBからレコードを取得します。
public function retrieveById($identifier)
{
$model = $this->createModel();
return $this->newModelQuery($model)
->where($model->getAuthIdentifierName(), $identifier)
->first();
}
ユーザ情報取得と認証
Illuminate\Auth\AuthManager
で user()
や attempt()
は __call
でguardに処理を移譲されます。
public function __call($method, $parameters)
{
return $this->guard()->{$method}(...$parameters);
}
https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Auth/AuthManager.php#L339-L342
user()
は上記の処理でAuthenticateミドルウェアで認証チェック済みの場合は、コントローラで処理する際はすでにモデルがセットされている状態になります。
attempt()
は $credentials
のarrayデータを受け取って providerの retriveByCredentials()
でデータを取得し、パスワードが正しいかどうかを hasValidCredentials
で検証します。
public function attempt(array $credentials = [], $remember = false)
{
$this->fireAttemptEvent($credentials, $remember);
$this->lastAttempted = $user = $this->provider->retrieveByCredentials($credentials);
if ($this->hasValidCredentials($user, $credentials)) {
$this->rehashPasswordIfRequired($user, $credentials);
$this->login($user, $remember);
return true;
}
$this->fireFailedEvent($user, $credentials);
return false;
}
https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Auth/SessionGuard.php#L387-L410
retriveByCredentials()
ではパスワード以外の$credentialsデータ(例えばemail)を抽出し、それらを使ってDBを検索します。
public function retrieveByCredentials(array $credentials)
{
$credentials = array_filter(
$credentials,
fn ($key) => ! str_contains($key, 'password'),
ARRAY_FILTER_USE_KEY
);
if (empty($credentials)) {
return;
}
$query = $this->newModelQuery();
foreach ($credentials as $key => $value) {
if (is_array($value) || $value instanceof Arrayable) {
$query->whereIn($key, $value);
} elseif ($value instanceof Closure) {
$value($query);
} else {
$query->where($key, $value);
}
}
return $query->first();
}
hasValidCredentials()
ではproviderの validateCredentials()
でパスワードの値を検証しています。
protected function hasValidCredentials($user, $credentials)
{
return $this->timebox->call(function ($timebox) use ($user, $credentials) {
$validated = ! is_null($user) && $this->provider->validateCredentials($user, $credentials);
if ($validated) {
$timebox->returnEarly();
$this->fireValidatedEvent($user);
}
return $validated;
}, 200 * 1000);
}
https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Auth/SessionGuard.php#L449-L462
ちなみに timebox
はバリデーション失敗時に早くレスポンスを返しすぎて攻撃者にヒントを与えないために指定の時間まで待ってレスポンスを返す仕組みです。
public function call(callable $callback, int $microseconds)
{
$exception = null;
$start = microtime(true);
try {
$result = $callback($this);
} catch (Throwable $caught) {
$exception = $caught;
}
$remainder = intval($microseconds - ((microtime(true) - $start) * 1000000));
if (! $this->earlyReturn && $remainder > 0) {
$this->usleep($remainder);
}
if ($exception) {
throw $exception;
}
return $result;
}
https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Support/Timebox.php#L25-L48
public function validateCredentials(UserContract $user, array $credentials)
{
if (is_null($plain = $credentials['password'])) {
return false;
}
return $this->hasher->check($plain, $user->getAuthPassword());
}
SessionGuard::login()
は updateSession()
でセッションを更新し、setUser()
でインスタンス変数にユーザモデルなどをセットします。
public function login(AuthenticatableContract $user, $remember = false)
{
$this->updateSession($user->getAuthIdentifier());
if ($remember) {
$this->ensureRememberTokenIsSet($user);
$this->queueRecallerCookie($user);
}
$this->fireLoginEvent($user, $remember);
$this->setUser($user);
}
https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Auth/SessionGuard.php#L521-L540
updateSession()
はセッションにユーザIDをセットして、migrate()
によってセッションIDを洗替えします。
protected function updateSession($id)
{
$this->session->put($this->getName(), $id);
$this->session->migrate(true);
}
https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Auth/SessionGuard.php#L548-L553