# 세션 기반 인증 전환 가이드 - 백엔드 (PHP/Laravel)

## 📋 개요

**목적**: JWT 토큰 기반 → 세션 기반 인증으로 전환하여 보안 강화

**주요 보안 개선 사항**:
- ✅ 로그아웃 시 즉시 세션 무효화 (토큰 만료 대기 불필요)
- ✅ 세션 하이재킹 실시간 감지 (IP/User-Agent 추적)
- ✅ 관리자의 강제 로그아웃 기능
- ✅ 1계정 1세션 강제 (동시 로그인 제한)
- ✅ 의심스러운 활동 자동 차단

---

## 🔧 1단계: 환경 설정

### 1.1 세션 드라이버 설정

```bash
# .env
SESSION_DRIVER=redis
SESSION_LIFETIME=120  # 2시간 (분 단위)
SESSION_SECURE_COOKIE=true
SESSION_DOMAIN=.yourdomain.com  # 서브도메인 공유 시
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
```

### 1.2 세션 설정 파일

```php
// config/session.php
return [
    'driver' => env('SESSION_DRIVER', 'redis'),
    'lifetime' => env('SESSION_LIFETIME', 120),
    'expire_on_close' => false,
    'encrypt' => true,              // 🔒 세션 데이터 암호화
    'http_only' => true,            // 🔒 XSS 방지
    'same_site' => 'strict',        // 🔒 CSRF 방지
    'secure' => env('SESSION_SECURE_COOKIE', true), // 🔒 HTTPS only

    // 세션 가비지 컬렉션
    'lottery' => [2, 100],

    // 세션 쿠키 이름
    'cookie' => env(
        'SESSION_COOKIE',
        Str::slug(env('APP_NAME', 'laravel'), '_').'_session'
    ),
];
```

---

## 🔐 2단계: 인증 가드 변경

### 2.1 Auth 설정

```php
// config/auth.php
'guards' => [
    'web' => [
        'driver' => 'session',
        'provider' => 'users',
    ],

    'api' => [
        'driver' => 'session',  // Sanctum → Session 변경
        'provider' => 'users',
    ],
],
```

---

## 🚪 3단계: 로그인 컨트롤러 수정

### 3.1 기존 코드 (토큰 기반)

```php
// ❌ 제거할 코드
public function login(Request $request)
{
    // JWT 토큰 발급
    $token = auth()->attempt($credentials);

    return response()->json([
        'access_token' => $token,
        'refresh_token' => $refreshToken,
        'token_type' => 'bearer',
        'expires_in' => 7200,
    ]);
}
```

### 3.2 새로운 코드 (세션 기반)

```php
// ✅ 새로운 로그인 로직
namespace App\Http\Controllers\Auth;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;

class LoginController extends Controller
{
    public function login(Request $request)
    {
        // 입력 검증
        $credentials = $request->validate([
            'user_id' => 'required|string',
            'user_pwd' => 'required|string',
        ]);

        // 🔒 세션 기반 인증
        if (Auth::attempt([
            'user_id' => $credentials['user_id'],
            'password' => $credentials['user_pwd']
        ], $request->filled('remember'))) {

            // 🔒 세션 재생성 (세션 고정 공격 방지)
            $request->session()->regenerate();

            // 🔒 보안 정보 저장 (하이재킹 감지용)
            session([
                'ip_address' => $request->ip(),
                'user_agent' => $request->userAgent(),
                'login_at' => now()->toDateTimeString(),
            ]);

            // 🔒 동시 로그인 제한 (옵션)
            $this->limitConcurrentSessions(Auth::user());

            // 사용자 정보 반환 (토큰 없음!)
            return response()->json([
                'message' => 'Login successful',
                'user' => [
                    'id' => Auth::user()->id,
                    'user_id' => Auth::user()->user_id,
                    'name' => Auth::user()->name,
                    'email' => Auth::user()->email,
                    'phone' => Auth::user()->phone,
                ],
                'tenant' => Auth::user()->tenant,
                'menus' => Auth::user()->menus,
                'roles' => Auth::user()->roles,
            ]);
        }

        // 인증 실패
        return response()->json([
            'error' => 'Invalid credentials'
        ], 401);
    }

    /**
     * 🔒 동시 로그인 제한 (1계정 1세션)
     */
    protected function limitConcurrentSessions($user)
    {
        // 현재 세션 ID 제외하고 모든 세션 삭제
        DB::table('sessions')
            ->where('user_id', $user->id)
            ->where('id', '!=', session()->getId())
            ->delete();
    }
}
```

---

## 🚪 4단계: 로그아웃 컨트롤러 수정

```php
// app/Http/Controllers/Auth/LogoutController.php
namespace App\Http\Controllers\Auth;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class LogoutController extends Controller
{
    public function logout(Request $request)
    {
        // 🔒 세션 무효화
        Auth::logout();

        // 🔒 세션 데이터 삭제
        $request->session()->invalidate();

        // 🔒 CSRF 토큰 재생성
        $request->session()->regenerateToken();

        return response()->json([
            'message' => 'Logged out successfully'
        ]);
    }
}
```

---

