세부 항목: - 인증 및 미들웨어 구현 가이드 - 품목 관리 마이그레이션 가이드 - API 분석 및 요구사항 문서 - 대시보드 통합 완료 문서 - 브라우저 호환성 및 쿠키 처리 가이드 - Next.js 15 마이그레이션 참고 문서 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
14 KiB
14 KiB
JWT + Cookie + Middleware 인증 설계 (최종)
확정된 API 정보:
- 인증 방식: Bearer Token (JWT)
- 로그인:
POST /api/v1/login - 응답:
{ token: "xxx" } - Token 저장: 쿠키 (Middleware 접근 가능)
✅ 핵심 발견
JWT도 쿠키에 저장하면 Middleware에서 처리 가능합니다!
// middleware.ts에서 JWT 토큰 쿠키 접근
const authToken = request.cookies.get('auth_token'); // ✅ 가능!
if (!authToken) {
redirect('/login');
}
따라서 기존 Middleware 설계를 거의 그대로 사용할 수 있습니다.
📋 아키텍처 (기존과 동일)
┌─────────────────────────────────────────────────────────────┐
│ Next.js Frontend │
├─────────────────────────────────────────────────────────────┤
│ Middleware (Server) │
│ ├─ Bot Detection (기존) │
│ ├─ Authentication Check (신규) │
│ │ ├─ JWT Token 쿠키 확인 │
│ │ └─ 없으면 /login 리다이렉트 │
│ └─ i18n Routing (기존) │
├─────────────────────────────────────────────────────────────┤
│ JWT Client (lib/auth/jwt-client.ts) │
│ ├─ Token을 쿠키에 저장 │
│ ├─ API 호출 시 Authorization 헤더 추가 │
│ └─ 401 응답 시 자동 로그아웃 │
├─────────────────────────────────────────────────────────────┤
│ Auth Context (contexts/AuthContext.tsx) │
│ ├─ 사용자 정보 관리 │
│ └─ login/logout 함수 │
└─────────────────────────────────────────────────────────────┘
↓ HTTP + Cookie + Authorization
┌─────────────────────────────────────────────────────────────┐
│ Laravel Backend │
├─────────────────────────────────────────────────────────────┤
│ JWT Middleware │
│ └─ Bearer Token 검증 │
├─────────────────────────────────────────────────────────────┤
│ API Endpoints │
│ ├─ POST /api/v1/login → { token: "xxx" } │
│ ├─ POST /api/v1/register │
│ ├─ GET /api/v1/user │
│ └─ POST /api/v1/logout │
└─────────────────────────────────────────────────────────────┘
🔐 인증 플로우
1. 로그인
1. POST /api/v1/login
→ { token: "eyJhbGci..." }
2. Token을 쿠키에 저장
document.cookie = 'auth_token=xxx; Secure; SameSite=Strict'
3. /dashboard 리다이렉트
4. Middleware가 쿠키 확인 ✓
5. 페이지 렌더링
2. API 호출
1. 쿠키에서 Token 읽기
2. Authorization 헤더에 추가
Authorization: Bearer xxx
3. Laravel이 JWT 검증
4. 데이터 반환
3. 보호된 페이지 접근
사용자 → /dashboard
↓
Middleware 실행
↓
auth_token 쿠키 확인
↓
있음 → 페이지 표시
없음 → /login 리다이렉트
🛠️ 핵심 구현
1. Token 저장 (lib/auth/token-storage.ts)
export const tokenStorage = {
/**
* JWT를 쿠키에 저장
* - Middleware에서 접근 가능
* - Secure + SameSite로 보안 강화
*/
set(token: string): void {
const maxAge = 86400; // 24시간
document.cookie = `auth_token=${token}; path=/; max-age=${maxAge}; SameSite=Strict; Secure`;
},
/**
* 쿠키에서 Token 읽기
* - 클라이언트에서만 사용
*/
get(): string | null {
if (typeof window === 'undefined') return null;
const match = document.cookie.match(/auth_token=([^;]+)/);
return match ? match[1] : null;
},
/**
* Token 삭제
*/
remove(): void {
document.cookie = 'auth_token=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT';
}
};
2. JWT Client (lib/auth/jwt-client.ts)
import { tokenStorage } from './token-storage';
class JwtClient {
private baseURL = 'https://api.5130.co.kr';
/**
* 로그인
*/
async login(email: string, password: string): Promise<User> {
const response = await fetch(`${this.baseURL}/api/v1/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
if (!response.ok) {
throw new Error('Login failed');
}
const { token } = await response.json();
// ✅ Token을 쿠키에 저장
tokenStorage.set(token);
// 사용자 정보 조회
return await this.getCurrentUser();
}
/**
* 현재 사용자 정보
*/
async getCurrentUser(): Promise<User> {
const token = tokenStorage.get();
if (!token) {
throw new Error('No token');
}
const response = await fetch(`${this.baseURL}/api/v1/user`, {
headers: {
'Authorization': `Bearer ${token}`, // ✅ Authorization 헤더
},
});
if (response.status === 401) {
tokenStorage.remove();
throw new Error('Unauthorized');
}
return await response.json();
}
/**
* 로그아웃
*/
async logout(): Promise<void> {
const token = tokenStorage.get();
if (token) {
await fetch(`${this.baseURL}/api/v1/logout`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
},
});
}
// ✅ 쿠키 삭제
tokenStorage.remove();
}
}
export const jwtClient = new JwtClient();
3. Middleware (middleware.ts) - 기존과 거의 동일!
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import createIntlMiddleware from 'next-intl/middleware';
import { locales, defaultLocale } from '@/i18n/config';
const intlMiddleware = createIntlMiddleware({
locales,
defaultLocale,
localePrefix: 'as-needed',
});
// 보호된 라우트
const PROTECTED_ROUTES = [
'/dashboard',
'/profile',
'/settings',
'/admin',
'/tenant',
'/users',
'/reports',
];
// 공개 라우트
const PUBLIC_ROUTES = [
'/',
'/login',
'/register',
'/about',
'/contact',
];
function isProtectedRoute(pathname: string): boolean {
return PROTECTED_ROUTES.some(route => pathname.startsWith(route));
}
function isPublicRoute(pathname: string): boolean {
return PUBLIC_ROUTES.some(route => pathname === route || pathname.startsWith(route));
}
function stripLocale(pathname: string): string {
for (const locale of locales) {
if (pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`) {
return pathname.slice(`/${locale}`.length) || '/';
}
}
return pathname;
}
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// 1. Bot Detection (기존 로직)
// ... bot check code ...
// 2. 정적 파일 제외
if (
pathname.includes('/_next/') ||
pathname.includes('/api/') ||
pathname.match(/\.(ico|png|jpg|jpeg|svg|gif|webp)$/)
) {
return intlMiddleware(request);
}
// 3. 로케일 제거
const pathnameWithoutLocale = stripLocale(pathname);
// 4. ✅ JWT Token 쿠키 확인
const authToken = request.cookies.get('auth_token');
const isAuthenticated = !!authToken;
// 5. 보호된 라우트 체크
if (isProtectedRoute(pathnameWithoutLocale) && !isAuthenticated) {
const url = new URL('/login', request.url);
url.searchParams.set('redirect', pathname);
return NextResponse.redirect(url);
}
// 6. 게스트 전용 라우트 (이미 로그인한 경우)
if (
(pathnameWithoutLocale === '/login' || pathnameWithoutLocale === '/register') &&
isAuthenticated
) {
return NextResponse.redirect(new URL('/dashboard', request.url));
}
// 7. i18n 미들웨어
return intlMiddleware(request);
}
export const config = {
matcher: [
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp|ico)$).*)',
],
};
변경 사항:
- const sessionCookie = request.cookies.get('laravel_session');
+ const authToken = request.cookies.get('auth_token');
거의 동일합니다!
4. Auth Context (contexts/AuthContext.tsx)
'use client';
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import { jwtClient } from '@/lib/auth/jwt-client';
import { useRouter } from 'next/navigation';
interface User {
id: number;
name: string;
email: string;
}
interface AuthContextType {
user: User | null;
loading: boolean;
login: (email: string, password: string) => Promise<void>;
logout: () => Promise<void>;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const router = useRouter();
// 초기 로드 시 사용자 정보 가져오기
useEffect(() => {
jwtClient.getCurrentUser()
.then(setUser)
.catch(() => setUser(null))
.finally(() => setLoading(false));
}, []);
const login = async (email: string, password: string) => {
const user = await jwtClient.login(email, password);
setUser(user);
router.push('/dashboard');
};
const logout = async () => {
await jwtClient.logout();
setUser(null);
router.push('/login');
};
return (
<AuthContext.Provider value={{ user, loading, login, logout }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
}
📊 세션 쿠키 vs JWT 쿠키 비교
| 항목 | 세션 쿠키 (Sanctum) | JWT 쿠키 (현재) |
|---|---|---|
| 쿠키 이름 | laravel_session |
auth_token |
| Middleware 접근 | ✅ 가능 | ✅ 가능 |
| 인증 체크 | 쿠키 존재 확인 | 쿠키 존재 확인 |
| API 호출 | 쿠키 자동 포함 | Authorization 헤더 |
| CSRF 토큰 | ✅ 필요 | ❌ 불필요 |
| 서버 상태 | Stateful (세션 저장) | Stateless |
| 보안 | HTTP-only 가능 | Secure + SameSite |
| 구현 복잡도 | 동일 | 동일 |
결론: Middleware 관점에서는 거의 동일합니다!
🎯 구현 순서
Phase 1: 기본 인프라 (30분)
- auth-config.ts
- token-storage.ts
- jwt-client.ts
- types/auth.ts
Phase 2: Middleware 통합 (20분)
- middleware.ts 업데이트
- JWT 토큰 쿠키 체크
- Protected routes 가드
Phase 3: Auth Context (20분)
- AuthContext.tsx
- layout.tsx에 AuthProvider 추가
Phase 4: 로그인 페이지 (40분)
- /login/page.tsx
- LoginForm 컴포넌트
- Form validation (react-hook-form + zod)
Phase 5: 테스트 (30분)
- 로그인 → 대시보드
- 비로그인 → 대시보드 → /login 튕김
- 로그아웃 → 다시 튕김
총 소요시간: 약 2시간 20분
✅ 최종 정리
핵심 포인트
- JWT를 쿠키에 저장 → Middleware 접근 가능
- 기존 Middleware 설계 유지 → 가드 컴포넌트 불필요
- 차이점은 미미함:
- 쿠키 이름:
laravel_session→auth_token - CSRF 토큰 불필요
- API 호출 시 Authorization 헤더 추가
- 쿠키 이름:
장점
- ✅ Middleware에서 서버사이드 인증 체크
- ✅ 클라이언트 가드 컴포넌트 불필요
- ✅ 중복 코드 제거
- ✅ 기존 설계(authentication-design.md) 거의 그대로 사용
변경 사항
최소한의 변경만 필요:
// 1. Token 저장: 쿠키 사용
tokenStorage.set(token);
// 2. Middleware: 쿠키 이름만 변경
const authToken = request.cookies.get('auth_token');
// 3. API 호출: Authorization 헤더 추가
headers: { 'Authorization': `Bearer ${token}` }
// 4. CSRF 토큰: 제거
// getCsrfToken() 불필요
🚀 다음 단계
- ✅ 설계 확정 완료
- ⏳ 디자인 컴포넌트 대기
- ⏳ 백엔드 API 엔드포인트 확인
- POST /api/v1/register
- GET /api/v1/user
- POST /api/v1/logout
- 🚀 구현 시작 (2-3시간)
준비되면 바로 시작합니다! 🎯