Skip to main content

Auth 모듈

📝 초안 (Draft)

이 문서는 검토 중입니다. 내용이 변경될 수 있습니다.

Sanctum 기반 인증 서비스를 제공하는 Core 모듈입니다.

개요

Auth 모듈은 Laravel Sanctum을 래핑한 인증 서비스를 제공합니다. 세션 기반 웹 인증과 토큰 기반 API 인증을 모두 지원하며, Interface 기반 설계로 확장이 용이합니다.

┌─────────────────────────────────────────────────────────────┐
│ Auth 모듈 │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Web 인증 │ │ API 인증 │ │ Token 관리 │ │
│ │ (Session) │ │ (Sanctum) │ │ │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │ │
│ └────────────────┼─────────────────┘ │
│ ↓ │
│ AuthService │
│ │ │
│ ┌────────────────┼────────────────┐ │
│ ↓ ↓ ↓ │
│ Permission Tenant Audit │
│ (레벨 확인) (컨텍스트) (로그 기록) │
└─────────────────────────────────────────────────────────────┘

핵심 컴포넌트

컴포넌트역할
AuthService인증 로직 통합 서비스
AuthServiceInterface테스트 용이성을 위한 인터페이스
UserAuthenticated로그인 성공 이벤트
UserLoggedOut로그아웃 이벤트
TokenCreatedAPI 토큰 생성 이벤트

AuthService 사용법

의존성 주입

<?php

namespace App\Http\Controllers;

use App\Core\Base\Auth\Contracts\AuthServiceInterface;

class AuthController extends Controller
{
public function __construct(
private AuthServiceInterface $authService
) {}

public function showLoginForm()
{
if ($this->authService->check()) {
return redirect('/dashboard');
}

return view('auth.login');
}
}

기본 인증

// 로그인 시도
$user = $this->authService->login([
'email' => $request->email,
'password' => $request->password,
], $request->boolean('remember'));

if ($user) {
return redirect('/dashboard');
}

return back()->withErrors(['email' => '인증에 실패했습니다.']);

로그아웃

public function logout()
{
$this->authService->logout();

return redirect('/');
}

웹 인증 (Session 기반)

로그인 컨트롤러 예시

<?php

namespace App\Http\Controllers\Auth;

use App\Core\Base\Auth\Contracts\AuthServiceInterface;
use App\Http\Requests\LoginRequest;
use Illuminate\Http\Request;

class LoginController extends Controller
{
public function __construct(
private AuthServiceInterface $authService
) {}

public function login(LoginRequest $request)
{
$user = $this->authService->login(
$request->only('email', 'password'),
$request->boolean('remember')
);

if (!$user) {
return back()
->withInput($request->only('email'))
->withErrors(['email' => '이메일 또는 비밀번호가 올바르지 않습니다.']);
}

// 인증 가능 여부 확인 (이메일 인증, 계정 활성화 등)
if (!$this->authService->canAuthenticate($user)) {
$this->authService->logout();
return back()->withErrors(['email' => '이 계정으로 로그인할 수 없습니다.']);
}

$request->session()->regenerate();

return redirect()->intended('/dashboard');
}

public function logout(Request $request)
{
$this->authService->logout();

return redirect('/');
}
}

Remember Me

// Remember 옵션 활성화
$user = $this->authService->login($credentials, remember: true);

// Remember 쿠키 설정 커스터마이징 (config/auth.php)
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\Models\User::class,
],
],

API 인증 (Token 기반)

토큰 발급

<?php

namespace App\Http\Controllers\Api;

use App\Core\Base\Auth\Contracts\AuthServiceInterface;
use App\Http\Requests\Api\LoginRequest;

class AuthController extends Controller
{
public function __construct(
private AuthServiceInterface $authService
) {}

public function login(LoginRequest $request)
{
$user = $this->authService->attempt($request->only('email', 'password'));

if (!$user) {
return response()->json([
'message' => '인증에 실패했습니다.',
], 401);
}

// 토큰 생성
$token = $this->authService->createToken(
user: $user,
name: $request->device_name ?? 'api-token',
abilities: ['*'] // 모든 권한
);

return response()->json([
'user' => $user,
'token' => $token,
'token_type' => 'Bearer',
]);
}

public function logout()
{
$user = $this->authService->user();

if ($user) {
// 현재 토큰만 폐기
$token = $this->authService->currentAccessToken();
if ($token && method_exists($token, 'delete')) {
$token->delete();
}
}

return response()->json(['message' => '로그아웃되었습니다.']);
}

public function user()
{
return response()->json($this->authService->user());
}
}

