11 KiB
인증 및 세션 처리
관련 문서: index.md | middleware.md
TL;DR (5초 요약)
1. Laravel Sanctum 토큰 전용 인증 (Bearer 토큰만 사용)
2. 세션은 /dev 대시보드 전용: StartApiSession 미들웨어로 로그인/로그아웃 라우트에서만 생성
3. 로그아웃: 토큰 삭제 + 세션 무효화 (세션 존재 시)
4. 슬라이딩 만료: 토큰 잔여시간 절반 미만 시 자동 갱신
5. 401 응답 시 세션 쿠키 자동 정리 (무조건적 forget cookie)
목차
- Laravel Sanctum 토큰 전용 인증
- StartApiSession 미들웨어
- 토큰 슬라이딩 만료
- 로그아웃 구현 원칙
- 401 응답 시 세션 쿠키 정리
- Laravel Sanctum plainTextToken 구조
- 세션 존재 확인
- 세션 무효화 3단계
- 완전한 로그아웃 구현 예시
- 안티 패턴
Laravel Sanctum 토큰 전용 인증
그누보드7 프로젝트는 SPA(Single Page Application)를 위한 토큰 전용 인증 방식을 사용합니다:
특징:
- Bearer 토큰 전용: API 인증은
Authorization: Bearer {token}헤더로만 수행 EnsureFrontendRequestsAreStateful미사용: 세션 기반 Sanctum Guard를 사용하지 않음currentAccessToken(): 항상PersonalAccessToken반환 (TransientToken 없음)- 세션은 /dev 대시보드 전용:
StartApiSession미들웨어가 로그인/로그아웃 라우트에서만 세션 생성
인증 가드 설정 (bootstrap/app.php):
// stateful 미들웨어 없음 — Sanctum Guard는 토큰만 체크
$middleware->alias([
'start.api.session' => \App\Http\Middleware\StartApiSession::class,
]);
StartApiSession 미들웨어
로그인/로그아웃 라우트에서만 세션을 시작하는 전용 미들웨어입니다.
목적:
/dev대시보드(Telescope, Horizon 등)는 세션 기반webguard 인증 사용- API 로그인 시
Auth::guard('web')->login($user)으로 세션에 사용자 저장 - 로그아웃 시 세션 무효화
동작 방식:
// StartApiSession은 세션 파이프라인만 실행 (CSRF 검증 없음, sanctum 속성 미설정)
EncryptCookies → AddQueuedCookiesToResponse → StartSession → Controller
적용 라우트:
// 로그인 라우트 (세션 생성)
Route::post('login', ...)->middleware('start.api.session');
// 로그아웃 라우트 (세션 무효화)
Route::post('logout', ...)->middleware('start.api.session');
핵심 차이 — stateful vs start.api.session:
| 항목 | stateful (제거됨) |
start.api.session |
|---|---|---|
| 적용 범위 | 모든 API 라우트 | 로그인/로그아웃만 |
| Sanctum 세션 인증 | ✅ 활성화 | ❌ 비활성화 |
| CSRF 검증 | ✅ 포함 | ❌ 미포함 |
sanctum 속성 설정 |
✅ 설정 | ❌ 미설정 |
| 세션 쿠키 발급 | 모든 API 응답 | 로그인/로그아웃 응답만 |
구현 파일: app/Http/Middleware/StartApiSession.php
토큰 슬라이딩 만료
G7은 슬라이딩 만료(Sliding Expiration) 방식으로 토큰 만료 시간을 자동 갱신합니다.
동작 원리
[로그인] ───────────────────────────────────────> [만료]
0분 60분
↑
30분에 API 요청 발생 (절반 미만 남음)
│
└─────────────────────────────────────> [만료]
30분 90분
갱신 조건
토큰 만료 시간은 다음 조건을 모두 만족할 때 갱신됩니다:
| 조건 | 설명 |
|---|---|
| 인증된 요청 | $request->user()가 존재 |
| PersonalAccessToken | 세션이 아닌 토큰 인증 |
| 만료 시간 설정됨 | expires_at이 null이 아님 (무한대 토큰 제외) |
| 절반 미만 남음 | 잔여 시간 < 설정값의 50% |
설정값
환경설정의 security.auth_token_lifetime 값을 사용합니다:
- 기본값: 30분
- 범위: 0~3600분 (0은 무한대)
구현 파일
- 미들웨어:
app/Http/Middleware/RefreshTokenExpiration.php - 등록 위치:
bootstrap/app.php(api그룹에 등록) - 테스트:
tests/Feature/Api/Auth/TokenExpirationTest.php
로그아웃 구현 원칙
필수: 토큰 삭제 + 세션 무효화 (세션이 존재하는 경우)
필수: currentAccessToken() 삭제 → 세션 존재 시 세션 무효화
토큰 전용 인증에서는 currentAccessToken()이 항상 PersonalAccessToken을 반환합니다:
// 1. 토큰 삭제
$token = $user->currentAccessToken();
if ($token instanceof \Laravel\Sanctum\PersonalAccessToken) {
$token->delete();
}
// 2. 세션 무효화 (StartApiSession 미들웨어가 세션을 시작한 경우)
if (request()->hasSession() && request()->session()->isStarted() && Auth::guard('web')->check()) {
Auth::guard('web')->logout();
request()->session()->invalidate();
request()->session()->regenerateToken();
}
특징:
stateful미들웨어 제거로TransientToken케이스가 없어짐currentAccessToken()이 항상PersonalAccessToken반환- 세션 무효화는
start.api.session미들웨어가 적용된 로그아웃 라우트에서만 실행됨
401 응답 시 세션 쿠키 정리
API 요청에서 AuthenticationException 발생 시 (토큰 만료, 미인증 등), 잔존 세션 쿠키를 만료시켜 보안 공백을 방지합니다.
구현 위치: bootstrap/app.php withExceptions()
$exceptions->render(function (\Illuminate\Auth\AuthenticationException $e, \Illuminate\Http\Request $request) {
if ($request->expectsJson()) {
return response()->json(['message' => __('auth.unauthenticated')], 401)
->withCookie(cookie()->forget(config('session.cookie')));
}
});
동작 방식:
- 무조건적 forget: 세션 쿠키 존재 여부와 관계없이 항상
Set-Cookie만료 헤더 전송 - 클라이언트에 해당 쿠키가 없으면
Set-Cookie헤더는 무시됨 (무해) - 클라이언트에 잔존 세션 쿠키가 있으면 즉시 삭제됨
테스트 파일: tests/Feature/Api/Auth/SessionCleanupOnUnauthorizedTest.php
Laravel Sanctum plainTextToken 구조
형식: {tokenId}|{actualToken}
예시:
9|iHzLc0VcFxEKi8dJlyqaPQoMBKBgs1ocpZpWKQzj0aaddebe
↓ ↓
DB ID SHA256 해시 후 DB 저장
DB 저장 방식:
|앞부분 (tokenId): DB의id컬럼에 저장|뒷부분 (actualToken): SHA256 해시 후token컬럼에 저장
파싱 방법:
// ✅ DO: explode()로 분리 후 뒷부분만 해시
[, $token] = explode('|', $bearerToken, 2);
$hashedToken = hash('sha256', $token);
// ❌ DON'T: 전체 문자열을 해시
$hashedToken = hash('sha256', $bearerToken); // 잘못된 해시값
세션 존재 확인
세션 무효화 전 반드시 세션 존재 여부를 확인해야 합니다:
if (request()->hasSession() && request()->session()->isStarted() && Auth::guard('web')->check()) {
// 세션 무효화 로직
}
체크 항목:
| 메서드 | 설명 |
|---|---|
hasSession() |
세션이 바인딩되어 있는지 확인 |
isStarted() |
세션이 실제로 시작되었는지 확인 |
Auth::guard('web')->check() |
세션에 인증된 사용자가 있는지 확인 |
세션 무효화 3단계
Auth::guard('web')->logout(); // 1. 세션에서 인증 정보 제거
request()->session()->invalidate(); // 2. 세션 ID 무효화 및 새 세션 생성
request()->session()->regenerateToken(); // 3. CSRF 토큰 재생성
각 단계 설명:
| 단계 | 메서드 | 역할 |
|---|---|---|
| 1 | logout() |
세션에서 사용자 인증 정보 제거 |
| 2 | invalidate() |
세션 ID를 무효화하고 새 세션 ID 생성 |
| 3 | regenerateToken() |
CSRF 토큰을 재생성 |
regenerateToken()을 하는 이유:
- 로그아웃 후에도 사용자가 같은 브라우저를 계속 사용할 수 있음
- 이전 CSRF 토큰을 무효화하고 새로운 CSRF 토큰 발급
- 로그아웃 이후의 공개 페이지 요청(로그인 폼 등)을 위한 새로운 보안 토큰 제공
완전한 로그아웃 구현 예시
<?php
namespace App\Services;
use App\Extension\HookManager;
use App\Models\User;
use Illuminate\Support\Facades\Auth;
class AuthService
{
/**
* 사용자를 로그아웃시키고 현재 디바이스의 인증 토큰만 삭제합니다.
*/
public function logout(User $user): void
{
// Hook 발생 (로그아웃 시작)
HookManager::doAction('core.auth.before_logout', $user);
// 토큰 삭제 처리 (토큰 전용 인증이므로 항상 PersonalAccessToken)
$token = $user->currentAccessToken();
if ($token instanceof \Laravel\Sanctum\PersonalAccessToken) {
$token->delete();
}
// 세션 무효화 (StartApiSession 미들웨어가 세션을 시작한 경우)
if (request()->hasSession() && request()->session()->isStarted() && Auth::guard('web')->check()) {
Auth::guard('web')->logout();
request()->session()->invalidate();
request()->session()->regenerateToken();
}
// Hook 발생 (로그아웃 완료)
HookManager::doAction('core.auth.logout', $user);
}
}
안티 패턴
// ❌ DON'T: EnsureFrontendRequestsAreStateful 사용 (제거됨)
$middleware->api(prepend: [
\Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
]);
// ❌ DON'T: 세션 존재 확인 없이 무효화 (에러 발생 가능)
Auth::guard('web')->logout(); // 세션이 없으면 에러
// ❌ DON'T: 토큰 삭제 없이 세션만 무효화
request()->session()->invalidate(); // 토큰이 살아있으면 재인증 가능
// ❌ DON'T: 전체 plainTextToken 해시 (DB 값과 불일치)
$hashedToken = hash('sha256', $bearerToken); // 잘못됨
참고 자료
관련 문서
- index.md - 백엔드 가이드 인덱스
- middleware.md - 미들웨어 등록 규칙 (OptionalSanctumMiddleware 포함)