Laravel 인증 시스템의 내부 구조
Auth 파사드와 AuthManager
Auth 파사드는 Illuminate\Auth\AuthManager의 프록시입니다. AuthManager는 드라이버 패턴으로 여러 가드를 관리하며, config/auth.php 설정을 기반으로 적절한 가드 인스턴스를 생성하고 캐시합니다.
// Auth::guard('web')의 내부 동작(AuthManager::guard()의 간략한 버전)
public function guard($name = null)
{
$name = $name ?: $this->getDefaultDriver();
return $this->guards[$name] ?? ($this->guards[$name] = $this->resolve($name));
}
resolve()는 config/auth.php의 guards 배열에서 driver 키를 읽어 대응하는 팩토리 클로저를 호출합니다. 기본 제공되는 session이나 token 드라이버도 동일한 구조로 등록되어 있습니다.
Guard 인터페이스와 StatefulGuard 인터페이스의 차이
Laravel 인증 가드는 최소한 Illuminate\Contracts\Auth\Guard를 구현해야 합니다. 세션을 유지해야 하는 경우에는 StatefulGuard를 구현합니다.
Guard 인터페이스(Illuminate\Contracts\Auth\Guard)
Guard 인터페이스(Illuminate\Contracts\Auth\Guard)
interface Guard
{
// 인증된 사용자가 존재하는지 확인
public function check();
// 게스트(미인증) 여부 확인
public function guest();
// 현재 인증된 사용자를 반환(미인증이면 null)
public function user();
// 현재 인증된 사용자의 ID를 반환
public function id();
// 주어진 자격 증명이 유효한지 확인(로그인은 수행하지 않음)
public function validate(array $credentials = []);
// 사용자가 설정되어 있는지 확인(미로그인 상태에서도 setUser로 주입 가능)
public function hasUser();
// 인증된 사용자를 수동으로 설정
public function setUser(Authenticatable $user);
}
StatefulGuard 인터페이스(Illuminate\Contracts\Auth\StatefulGuard)
StatefulGuard 인터페이스(Illuminate\Contracts\Auth\StatefulGuard)
StatefulGuard는 Guard를 상속하며, 세션이나 쿠키를 사용한 로그인 상태 유지에 필요한 메서드를 추가합니다.interface StatefulGuard extends Guard
{
// 자격 증명을 검증하고 로그인(remember 플래그 포함)
public function attempt(array $credentials = [], $remember = false);
// 세션을 저장하지 않고 1개의 요청에서만 인증
public function once(array $credentials = []);
// 사용자 인스턴스를 직접 로그인
public function login(Authenticatable $user, $remember = false);
// ID를 지정하여 로그인
public function loginUsingId($id, $remember = false);
// ID를 지정하여 1개의 요청에서만 인증
public function onceUsingId($id);
// "로그인 상태 유지" 쿠키로 로그인했는지 확인
public function viaRemember();
// 로그아웃
public function logout();
}
API 인증이나 자체 토큰 인증처럼 세션이 필요하지 않은 가드는
Guard만 구현하면 됩니다. 관리자 로그인처럼 세션이 필요한 경우에는 StatefulGuard를 구현합니다.커스텀 가드 구현
GuardHelpers 트레이트
Guard 인터페이스의 check(), guest(), id(), hasUser()는 대부분 공통된 구현이 되기 때문에, Laravel은 Illuminate\Auth\GuardHelpers 트레이트를 제공합니다. 이 트레이트를 사용하면 필수 구현을 user()와 validate() 두 메서드로 줄일 수 있습니다.
// GuardHelpers가 제공하는 기본 구현(발췌)
trait GuardHelpers
{
protected $user;
public function check(): bool
{
return ! is_null($this->user());
}
public function guest(): bool
{
return ! $this->check();
}
public function id(): mixed
{
return $this->user()?->getAuthIdentifier();
}
public function hasUser(): bool
{
return ! is_null($this->user);
}
public function setUser(Authenticatable $user): static
{
$this->user = $user;
return $this;
}
}
API 토큰 인증 가드 구현 예제
TokenGuard의 설계를 참고하여 간단한 API 토큰 인증 가드를 구현합니다. 요청 헤더 또는 쿼리 파라미터에서 토큰을 가져와 UserProvider를 통해 사용자를 해석합니다.
가드 클래스 생성
app/Auth 디렉터리에 가드 클래스를 생성합니다.<?php
namespace App\Auth;
use Illuminate\Auth\GuardHelpers;
use Illuminate\Contracts\Auth\Guard;
use Illuminate\Contracts\Auth\UserProvider;
use Illuminate\Http\Request;
class ApiTokenGuard implements Guard
{
use GuardHelpers;
protected Request $request;
public function __construct(UserProvider $provider, Request $request)
{
$this->provider = $provider;
$this->request = $request;
}
/**
* 현재 인증된 사용자를 반환
*/
public function user(): ?\Illuminate\Contracts\Auth\Authenticatable
{
// 캐시된 사용자가 있다면 반환
if (! is_null($this->user)) {
return $this->user;
}
$token = $this->getTokenForRequest();
if (empty($token)) {
return null;
}
// UserProvider를 통해 사용자 조회
$this->user = $this->provider->retrieveByCredentials([
'api_token' => $token,
]);
return $this->user;
}
/**
* 자격 증명 검증만 수행(로그인은 하지 않음)
*/
public function validate(array $credentials = []): bool
{
if (empty($credentials['api_token'])) {
return false;
}
return (bool) $this->provider->retrieveByCredentials($credentials);
}
/**
* 요청에서 토큰을 가져옴
*
* 우선순위: Bearer 헤더 → 쿼리 파라미터 → 요청 바디
*/
protected function getTokenForRequest(): ?string
{
$token = $this->request->bearerToken();
if (empty($token)) {
$token = $this->request->query('api_token');
}
if (empty($token)) {
$token = $this->request->input('api_token');
}
return $token ?: null;
}
/**
* 요청 인스턴스를 교체
*/
public function setRequest(Request $request): static
{
$this->request = $request;
return $this;
}
}
서비스 프로바이더에서 가드 등록
AppServiceProvider의 boot() 메서드에서 Auth::extend()를 사용해 가드를 등록합니다.<?php
namespace App\Providers;
use App\Auth\ApiTokenGuard;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
Auth::extend('api-token', function (Application $app, string $name, array $config) {
// Auth::createUserProvider()로 config의 provider를 해석
$provider = Auth::createUserProvider($config['provider'] ?? 'users');
return new ApiTokenGuard($provider, $app->make('request'));
});
}
}
Auth::createUserProvider()는 config/auth.php의 providers 설정을 읽어 해당하는 UserProvider 인스턴스를 반환합니다. 커스텀 프로바이더를 만들지 않는 한, 이 호출 방식으로 기본 EloquentUserProvider를 사용할 수 있습니다.config/auth.php에서 가드 설정
config/auth.php에 새로운 가드를 추가합니다.'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
// 추가한 커스텀 가드
'api' => [
'driver' => 'api-token', // Auth::extend()의 첫 번째 인자와 일치시킴
'provider' => 'users',
],
],
라우트에 가드 적용
auth 미들웨어에 가드 이름을 지정합니다.// routes/api.php
use Illuminate\Support\Facades\Route;
Route::middleware('auth:api')->group(function () {
Route::get('/user', function () {
return auth()->user();
});
Route::get('/posts', [\App\Http\Controllers\PostController::class, 'index']);
});
Auth::guard('api') 또는 auth('api')를 호출합니다.$user = Auth::guard('api')->user();
클로저를 이용한 간단한 가드
Auth::viaRequest()를 사용하면 클래스를 만들지 않고 클로저만으로 간단한 가드를 정의할 수 있습니다. 프로토타입이나 매우 단순한 인증에 적합합니다.
use Illuminate\Http\Request;
use App\Models\User;
// AppServiceProvider::boot() 내부
Auth::viaRequest('custom-token', function (Request $request): ?User {
$token = $request->bearerToken();
if (empty($token)) {
return null;
}
return User::where('api_token', $token)->first();
});
config/auth.php 설정:
'guards' => [
'api' => [
'driver' => 'custom-token',
],
],
Auth::viaRequest()로 정의한 가드는 UserProvider를 사용하지 않기 때문에 retrieveById()와 같은 프로바이더 메서드가 작동하지 않습니다. 프로덕션 환경에서는 Auth::extend()를 사용하는 클래스 기반 가드를 권장합니다.커스텀 UserProvider 구현
사용자 정보를 데이터베이스가 아닌 다른 소스(외부 API, LDAP 등)에서 가져올 경우, Illuminate\Contracts\Auth\UserProvider 인터페이스를 구현합니다.
<?php
namespace App\Auth;
use App\Models\User;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Auth\UserProvider;
class ApiUserProvider implements UserProvider
{
public function __construct(
protected string $apiBaseUrl,
protected string $model = User::class,
) {}
/**
* ID로 사용자 조회
*/
public function retrieveById(mixed $identifier): ?Authenticatable
{
return ($this->model)::find($identifier);
}
/**
* "로그인 상태 유지" 토큰으로 사용자 조회
* 세션을 사용하지 않는 가드에서는 null을 반환하기만 하면 됨
*/
public function retrieveByToken(mixed $identifier, string $token): ?Authenticatable
{
return null;
}
/**
* "로그인 상태 유지" 토큰 갱신
* 세션을 사용하지 않는 가드에서는 아무것도 하지 않아도 됨
*/
public function updateRememberToken(Authenticatable $user, string $token): void {}
/**
* 자격 증명으로 사용자 조회
* 가드의 validate()와 user()에서 호출됨
*/
public function retrieveByCredentials(array $credentials): ?Authenticatable
{
if (empty($credentials['api_token'])) {
return null;
}
// 외부 API에서 토큰을 검증하고 사용자 정보를 가져오는 예제
$response = \Illuminate\Support\Facades\Http::withToken($credentials['api_token'])
->get("{$this->apiBaseUrl}/auth/me");
if (! $response->successful()) {
return null;
}
$data = $response->json();
// 로컬 DB의 사용자와 연동하거나 동적으로 모델 생성
return User::firstOrCreate(
['external_id' => $data['id']],
['name' => $data['name'], 'email' => $data['email']],
);
}
/**
* 조회한 사용자의 자격 증명을 검증
* 토큰 인증에서는 retrieveByCredentials가 성공하면 true로 충분
*/
public function validateCredentials(Authenticatable $user, array $credentials): bool
{
return true;
}
/**
* 비밀번호 재해시가 필요한지 확인
* 토큰 인증에서는 항상 false로 둠
*/
public function rehashPasswordIfRequired(Authenticatable $user, array $credentials, bool $force = false): void {}
}
커스텀 UserProvider 등록
// AppServiceProvider::boot()
Auth::provider('api-user', function (Application $app, array $config) {
return new \App\Auth\ApiUserProvider(
config('services.auth_api.base_url'),
$config['model'] ?? \App\Models\User::class,
);
});
config/auth.php의 providers 섹션에 추가합니다:
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\Models\User::class,
],
// 커스텀 프로바이더
'api-users' => [
'driver' => 'api-user',
'model' => App\Models\User::class,
],
],
'guards' => [
'api' => [
'driver' => 'api-token',
'provider' => 'api-users', // 커스텀 프로바이더 지정
],
],
실전 유스케이스
멀티 인증(관리자와 일반 사용자에 별도 가드)
관리자 모델 생성
관리자용 Eloquent 모델을 준비합니다.
Authenticatable을 상속함으로써 Auth 시스템과 연동할 수 있습니다.php artisan make:model Admin -m
<?php
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
class Admin extends Authenticatable
{
protected $fillable = ['name', 'email', 'password'];
protected $hidden = ['password', 'remember_token'];
}
config/auth.php 설정
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'admin' => [
'driver' => 'session',
'provider' => 'admins', // 관리자용 프로바이더
],
],
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\Models\User::class,
],
'admins' => [
'driver' => 'eloquent',
'model' => App\Models\Admin::class, // 관리자 모델
],
],
라우트와 미들웨어 설정
// routes/web.php
// 일반 사용자용 라우트(기본 web 가드)
Route::middleware('auth')->group(function () {
Route::get('/dashboard', [DashboardController::class, 'index']);
});
// 관리자용 라우트(admin 가드)
Route::prefix('admin')->middleware('auth:admin')->group(function () {
Route::get('/dashboard', [AdminDashboardController::class, 'index']);
});
가드를 지정한 로그인 처리 작성
<?php
namespace App\Http\Controllers\Admin;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class LoginController extends Controller
{
public function store(Request $request)
{
$credentials = $request->validate([
'email' => 'required|email',
'password' => 'required',
]);
// admin 가드를 명시하여 attempt 수행
if (Auth::guard('admin')->attempt($credentials)) {
$request->session()->regenerate();
return redirect()->intended('/admin/dashboard');
}
return back()->withErrors(['email' => '로그인 정보가 올바르지 않습니다.']);
}
public function destroy(Request $request)
{
Auth::guard('admin')->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect('/admin/login');
}
}
JWT 토큰을 이용한 외부 API 인증
외부 JWT 인증 서비스를 사용하는 경우의 커스텀 가드 구현 예제입니다.<?php
namespace App\Auth;
use App\Models\User;
use Illuminate\Auth\GuardHelpers;
use Illuminate\Contracts\Auth\Guard;
use Illuminate\Contracts\Auth\UserProvider;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
class JwtGuard implements Guard
{
use GuardHelpers;
protected Request $request;
protected ?array $payload = null;
public function __construct(UserProvider $provider, Request $request)
{
$this->provider = $provider;
$this->request = $request;
}
public function user(): ?\Illuminate\Contracts\Auth\Authenticatable
{
if (! is_null($this->user)) {
return $this->user;
}
$token = $this->request->bearerToken();
if (empty($token)) {
return null;
}
// 외부 서비스에서 JWT 검증
$payload = $this->verifyToken($token);
if (is_null($payload)) {
return null;
}
$this->payload = $payload;
$this->user = $this->provider->retrieveById($payload['sub']);
return $this->user;
}
public function validate(array $credentials = []): bool
{
if (empty($credentials['token'])) {
return false;
}
return ! is_null($this->verifyToken($credentials['token']));
}
/**
* JWT 검증 및 payload 획득
*/
protected function verifyToken(string $token): ?array
{
$response = Http::withToken($token)
->get(config('services.auth.verify_url'));
if (! $response->successful()) {
return null;
}
return $response->json();
}
/**
* 검증된 JWT payload를 반환
*/
public function payload(): ?array
{
return $this->payload;
}
}
AppServiceProvider에서 등록:
Auth::extend('jwt', function (Application $app, string $name, array $config) {
return new \App\Auth\JwtGuard(
Auth::createUserProvider($config['provider'] ?? 'users'),
$app->make('request'),
);
});
Auth::guard('jwt')->payload()처럼 커스텀 가드 고유의 메서드에도 접근할 수 있습니다. Auth::guard()가 반환하는 것은 가드 인스턴스 그 자체이므로, 인터페이스에 없는 메서드도 호출할 수 있습니다.테스트
커스텀 가드의 유닛 테스트에서는UserProvider를 목(mock)으로 만들어 가드의 동작을 확인합니다.
<?php
namespace Tests\Unit\Auth;
use App\Auth\ApiTokenGuard;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Auth\UserProvider;
use Illuminate\Http\Request;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class ApiTokenGuardTest extends TestCase
{
private UserProvider&MockObject $provider;
private ApiTokenGuard $guard;
protected function setUp(): void
{
parent::setUp();
$this->provider = $this->createMock(UserProvider::class);
}
public function test_user_returns_null_when_no_token(): void
{
$request = Request::create('/');
$guard = new ApiTokenGuard($this->provider, $request);
$this->assertNull($guard->user());
}
public function test_user_returns_user_with_valid_bearer_token(): void
{
$mockUser = $this->createMock(Authenticatable::class);
$this->provider
->expects($this->once())
->method('retrieveByCredentials')
->with(['api_token' => 'valid-token'])
->willReturn($mockUser);
$request = Request::create('/');
$request->headers->set('Authorization', 'Bearer valid-token');
$guard = new ApiTokenGuard($this->provider, $request);
$this->assertSame($mockUser, $guard->user());
}
public function test_check_returns_false_when_no_token(): void
{
$request = Request::create('/');
$guard = new ApiTokenGuard($this->provider, $request);
$this->assertFalse($guard->check());
}
public function test_validate_returns_false_without_api_token_credential(): void
{
$request = Request::create('/');
$guard = new ApiTokenGuard($this->provider, $request);
$this->assertFalse($guard->validate([]));
}
}
ActingAs를 사용한 기능 테스트에서는 특정 가드에 사용자를 설정할 수 있습니다.
// 특정 가드로 사용자를 인증하여 테스트
$this->actingAs($user, 'api')
->getJson('/api/user')
->assertOk();
관련 페이지
인증(입문)
스타터 킷과 표준 인증 흐름을 확인합니다.
서비스 컨테이너
가드 등록에서 사용하는 서비스 컨테이너의 구조를 이해합니다.