## 🛡️ 5단계: 세션 하이재킹 감지 미들웨어

### 5.1 미들웨어 생성

```bash
php artisan make:middleware DetectSessionHijacking
```

### 5.2 미들웨어 코드

```php
// app/Http/Middleware/DetectSessionHijacking.php
namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;

class DetectSessionHijacking
{
    /**
     * 세션 하이재킹 감지 및 차단
     */
    public function handle(Request $request, Closure $next)
    {
        if (Auth::check()) {
            $user = Auth::user();

            // 🔒 IP 주소 변경 감지
            if (session('ip_address') && session('ip_address') !== $request->ip()) {
                Log::warning('Session hijacking detected: IP changed', [
                    'user_id' => $user->id,
                    'old_ip' => session('ip_address'),
                    'new_ip' => $request->ip(),
                ]);

                // 세션 파괴 및 로그아웃
                Auth::logout();
                $request->session()->invalidate();
                $request->session()->regenerateToken();

                return response()->json([
                    'error' => 'Session security violation detected',
                    'code' => 'SESSION_HIJACKED',
                    'message' => 'Your session has been terminated for security reasons.'
                ], 401);
            }

            // 🔒 User-Agent 변경 감지
            if (session('user_agent') && session('user_agent') !== $request->userAgent()) {
                Log::warning('Session hijacking detected: User-Agent changed', [
                    'user_id' => $user->id,
                    'old_ua' => session('user_agent'),
                    'new_ua' => $request->userAgent(),
                ]);

                Auth::logout();
                $request->session()->invalidate();
                $request->session()->regenerateToken();

                return response()->json([
                    'error' => 'Session security violation detected',
                    'code' => 'SESSION_HIJACKED'
                ], 401);
            }
        }

        return $next($request);
    }
}
```

### 5.3 미들웨어 등록

```php
// app/Http/Kernel.php
protected $middlewareGroups = [
    'api' => [
        \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
        'throttle:api',
        \Illuminate\Routing\Middleware\SubstituteBindings::class,
        \App\Http\Middleware\DetectSessionHijacking::class, // ✅ 추가
    ],
];
```

---

## 🌐 6단계: CORS 설정 (중요!)

### 6.1 CORS 설정 파일

```php
// config/cors.php
return [
    'paths' => ['api/*', 'sanctum/csrf-cookie'],

    'allowed_methods' => ['*'],

    'allowed_origins' => [
        'http://localhost:3000',           // 개발 환경
        'https://yourdomain.com',          // 프로덕션
        'https://app.yourdomain.com',      // 프로덕션 앱
    ],

    'allowed_origins_patterns' => [],

    'allowed_headers' => ['*'],

    'exposed_headers' => [],

    'max_age' => 0,

    'supports_credentials' => true,  // ✅ 세션 쿠키 전송 허용 (필수!)
];
```

---

## 🗑️ 7단계: 토큰 관련 코드 제거

### 7.1 삭제할 엔드포인트

```php
// routes/api.php

// ❌ 삭제: 토큰 갱신 엔드포인트 (세션은 자동 갱신)
// Route::post('/refresh', [TokenController::class, 'refresh']);
```

### 7.2 삭제할 컨트롤러

```bash
# ❌ 삭제 또는 주석 처리
# app/Http/Controllers/Auth/TokenRefreshController.php
```

---

## ✅ 8단계: 세션 확인 엔드포인트 추가

```php
// routes/api.php
Route::get('/auth/check', [AuthController::class, 'check']);
```

```php
// app/Http/Controllers/Auth/AuthController.php
public function check(Request $request)
{
    if (Auth::check()) {
        return response()->json([
            'authenticated' => true,
            'user' => [
                'id' => Auth::user()->id,
                'name' => Auth::user()->name,
                'email' => Auth::user()->email,
            ]
        ]);
    }

    return response()->json([
        'authenticated' => false
    ]);
}
```

---

## 🧪 9단계: 테스트

### 9.1 로그인 테스트

```bash
curl -X POST http://localhost:8000/api/v1/login \
  -H "Content-Type: application/json" \
  -H "X-API-KEY: your-api-key" \
  -d '{"user_id": "test", "user_pwd": "password"}' \
  -c cookies.txt  # 쿠키 저장

# 응답:
# {
#   "message": "Login successful",
#   "user": {...},
#   "tenant": {...}
# }
#
# Set-Cookie: laravel_session=abc123...
```

### 9.2 세션 확인 테스트

```bash
curl -X GET http://localhost:8000/api/v1/auth/check \
  -H "X-API-KEY: your-api-key" \
  -b cookies.txt  # 저장된 쿠키 사용

# 응답:
# {
#   "authenticated": true,
#   "user": {...}
# }
```

### 9.3 로그아웃 테스트

```bash
curl -X POST http://localhost:8000/api/v1/logout \
  -H "X-API-KEY: your-api-key" \
  -b cookies.txt

# 응답:
# {
#   "message": "Logged out successfully"
# }
```

### 9.4 세션 하이재킹 감지 테스트

