Files
sam-react-prod/claudedocs/auth/[IMPL-2025-11-10] token-management-guide.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

10 KiB

Token Management System Guide

완전한 Access Token & Refresh Token 시스템 구현 가이드

📋 목차

  1. 시스템 개요
  2. 토큰 라이프사이클
  3. API 엔드포인트
  4. 자동 토큰 갱신
  5. 사용 예시
  6. 보안 고려사항

시스템 개요

토큰 구조

{
  "access_token": "214|EU7drdTBYN1fru0MylLXwjJbi2svXcikn5ofvmTI354d09c7",
  "refresh_token": "215|6hAPWcO05jtfSDV9Yz4kLQi3qZDFuycMqrNITOV3c27bd0cb",
  "token_type": "Bearer",
  "expires_in": 7200,
  "expires_at": "2025-11-10 15:49:38"
}

저장 방식

HttpOnly 쿠키 (XSS 공격 방지):

  • access_token: 2시간 만료 (7200초)
  • refresh_token: 7일 만료 (604800초)

보안 속성:

  • HttpOnly: JavaScript 접근 불가
  • Secure: HTTPS만 전송
  • SameSite=Strict: CSRF 공격 방지

토큰 라이프사이클

1. 로그인 (Token 발급)

사용자 로그인
    ↓
POST /api/auth/login
    ↓
PHP Backend /api/v1/login
    ↓
access_token + refresh_token 발급
    ↓
HttpOnly 쿠키에 저장
    ↓
대시보드로 이동

2. 인증된 요청

보호된 페이지 접근
    ↓
Middleware 인증 체크
    ↓
access_token 존재?
    ├─ Yes → 접근 허용
    └─ No → refresh_token 확인
          ├─ 있음 → 자동 갱신 시도
          └─ 없음 → 로그인 페이지로

3. 토큰 갱신

access_token 만료 (2시간 후)
    ↓
보호된 API 호출 시도
    ↓
401 Unauthorized 응답
    ↓
POST /api/auth/refresh
    ↓
refresh_token으로 새 토큰 발급
    ↓
새 access_token + refresh_token 쿠키 업데이트
    ↓
원래 API 호출 재시도
    ↓
성공

4. 로그아웃

사용자 로그아웃
    ↓
POST /api/auth/logout
    ↓
PHP Backend /api/v1/logout (토큰 무효화)
    ↓
HttpOnly 쿠키 삭제
    ↓
로그인 페이지로 이동

API 엔드포인트

1. Login API

Endpoint: POST /api/auth/login

Request:

{
  user_id: string;
  user_pwd: string;
}

Response:

{
  message: string;
  user: UserObject;
  tenant: TenantObject | null;
  menus: MenuItem[];
  token_type: "Bearer";
  expires_in: number;
  expires_at: string;
}

쿠키 설정:

  • access_token (HttpOnly, 2시간)
  • refresh_token (HttpOnly, 7일)

2. Refresh Token API

Endpoint: POST /api/auth/refresh

쿠키 필요: refresh_token

Response (성공):

{
  message: "Token refreshed successfully";
  token_type: "Bearer";
  expires_in: number;
  expires_at: string;
}

Response (실패):

{
  error: "Token refresh failed";
  needsReauth: true;
}

쿠키 업데이트:

  • access_token (2시간)
  • refresh_token (7일)

3. Auth Check API

Endpoint: GET /api/auth/check

기능:

  1. access_token 존재 → 200 OK with authenticated: true
  2. access_token 없음 + refresh_token 있음 → 자동 갱신 시도
    • 갱신 성공 → 200 OK with authenticated: true, refreshed: true
    • 갱신 실패 → 401 Unauthorized
  3. 둘 다 없음 → 401 Unauthorized

Response:

// ✅ 인증 성공 (200)
{
  authenticated: true;
  refreshed?: boolean; // 자동 갱신 여부
}