토큰 권한 (Abilities)

// 제한된 권한으로 토큰 생성
$readOnlyToken = $this->authService->createToken(
user: $user,
name: 'read-only-token',
abilities: ['read:users', 'read:products']
);

// 특정 권한으로 토큰 생성
$adminToken = $this->authService->createToken(
user: $user,
name: 'admin-token',
abilities: ['*'] // 모든 권한
);

// 라우트에서 권한 확인
Route::middleware(['auth:sanctum', 'abilities:read:users'])->group(function () {
Route::get('/users', [UserController::class, 'index']);
});

Route::middleware(['auth:sanctum', 'ability:write:users'])->group(function () {
Route::post('/users', [UserController::class, 'store']);
});

토큰 관리

// 모든 토큰 폐기 (비밀번호 변경 시 등)
$this->authService->revokeAllTokens($user);

// 특정 토큰 폐기
$this->authService->revokeToken($user, $tokenId);

// 현재 토큰 정보
$currentToken = $this->authService->currentAccessToken();

API 라우트 설정

// routes/api.php
use Illuminate\Support\Facades\Route;

// 공개 API
Route::post('/auth/login', [AuthController::class, 'login']);
Route::post('/auth/register', [AuthController::class, 'register']);

// 인증 필요 API
Route::middleware('auth:sanctum')->group(function () {
Route::post('/auth/logout', [AuthController::class, 'logout']);
Route::get('/auth/user', [AuthController::class, 'user']);

// 테넌트 컨텍스트 필요
Route::middleware('tenant.context')->group(function () {
Route::apiResource('products', ProductController::class);
Route::apiResource('orders', OrderController::class);
});
});

인증 이벤트

Auth 모듈은 주요 인증 이벤트를 발행합니다.

UserAuthenticated 이벤트

use App\Core\Base\Auth\Events\UserAuthenticated;

// 이벤트 리스너 등록 (EventServiceProvider)
protected $listen = [
UserAuthenticated::class => [
RecordLoginActivity::class,
UpdateLastLoginTimestamp::class,
],
];

// 리스너 예시
class RecordLoginActivity
{
public function handle(UserAuthenticated $event): void
{
$user = $event->user;
$remember = $event->remember;

Log::info("User {$user->id} logged in", [
'remember' => $remember,
'ip' => request()->ip(),
]);
}
}

UserLoggedOut 이벤트

use App\Core\Base\Auth\Events\UserLoggedOut;

class ClearUserSession
{
public function handle(UserLoggedOut $event): void
{
// 사용자 캐시 정리
Cache::forget("user.{$event->user->id}.permissions");
}
}

TokenCreated 이벤트

use App\Core\Base\Auth\Events\TokenCreated;

class NotifyTokenCreation
{
public function handle(TokenCreated $event): void
{
// 보안 알림 발송
$event->user->notify(new ApiTokenCreatedNotification($event->name));
}
}

설정 (config/core.php)

'auth' => [
// 기본 가드
'guard' => env('CORE_AUTH_GUARD', 'web'),

// 이메일 인증 필요 여부
'require_email_verification' => env('CORE_REQUIRE_EMAIL_VERIFICATION', false),

// 비밀번호 정책
'password' => [
'min_length' => 8,
'require_uppercase' => true,
'require_lowercase' => true,
'require_numbers' => true,
'require_symbols' => false,
],

// 세션 설정
'session' => [
'lifetime' => 120, // 분
'expire_on_close' => false,
],

// API 토큰 설정
'api_token' => [
'expiration' => null, // null = 만료 없음, 분 단위
],
],

AuthService 메서드 레퍼런스

메서드설명반환값
attempt($credentials)인증 시도 (로그인 X)?UserInterface
login($credentials, $remember)로그인?UserInterface
logout()로그아웃void
user()현재 사용자?UserInterface
check()인증 여부bool
guest()게스트 여부bool
id()사용자 ID?int
createToken($user, $name, $abilities)API 토큰 생성string
revokeAllTokens($user)모든 토큰 폐기void
revokeToken($user, $tokenId)특정 토큰 폐기void
currentAccessToken()현재 토큰?object
validate($credentials)자격 증명 검증bool
loginUser($user, $remember)사용자 직접 로그인void
verifyPassword($user, $password)비밀번호 확인bool
canAuthenticate($user)인증 가능 여부bool

비밀번호 확인

비밀번호 변경이나 민감한 작업 전 현재 비밀번호 확인:

