333 lines
11 KiB
Markdown
333 lines
11 KiB
Markdown
# 인증 및 세션 처리
|
|
|
|
> **관련 문서**: [index.md](index.md) | [middleware.md](middleware.md)
|
|
|
|
---
|
|
|
|
## TL;DR (5초 요약)
|
|
|
|
```text
|
|
1. Laravel Sanctum 토큰 전용 인증 (Bearer 토큰만 사용)
|
|
2. 세션은 /dev 대시보드 전용: StartApiSession 미들웨어로 로그인/로그아웃 라우트에서만 생성
|
|
3. 로그아웃: 토큰 삭제 + 세션 무효화 (세션 존재 시)
|
|
4. 슬라이딩 만료: 토큰 잔여시간 절반 미만 시 자동 갱신
|
|
5. 401 응답 시 세션 쿠키 자동 정리 (무조건적 forget cookie)
|
|
```
|
|
|
|
---
|
|
|
|
## 목차
|
|
|
|
1. [Laravel Sanctum 토큰 전용 인증](#laravel-sanctum-토큰-전용-인증)
|
|
2. [StartApiSession 미들웨어](#startapisession-미들웨어)
|
|
3. [토큰 슬라이딩 만료](#토큰-슬라이딩-만료)
|
|
4. [로그아웃 구현 원칙](#로그아웃-구현-원칙)
|
|
5. [401 응답 시 세션 쿠키 정리](#401-응답-시-세션-쿠키-정리)
|
|
6. [Laravel Sanctum plainTextToken 구조](#laravel-sanctum-plaintexttoken-구조)
|
|
7. [세션 존재 확인](#세션-존재-확인)
|
|
8. [세션 무효화 3단계](#세션-무효화-3단계)
|
|
9. [완전한 로그아웃 구현 예시](#완전한-로그아웃-구현-예시)
|
|
10. [안티 패턴](#안티-패턴)
|
|
|
|
---
|
|
|
|
## Laravel Sanctum 토큰 전용 인증
|
|
|
|
그누보드7 프로젝트는 SPA(Single Page Application)를 위한 **토큰 전용 인증 방식**을 사용합니다:
|
|
|
|
**특징**:
|
|
- **Bearer 토큰 전용**: API 인증은 `Authorization: Bearer {token}` 헤더로만 수행
|
|
- **`EnsureFrontendRequestsAreStateful` 미사용**: 세션 기반 Sanctum Guard를 사용하지 않음
|
|
- **`currentAccessToken()`**: 항상 `PersonalAccessToken` 반환 (TransientToken 없음)
|
|
- **세션은 /dev 대시보드 전용**: `StartApiSession` 미들웨어가 로그인/로그아웃 라우트에서만 세션 생성
|
|
|
|
**인증 가드 설정** (`bootstrap/app.php`):
|
|
```php
|
|
// stateful 미들웨어 없음 — Sanctum Guard는 토큰만 체크
|
|
$middleware->alias([
|
|
'start.api.session' => \App\Http\Middleware\StartApiSession::class,
|
|
]);
|
|
```
|
|
|
|
---
|
|
|
|
## StartApiSession 미들웨어
|
|
|
|
로그인/로그아웃 라우트에서만 세션을 시작하는 전용 미들웨어입니다.
|
|
|
|
**목적**:
|
|
- `/dev` 대시보드(Telescope, Horizon 등)는 세션 기반 `web` guard 인증 사용
|
|
- API 로그인 시 `Auth::guard('web')->login($user)`으로 세션에 사용자 저장
|
|
- 로그아웃 시 세션 무효화
|
|
|
|
**동작 방식**:
|
|
```php
|
|
// StartApiSession은 세션 파이프라인만 실행 (CSRF 검증 없음, sanctum 속성 미설정)
|
|
EncryptCookies → AddQueuedCookiesToResponse → StartSession → Controller
|
|
```
|
|
|
|
**적용 라우트**:
|
|
```php
|
|
// 로그인 라우트 (세션 생성)
|
|
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`을 반환합니다:
|
|
|
|
```php
|
|
// 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()`
|
|
|
|
```php
|
|
$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` 컬럼에 저장
|
|
|
|
**파싱 방법**:
|
|
```php
|
|
// ✅ DO: explode()로 분리 후 뒷부분만 해시
|
|
[, $token] = explode('|', $bearerToken, 2);
|
|
$hashedToken = hash('sha256', $token);
|
|
|
|
// ❌ DON'T: 전체 문자열을 해시
|
|
$hashedToken = hash('sha256', $bearerToken); // 잘못된 해시값
|
|
```
|
|
|
|
---
|
|
|
|
## 세션 존재 확인
|
|
|
|
세션 무효화 전 반드시 세션 존재 여부를 확인해야 합니다:
|
|
|
|
```php
|
|
if (request()->hasSession() && request()->session()->isStarted() && Auth::guard('web')->check()) {
|
|
// 세션 무효화 로직
|
|
}
|
|
```
|
|
|
|
**체크 항목**:
|
|
| 메서드 | 설명 |
|
|
|--------|------|
|
|
| `hasSession()` | 세션이 바인딩되어 있는지 확인 |
|
|
| `isStarted()` | 세션이 실제로 시작되었는지 확인 |
|
|
| `Auth::guard('web')->check()` | 세션에 인증된 사용자가 있는지 확인 |
|
|
|
|
---
|
|
|
|
## 세션 무효화 3단계
|
|
|
|
```php
|
|
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
|
|
<?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);
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 안티 패턴
|
|
|
|
```php
|
|
// ❌ 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); // 잘못됨
|
|
```
|
|
|
|
---
|
|
|
|
## 참고 자료
|
|
|
|
- **구현 파일**: [app/Services/AuthService.php](../../../app/Services/AuthService.php)
|
|
- **테스트 파일**: [tests/Feature/Api/Admin/AdminAuthTest.php](../../../tests/Feature/Api/Admin/AdminAuthTest.php)
|
|
|
|
---
|
|
|
|
## 관련 문서
|
|
|
|
- [index.md](index.md) - 백엔드 가이드 인덱스
|
|
- [middleware.md](middleware.md) - 미들웨어 등록 규칙 (OptionalSanctumMiddleware 포함)
|