// ❌ 인증 실패 (401)
{
  error: string; // 'Not authenticated' 또는 'Token refresh failed'
}

참고:

  • 🔵 Next.js 내부 API (PHP 백엔드 X)
  • 성능 최적화: 로컬 쿠키만 확인하여 빠른 응답

⚠️ 2025-11-27 변경사항:

  • LoginPage.tsx에서 auth/check 호출 제거됨
  • 제거 이유:
    1. 미들웨어(middleware.ts)에서 이미 동일한 처리를 함 (guestOnlyRoutes 리다이렉트)
    2. 401 응답이 Network 탭에 에러로 표시되어 백엔드 개발자 혼란 유발
    3. 불필요한 API 호출로 인한 성능 저하
  • 대체 방안: 미들웨어가 서버 사이드에서 쿠키 체크 후 리다이렉트 처리
  • 참고: src/components/auth/LoginPage.tsx 주석 참조

4. Logout API

Endpoint: POST /api/auth/logout

기능:

  1. PHP 백엔드에 로그아웃 요청 (토큰 무효화)
  2. access_token, refresh_token 쿠키 삭제

자동 토큰 갱신

1. Middleware에서 자동 갱신

src/middleware.ts:

// access_token 또는 refresh_token이 있으면 인증됨
const accessToken = request.cookies.get('access_token');
const refreshToken = request.cookies.get('refresh_token');

if ((accessToken && accessToken.value) || (refreshToken && refreshToken.value)) {
  return { isAuthenticated: true, authMode: 'bearer' };
}

2. Auth Check에서 자동 갱신

src/app/api/auth/check/route.ts:

// access_token 없고 refresh_token만 있으면 자동 갱신
if (refreshToken && !accessToken) {
  const refreshResponse = await fetch('/api/v1/refresh', {...});
  // 새 토큰을 HttpOnly 쿠키로 설정
}

3. Proxy에서 자동 갱신 ( 2025-11-27 구현)

src/app/api/proxy/[...path]/route.ts:

// 401 응답 시 자동 토큰 갱신 후 재시도
if (backendResponse.status === 401 && refreshToken) {
  const refreshResult = await refreshAccessToken(refreshToken);

  if (refreshResult.success && refreshResult.accessToken) {
    // 새 토큰으로 원래 요청 재시도
    token = refreshResult.accessToken;
    backendResponse = await executeBackendRequest(url, method, token, body, contentType);

    // 새 토큰을 쿠키에 저장
    createTokenCookies(newTokens).forEach(cookie => {
      clientResponse.headers.append('Set-Cookie', cookie);
    });
  } else {
    // 리프레시 실패 → 쿠키 삭제 후 401 반환
    return NextResponse.json({ error: 'Authentication failed', needsReauth: true }, { status: 401 });
  }
}

동작 방식:

  1. 백엔드 API 호출 (access_token 사용)
  2. 401 Unauthorized 응답 받음
  3. refresh_token으로 /api/v1/refresh 호출
  4. 성공 시: 새 토큰으로 원래 요청 재시도 + 쿠키 업데이트
  5. 실패 시: 쿠키 삭제 + needsReauth: true 응답

