Files
sam-react-prod/claudedocs/auth/[REF] session-migration-backend.md
byeongcheolryu 65a8510c0b fix: 품목기준관리 실시간 동기화 수정
- BOM 항목 추가/수정/삭제 시 섹션탭 즉시 반영
- 섹션 복제 시 UI 즉시 업데이트 (null vs undefined 이슈 해결)
- 항목 수정 기능 추가 (useTemplateManagement)
- 실시간 동기화 문서 추가

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-27 22:19:50 +09:00

14 KiB

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

📋 개요

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

주요 보안 개선 사항:

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

🔧 1단계: 환경 설정

1.1 세션 드라이버 설정

# .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 세션 설정 파일

// 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 설정

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

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

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

3.1 기존 코드 (토큰 기반)

// ❌ 제거할 코드
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 새로운 코드 (세션 기반)

// ✅ 새로운 로그인 로직
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단계: 로그아웃 컨트롤러 수정

// 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 미들웨어 생성

php artisan make:middleware DetectSessionHijacking

5.2 미들웨어 코드

// 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 미들웨어 등록

// 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 설정 파일

// 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 삭제할 엔드포인트

// routes/api.php

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

7.2 삭제할 컨트롤러

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

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

// routes/api.php
Route::get('/auth/check', [AuthController::class, 'check']);
// 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 로그인 테스트

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 세션 확인 테스트

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 로그아웃 테스트

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 세션 하이재킹 감지 테스트

# 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 (무차별 대입 공격 방지)

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

// 5번 시도 후 1분 대기

10.2 세션 활동 로그

// 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();
});
// 로그인 시 기록
SessionLog::create([
    'user_id' => Auth::id(),
    'ip_address' => $request->ip(),
    'user_agent' => $request->userAgent(),
    'login_at' => now(),
]);

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

// 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) 필수

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

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

2. CORS 설정 필수

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

3. HTTPS 필수 (프로덕션)

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

4. 세션 쿠키 이름 확인

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

📞 프론트엔드 팀 공유 사항

API 변경 사항

로그인 응답 변경:

// ❌ 이전 (토큰 반환)
{
  "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에 세션 데이터 저장 확인

📚 참고 자료


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