- 법인차량 관리 3개 페이지 (차량등록, 운행일지, 정비이력) - MES 데이터 정합성 분석 보고서 v1/v2 - sam-docs 프론트엔드 기술문서 v1 (9개 챕터) - claudedocs 가이드/테스트URL 업데이트
138 lines
4.1 KiB
Markdown
138 lines
4.1 KiB
Markdown
# 인증 및 API 통신
|
|
|
|
## 인증 아키텍처
|
|
|
|
SAM ERP는 **HttpOnly Cookie 기반 인증**을 사용합니다. JavaScript에서 토큰을 직접 접근할 수 없으므로, 모든 인증 API 호출은 **Next.js API Proxy**를 통해 처리됩니다.
|
|
|
|
```
|
|
클라이언트 (브라우저)
|
|
│
|
|
├── Server Action 호출 (useEffect에서)
|
|
│ └── serverFetch() → 서버에서 쿠키 읽기 → 백엔드 API 호출
|
|
│
|
|
└── 프록시 API 호출 (fetch('/api/proxy/...'))
|
|
└── API Proxy Route → 서버에서 쿠키 읽기 → 백엔드 API 호출
|
|
```
|
|
|
|
## 쿠키 구조
|
|
|
|
| 쿠키명 | HttpOnly | 용도 | Max-Age |
|
|
|--------|:--------:|------|---------|
|
|
| `access_token` | O | API 인증 토큰 | 2시간 |
|
|
| `refresh_token` | O | 토큰 갱신용 | 7일 |
|
|
| `is_authenticated` | X | 클라이언트 인증 상태 확인 | 2시간 |
|
|
|
|
- `access_token`, `refresh_token`: HttpOnly → JavaScript 접근 불가 (XSS 방지)
|
|
- `is_authenticated`: non-HttpOnly → 클라이언트에서 인증 상태 확인 가능 (FCM 등)
|
|
- 프로덕션: `Secure` 플래그 활성화 (HTTPS만)
|
|
- `SameSite=Lax`: CSRF 방지
|
|
|
|
## 인증 흐름
|
|
|
|
### 로그인
|
|
|
|
```
|
|
1. 사용자 → /api/auth/login (POST)
|
|
2. 백엔드 → access_token + refresh_token 반환
|
|
3. API Route → Set-Cookie (HttpOnly) 설정
|
|
4. 클라이언트 → /(protected)/dashboard 리다이렉트
|
|
```
|
|
|
|
### API 요청 (Server Action)
|
|
|
|
```
|
|
1. 클라이언트 → Server Action 호출
|
|
2. serverFetch() → 쿠키에서 access_token 읽기
|
|
3. authenticatedFetch() → Authorization 헤더에 토큰 추가
|
|
4. 백엔드 API 호출 → 응답 반환
|
|
```
|
|
|
|
### 토큰 만료 시 (401 자동 갱신)
|
|
|
|
```
|
|
1. API 요청 → 401 응답 수신
|
|
2. authenticatedFetch() → refresh_token으로 갱신 요청
|
|
3. 새 토큰 수신 → 쿠키 업데이트
|
|
4. 원래 요청 재시도 → 성공
|
|
5. 갱신 실패 → 쿠키 삭제 → /login 리다이렉트
|
|
```
|
|
|
|
### 토큰 갱신 중복 방지
|
|
|
|
```typescript
|
|
// globalThis 레벨 캐싱 (5초)
|
|
// 여러 요청이 동시에 401을 받아도 refresh는 1회만 실행
|
|
// 진행 중인 refresh Promise를 공유하여 대기
|
|
```
|
|
|
|
## API 프록시 (`/api/proxy/[...path]`)
|
|
|
|
클라이언트에서 직접 백엔드 API를 호출해야 하는 경우 프록시 사용:
|
|
|
|
```typescript
|
|
// 클라이언트에서 프록시 호출
|
|
const response = await fetch('/api/proxy/item-master/init');
|
|
const data = await response.json();
|
|
```
|
|
|
|
프록시 내부 동작:
|
|
1. HttpOnly 쿠키에서 `access_token` 읽기
|
|
2. 백엔드 URL 구성 (`/api/proxy/*` → 백엔드 `/*`)
|
|
3. `Authorization: Bearer {token}` 헤더 추가
|
|
4. 요청 전달 → 응답 반환
|
|
5. 401 시 자동 토큰 갱신 후 재시도
|
|
6. 새 토큰 → Set-Cookie 헤더로 클라이언트에 전달
|
|
|
|
## 인증 보호
|
|
|
|
### Protected Layout
|
|
|
|
```tsx
|
|
// (protected)/layout.tsx
|
|
export default function ProtectedLayout({ children }) {
|
|
// 인증 가드 (뒤로가기 캐시 감지)
|
|
useAuthGuard();
|
|
|
|
return (
|
|
<RootProvider>
|
|
<ApiErrorProvider> {/* 401 에러 자동 처리 */}
|
|
<FCMProvider> {/* 푸시 알림 */}
|
|
<AuthenticatedLayout>
|
|
<PermissionGate> {/* 권한 기반 접근 제어 */}
|
|
{children}
|
|
</PermissionGate>
|
|
</AuthenticatedLayout>
|
|
</FCMProvider>
|
|
</ApiErrorProvider>
|
|
</RootProvider>
|
|
);
|
|
}
|
|
```
|
|
|
|
### 인증 상태 확인 (클라이언트)
|
|
|
|
```typescript
|
|
import { hasAuthToken } from '@/lib/api/auth-headers';
|
|
|
|
// is_authenticated 쿠키 확인 (non-HttpOnly)
|
|
if (hasAuthToken()) {
|
|
// 인증됨
|
|
}
|
|
```
|
|
|
|
## 로그아웃
|
|
|
|
완전한 로그아웃 절차:
|
|
1. Zustand 스토어 초기화 (useAuthStore, useMasterDataStore, useItemMasterStore)
|
|
2. sessionStorage 캐시 삭제 (page_config_*, mes-*)
|
|
3. localStorage 사용자 데이터 삭제
|
|
4. FCM 토큰 해제 (Capacitor 환경)
|
|
5. 서버 로그아웃 API 호출
|
|
6. /login 리다이렉트
|
|
|
|
## 주의사항
|
|
|
|
- **Server Component에서 쿠키 수정 불가** → Client Component 사용 필수
|
|
- **`alert()`, `confirm()`, `prompt()` 사용 금지** → Radix UI Dialog 또는 `toast` 사용
|
|
- **API 직접 호출 금지** → 반드시 Server Action 또는 프록시 사용
|