장점: 프론트엔드 코드 수정 없이 모든 /api/proxy/* 요청에 자동 토큰 갱신 적용

4. API Client에서 자동 갱신 (Legacy)

src/lib/api/client.ts:

// withTokenRefresh 헬퍼 함수 사용
const data = await withTokenRefresh(() =>
  apiClient.get('/protected/resource')
);

동작 방식:

  1. API 호출 시도
  2. 401 응답 받음
  3. /api/auth/refresh 호출
  4. 성공 시 원래 API 재시도
  5. 실패 시 로그인 페이지로 리다이렉트

참고: 대부분의 API 호출은 프록시를 통해 자동 갱신되므로 직접 사용할 필요 없음


사용 예시

예시 1: 보호된 페이지에서 API 호출

// src/app/[locale]/(protected)/dashboard/page.tsx
import { withTokenRefresh } from '@/lib/api/client';

export default function Dashboard() {
  const fetchData = async () => {
    try {
      // 자동 토큰 갱신 포함
      const data = await withTokenRefresh(() =>
        fetch('/api/protected/data', {
          credentials: 'include' // 쿠키 포함
        })
      );

      console.log('Data fetched:', data);
    } catch (error) {
      console.error('Fetch failed:', error);
    }
  };

  return <div>...</div>;
}

예시 2: 수동 토큰 갱신

// src/lib/auth/token-refresh.ts
import { refreshTokenClient } from '@/lib/auth/token-refresh';

async function handleProtectedAction() {
  try {
    // API 호출
    const response = await fetch('/api/protected/action');

    if (!response.ok) {
      // 401 에러 시 토큰 갱신 시도
      const refreshed = await refreshTokenClient();

      if (refreshed) {
        // 재시도
        return await fetch('/api/protected/action');
      }
    }

    return response;
  } catch (error) {
    console.error('Action failed:', error);
  }
}

예시 3: Protected Layout

// src/app/[locale]/(protected)/layout.tsx
"use client";

import { useAuthGuard } from '@/hooks/useAuthGuard';

export default function ProtectedLayout({ children }) {
  // 자동으로 /api/auth/check 호출
  // access_token 없으면 refresh_token으로 자동 갱신
  useAuthGuard();

  return <>{children}</>;
}

보안 고려사항

구현된 보안 기능

  1. HttpOnly 쿠키

    • JavaScript에서 토큰 접근 불가
    • XSS 공격으로부터 보호
  2. Secure 플래그

    • HTTPS에서만 쿠키 전송
    • 중간자 공격 방지
  3. SameSite=Strict

    • CSRF 공격 방지
    • 크로스 사이트 요청 차단
  4. 토큰 만료 시간

    • Access Token: 2시간 (짧은 수명)
    • Refresh Token: 7일 (긴 수명)
  5. 에러 메시지 일반화

    • 백엔드 상세 에러 노출 방지
    • 정보 유출 차단

⚠️ 추가 권장 사항

  1. Token Rotation

    • Refresh 시 새로운 refresh_token 발급 (현재 구현됨 )
  2. Rate Limiting

    • 로그인 시도 제한
    • Refresh 요청 제한
  3. IP 검증

    • 토큰 발급 시 IP 기록
    • 다른 IP에서 사용 시 경고
  4. Device Fingerprinting

    • 토큰 발급 디바이스 기록
    • 이상 접근 탐지
  5. Logout Blacklist

    • 로그아웃 된 토큰 블랙리스트 관리
    • 재사용 방지

트러블슈팅

문제 1: 로그인 후 바로 로그아웃됨

원인: 쿠키가 설정되지 않음

해결:

  1. 브라우저 개발자 도구 → Application → Cookies 확인
  2. access_token, refresh_token 존재 확인
  3. 없으면 /api/auth/login 응답 헤더 확인

문제 2: Token refresh 무한 루프

원인: Refresh token도 만료됨

해결:

  1. /api/auth/refresh 응답 확인
  2. 401 응답 시 로그인 페이지로 리다이렉트
  3. needsReauth: true 플래그 확인

문제 3: CORS 에러

원인: 크로스 도메인 요청 시 쿠키 전송 실패

해결:

fetch('/api/protected', {
  credentials: 'include' // 쿠키 포함
})

참고 파일

  • src/app/api/auth/login/route.ts - 로그인 API
  • src/app/api/auth/refresh/route.ts - 토큰 갱신 API
  • src/app/api/auth/check/route.ts - 인증 체크 API
  • src/app/api/auth/logout/route.ts - 로그아웃 API
  • src/middleware.ts - 인증 미들웨어
  • src/lib/auth/token-refresh.ts - 토큰 갱신 유틸리티
  • src/lib/api/client.ts - API 클라이언트 (자동 갱신)