```bash
# 1. 로그인 (IP: A)
curl -X POST http://localhost:8000/api/v1/login \
  -H "X-API-KEY: your-api-key" \
  -d '{"user_id": "test", "user_pwd": "password"}' \
  -c cookies.txt

# 2. 다른 IP에서 같은 세션 ID 사용 시도 (IP: B)
# → 자동 차단되어야 함
```

---

## 🔒 10단계: 추가 보안 강화 (옵션)

### 10.1 Rate Limiting (무차별 대입 공격 방지)

```php
// routes/api.php
Route::middleware(['throttle:5,1'])->group(function () {
    Route::post('/login', [LoginController::class, 'login']);
});

// 5번 시도 후 1분 대기
```

### 10.2 세션 활동 로그

```php
// app/Models/SessionLog.php 생성
Schema::create('session_logs', function (Blueprint $table) {
    $table->id();
    $table->unsignedBigInteger('user_id');
    $table->string('ip_address');
    $table->text('user_agent');
    $table->timestamp('login_at');
    $table->timestamp('logout_at')->nullable();
    $table->timestamps();
});
```

```php
// 로그인 시 기록
SessionLog::create([
    'user_id' => Auth::id(),
    'ip_address' => $request->ip(),
    'user_agent' => $request->userAgent(),
    'login_at' => now(),
]);
```

### 10.3 관리자 강제 로그아웃 기능

```php
// app/Http/Controllers/Admin/SessionController.php
public function forceLogout(Request $request, $userId)
{
    // 특정 사용자의 모든 세션 삭제
    DB::table('sessions')
        ->where('user_id', $userId)
        ->delete();

    return response()->json([
        'message' => 'User sessions terminated'
    ]);
}
```

---

## 📊 마이그레이션 체크리스트

### 필수 작업

- [ ] `.env` 파일 세션 드라이버 설정
- [ ] `config/session.php` 보안 설정 적용
- [ ] `config/auth.php` 가드를 세션으로 변경
- [ ] 로그인 컨트롤러 수정 (토큰 제거, 세션 사용)
- [ ] 로그아웃 컨트롤러 수정 (세션 무효화)
- [ ] `config/cors.php`에서 `supports_credentials: true` 설정
- [ ] 세션 하이재킹 감지 미들웨어 추가
- [ ] `/api/v1/refresh` 엔드포인트 삭제
- [ ] `/api/v1/auth/check` 엔드포인트 추가

### 권장 작업

- [ ] Rate Limiting 적용
- [ ] 세션 활동 로그 테이블 생성
- [ ] 관리자 강제 로그아웃 기능 구현
- [ ] 동시 로그인 제한 적용

### 테스트

- [ ] 로그인 → 세션 생성 확인
- [ ] 로그아웃 → 세션 파괴 확인
- [ ] 세션 하이재킹 감지 테스트
- [ ] CORS 크로스 도메인 테스트
- [ ] 동시 로그인 제한 테스트

---

## 🚨 주의사항

### 1. 세션 저장소 (Redis) 필수

```bash
# Redis 설치 확인
redis-cli ping
# 응답: PONG

# Redis 접속 테스트
redis-cli
> KEYS *session*
```

### 2. CORS 설정 필수

- `supports_credentials: true` 반드시 설정
- 프론트엔드 도메인을 `allowed_origins`에 추가
- `*` (와일드카드) 사용 불가 (credentials와 충돌)

### 3. HTTPS 필수 (프로덕션)

```bash
# .env
SESSION_SECURE_COOKIE=true  # HTTPS만 쿠키 전송
```

### 4. 세션 쿠키 이름 확인

```php
// config/session.php
'cookie' => 'laravel_session',  // 프론트엔드에서 이 이름 사용
```

---

## 📞 프론트엔드 팀 공유 사항

### API 변경 사항

**로그인 응답 변경**:
```json
// ❌ 이전 (토큰 반환)
{
  "access_token": "eyJhbG...",
  "refresh_token": "eyJhbG...",
  "token_type": "bearer",
  "expires_in": 7200
}

// ✅ 이후 (토큰 없음, 세션 쿠키만)
{
  "message": "Login successful",
  "user": {...},
  "tenant": {...}
}

// Set-Cookie: laravel_session=abc123...
```

**필수 요구사항**:
- 모든 API 호출에 `credentials: 'include'` 추가
- 세션 쿠키를 자동으로 포함하여 전송
- `/api/auth/refresh` 엔드포인트 사용 중단

---

## 🎯 완료 후 확인사항

1. ✅ 로그인 시 세션 쿠키 생성
2. ✅ 로그아웃 시 즉시 접근 차단
3. ✅ IP 변경 시 자동 차단
4. ✅ User-Agent 변경 시 자동 차단
5. ✅ 관리자 강제 로그아웃 작동
6. ✅ Redis에 세션 데이터 저장 확인

---

## 📚 참고 자료

- [Laravel Session 공식 문서](https://laravel.com/docs/session)
- [Laravel Authentication 공식 문서](https://laravel.com/docs/authentication)
- [Redis Session Driver](https://laravel.com/docs/redis)

---

**작성일**: 2025-11-12
**작성자**: Claude Code
**버전**: 1.0