public function changePassword(Request $request)
{
$user = $this->authService->user();

// 현재 비밀번호 확인
if (!$this->authService->verifyPassword($user, $request->current_password)) {
return back()->withErrors([
'current_password' => '현재 비밀번호가 올바르지 않습니다.',
]);
}

// 새 비밀번호 설정
$user->password = Hash::make($request->new_password);
$user->save();

// 다른 세션/토큰 폐기 (선택)
$this->authService->revokeAllTokens($user);

return back()->with('success', '비밀번호가 변경되었습니다.');
}

Permission과 연동

Auth 모듈은 Permission 모듈과 함께 사용됩니다.

// 인증 + 레벨 확인
Route::middleware(['auth', 'level:2'])->group(function () {
// Tenant Admin 이상만 접근
Route::resource('settings', SettingController::class);
});

// API 인증 + 레벨 확인
Route::middleware(['auth:sanctum', 'level:1'])->group(function () {
// SaaS Admin 이상만 접근
Route::get('/admin/tenants', [TenantController::class, 'index']);
});

테스트 작성

웹 인증 테스트

use App\Models\User;

test('사용자가 로그인할 수 있다', function () {
$user = User::factory()->create([
'password' => bcrypt('password123'),
]);

$response = $this->post('/login', [
'email' => $user->email,
'password' => 'password123',
]);

$response->assertRedirect('/dashboard');
$this->assertAuthenticatedAs($user);
});

test('잘못된 비밀번호로 로그인할 수 없다', function () {
$user = User::factory()->create();

$response = $this->post('/login', [
'email' => $user->email,
'password' => 'wrong-password',
]);

$response->assertSessionHasErrors('email');
$this->assertGuest();
});

test('로그아웃하면 인증이 해제된다', function () {
$user = User::factory()->create();

$this->actingAs($user)
->post('/logout')
->assertRedirect('/');

$this->assertGuest();
});

API 인증 테스트

test('API 토큰으로 인증된 요청을 할 수 있다', function () {
$user = User::factory()->create();
$token = $user->createToken('test-token')->plainTextToken;

$response = $this->withHeader('Authorization', "Bearer {$token}")
->getJson('/api/user');

$response->assertOk()
->assertJsonPath('id', $user->id);
});

test('토큰 없이 보호된 API에 접근할 수 없다', function () {
$response = $this->getJson('/api/user');

$response->assertUnauthorized();
});

test('API 로그인 후 토큰을 받을 수 있다', function () {
$user = User::factory()->create([
'password' => bcrypt('password123'),
]);

$response = $this->postJson('/api/auth/login', [
'email' => $user->email,
'password' => 'password123',
'device_name' => 'test-device',
]);

$response->assertOk()
->assertJsonStructure([
'user' => ['id', 'name', 'email'],
'token',
'token_type',
]);
});

AuthService 단위 테스트

use App\Core\Base\Auth\Contracts\AuthServiceInterface;

test('AuthService로 사용자 로그인할 수 있다', function () {
$authService = app(AuthServiceInterface::class);

$user = User::factory()->create([
'password' => bcrypt('password123'),
]);

$result = $authService->login([
'email' => $user->email,
'password' => 'password123',
]);

expect($result)->not->toBeNull()
->and($result->id)->toBe($user->id)
->and($authService->check())->toBeTrue();
});

흔한 실수와 해결

1. 세션 재생성 누락

// ❌ 잘못된 예: 세션 재생성 없음
public function login(Request $request)
{
$this->authService->login($request->only('email', 'password'));
return redirect('/dashboard');
}

// ✅ 올바른 예: 세션 재생성으로 세션 고정 공격 방지
public function login(Request $request)
{
$this->authService->login($request->only('email', 'password'));
$request->session()->regenerate();
return redirect('/dashboard');
}

2. API와 Web Guard 혼동

// ❌ 잘못된 예: API 컨트롤러에서 web guard 사용
Route::middleware('auth')->group(function () {
Route::get('/api/user', ...); // web guard 적용
});

// ✅ 올바른 예: API에는 sanctum guard 사용
Route::middleware('auth:sanctum')->group(function () {
Route::get('/api/user', ...); // sanctum guard 적용
});

3. 토큰 노출

// ❌ 잘못된 예: 토큰을 로그에 기록
Log::info("Token created: {$token}");

// ✅ 올바른 예: 토큰 정보만 기록
Log::info("Token created for user {$user->id}", [
'token_name' => $name,
'abilities' => $abilities,
]);

관련 문서