Files
cloud-sharp-docs/설계/pipeline/다운로드 파이프라인.md
2026-03-18 17:53:41 +09:00

284 lines
10 KiB
Markdown

## 개요
이 문서는 **인증 사용자**와 **공유 링크 사용자** 모두를 지원하는 파일 다운로드 시스템의 전체 흐름을 정의한다. 핵심 구조는 다음과 같다:
> **권한 검증 → 다운로드 가능 상태 사전 검증 → 다운로드 세션 발급 → 최종 경로 검증 및 스트리밍**
---
## 전체 흐름 (8단계)
![[download_flowchart_part1.svg]]
![[download_flowchart_part2.svg]]
---
## 단계별 상세
### 1단계: 사용자 인증 / 공유 링크 접근
**목적:** 요청자의 신원을 확인하고 내부 컨텍스트로 정규화한다.
| 접근 경로 | 인증 방식 | 정규화 결과 |
|-----------|-----------|-------------|
| 인증 사용자 | `Authorization` 헤더의 JWT Bearer 토큰 | `subjectType=user`, `subjectId={userId}` |
| 공유 링크 사용자 | 쿼리 파라미터 또는 별도 엔드포인트의 `share_token` | `subjectType=share_link`, `subjectId={shareLinkId}` |
**에러 처리:**
| 상황 | 응답 |
|------|------|
| 인증 토큰 유효하지 않음 / 만료 | `401 Unauthorized` |
| 공유 토큰 유효하지 않음 | `404 Not Found` |
---
### 2단계: 파일 권한 / 공유 상태 검증
**목적:** DB 조회를 통해 요청자가 해당 파일에 접근할 수 있는지 확인한다.
**검증 항목:**
| 접근 유형 | 검증 내용 |
| --------- | ---------------------------------------- |
| 인증 사용자 | 파일 소유자 여부, 공유 대상 여부, 조직/프로젝트/폴더 단위 접근 권한 |
| 공유 링크 사용자 | 링크 활성화 여부, 만료 여부, 비밀번호 검증, 다운로드 허용 여부 |
**핵심 원칙:** 파일 존재 여부 노출을 최소화하기 위해 아래 경우는 **모두 `404 Not Found`** 로 처리한다.
- 파일이 실제로 존재하지 않음
- 요청자에게 접근 권한이 없음
- 공유 링크가 파일에 매핑되지 않음
- 비활성화된 공유 링크로 접근 시도
> 외부 클라이언트 관점에서 **"접근 불가"와 "존재하지 않음"을 구분할 수 없다.**
---
### 3단계: 파일 메타데이터 및 다운로드 가능 상태 검증
**목적:** 다운로드가 불가능한 파일에 대해 세션을 발급하지 않기 위한 **사전 검증**이다.
**검증 항목:**
- 파일 메타데이터 존재 여부
- `storage_key` 존재 여부
- 삭제 / 손상 / 비활성 / 격리 상태 여부
- 다운로드 정책상 차단 대상 여부
- (가능 시) 실제 스토리지 객체 존재 여부
**실패 시:** `404 Not Found`
> ⚠️ 이 단계는 사전 검증이다. 세션 발급 후 실제 스트리밍 시점까지 파일 상태가 변할 수 있으므로, **최종 검증은 6단계에서 다시 수행**한다.
---
### 4단계: 다운로드 세션 토큰 발급
**목적:** 실제 파일 스트리밍에 사용할 세션 토큰을 발급한다.
**세션 토큰 속성:**
| 항목 | 값 |
|------|-----|
| TTL | 약 5분 |
| 포함 정보 | 파일 ID, 요청자 식별자(`subjectType`, `subjectId`), 세션 ID, 만료 시각 |
| 보호 방식 | HMAC 또는 JWT 서명, 필요 시 Redis/DB에 메타데이터 저장 |
**세션이 지원하는 요청 유형:**
- 브라우저의 동일 다운로드 재요청
- `Range` 기반 이어받기(resume)
- 네트워크 끊김 후 재시도
- 일부 브라우저/다운로드 매니저의 병렬 Range 요청
**응답 예시:**
```json
{
"downloadUrl": "/files/stream?sessionToken={download_session_token}",
"expiresAt": "2026-03-17T12:00:00Z"
}
```
**리스크 및 대응:**
| 리스크 | 대응 |
|--------|------|
| TTL이 너무 길면 URL 탈취 악용 가능 | TTL 5분 내외로 제한 |
| TTL이 너무 짧으면 대용량 파일 재요청 실패 | 세션 TTL 내 반복 요청 허용 |
| 토큰 유출 | 파일 ID + 요청자 식별자에 바인딩, 선택적 IP/UA 바인딩 |
**추가 보안:**
- 다운로드 세션은 **발급 시점 권한**을 기준으로 TTL 동안 유효
- Access log 마스킹
- `Cache-Control: private, no-store`
---
### 5단계: 세션 토큰으로 파일 스트림 요청
**클라이언트 요청 예시:**
```http
GET /files/stream?sessionToken={download_session_token}
Range: bytes=0-1048575
```
**처리 규칙:**
| 상황 | 처리 |
|------|------|
| 세션 토큰 만료 / 위변조 | `401 Unauthorized` |
| 세션이 가리키는 파일과 요청 대상 불일치 | `404 Not Found` |
| 세션 TTL 내 반복 GET / Range / resume | ✅ 허용 |
| Multi-range 요청 | ❌ 미지원 (정책에 따라 `416` 또는 전체 응답) |
**설계 원칙:**
> 세션 자체는 짧게 유지하되, 세션이 살아있는 동안은 브라우저 다운로드 모델을 수용한다.
**"1회용 토큰 + 즉시 무효화" 방식을 사용하지 않는 이유:**
브라우저는 다운로드 중 동일 URL로 재요청, Range resume, 병렬 요청, 프록시 중복 요청 등을 발생시킬 수 있어 충돌한다.
---
### 6단계: `storage_key` → 실제 파일 경로 resolve 및 최종 검증
**목적:** 스트림을 열기 직전에 실제 파일 경로를 확정하고 최종 검증을 수행한다.
**처리 절차:**
1. 키-경로 맵(DB 또는 설정) 조회 → 절대 경로 획득
2. `realpath()` 등으로 경로 정규화
3. 정규화된 경로가 허용된 루트 디렉터리 내부인지 검증 (예: `/data/uploads/`)
4. 실제 파일 존재 여부 확인
5. 파일 open 가능 여부 확인
**보안 요구사항:**
- Path traversal 방어 필수 (`../`, 심볼릭 링크 우회, 상대 경로 등 차단)
- 허용 루트 외부로 벗어나면 즉시 실패 처리
**에러 처리:**
| 상황 | 응답 | 내부 처리 |
|------|------|-----------|
| 실제 파일 미존재 | `404 Not Found` | - |
| 메타데이터 존재하나 스토리지 파일 누락 | `404 Not Found` | 경고 로그 / 장애 알림 |
---
### 7단계: 파일 스트림 응답
**기본 응답 헤더:**
| 헤더 | 값 |
|------|-----|
| `Content-Type` | 서버가 결정한 안전한 MIME 타입 |
| `Content-Disposition` | `attachment; filename="..."; filename*=UTF-8''...` |
| `Accept-Ranges` | `bytes` |
| `Content-Length` | 전체 또는 부분 응답 크기 |
| `ETag` | 파일 버전 식별자 |
| `Last-Modified` | 파일 최종 수정 시각 (선택) |
**Range 처리:**
| 조건 | 응답 |
|------|------|
| `Range` 헤더 없음 | `200 OK` (전체 파일) |
| `Range` 헤더 있음 | `206 Partial Content` (부분 응답) |
| 범위 초과 | `416 Range Not Satisfiable` |
| `If-Range` 일치 | 부분 응답 |
| `If-Range` 불일치 | 전체 파일 재전송 또는 정책에 따라 처리 |
**MIME 타입 처리 원칙:**
1. 서버 측 MIME 판별 사용 (클라이언트 업로드 값 불신)
2. 저장된 메타데이터와 확장자는 보조적으로 사용
3. 판별 불확실 시 `application/octet-stream`
4. XSS 가능 타입이라도 `Content-Disposition: attachment`로 강제
**파일명 처리 원칙:**
- 제거/이스케이프 대상: `"`, `\r`, `\n`, `\0`, `/`, `\`
- 최대 길이 제한 적용
- 비 ASCII 문자는 `filename*`에 RFC 5987 방식으로 인코딩
```http
Content-Disposition: attachment; filename="safe_name.ext"; filename*=UTF-8''original%20name.ext
```
**성능 / 운영 보호 정책:**
- 사용자별 동시 다운로드 수 제한
- 공유 링크별 rate limit
- 전체 서버 동시 스트림 수 제한
- 대용량 파일 전송 속도 제한 (필요 시)
- `sendfile` / `X-Accel-Redirect` / `X-Sendfile` 활용 검토
### 8단계: 로그 기록 / 카운트 증가
**카운트 증가 정책:**
- **공유 링크 다운로드에만 적용**
- `download_count`는 **세션 단위 전체 파일 다운로드 완료 시점**에만 증가
- 운영 분석을 위해 시도 수와 완료 수를 분리 집계 가능
| 카운트 항목 | 증가 시점 | 용도 |
|------------|-----------|------|
| `download_attempt_count` | 스트림 요청 시작 시점 | 시도 횟수 추적 |
| `download_completed_count` | 전체 파일 전송 완료 시점 | 실제 완료 횟수 추적 |
> 분리 집계를 통해 네트워크 중단, 사용자 취소, 실패율 등을 더 정확히 분석할 수 있다.
---
## 에러 응답 정리
| 상황 | HTTP 상태 코드 |
|------|----------------|
| 인증 실패 / 인증 토큰 만료 | `401 Unauthorized` |
| 다운로드 세션 토큰 만료 / 위변조 | `401 Unauthorized` |
| 파일 없음 또는 권한 없음 | `404 Not Found` |
| Range 범위 초과 | `416 Range Not Satisfiable` |
| 정상 전체 응답 | `200 OK` |
| 정상 부분 응답 (Range) | `206 Partial Content` |
---
## 보안 체크리스트
| # | 항목 | 관련 단계 |
|---|------|-----------|
| 1 | JWT / share token 서명 검증 | 1단계 |
| 2 | 파일 접근 권한 및 공유 상태 검증 | 2단계 |
| 3 | 권한 없음과 파일 없음은 외부에 동일하게 `404` 처리 | 2단계 |
| 4 | 파일 메타데이터 및 다운로드 가능 상태 사전 검증 | 3단계 |
| 5 | 다운로드 세션 토큰 단명 발급 (권장 TTL: 약 5분) | 4단계 |
| 6 | 세션 토큰은 파일 ID 및 요청자 식별자에 바인딩 | 4단계 |
| 7 | 세션 TTL 내 GET / Range / resume / 병렬 요청 허용 | 5단계 |
| 8 | Path traversal 방어 (`realpath` + 루트 경로 검증) | 6단계 |
| 9 | 실제 스트리밍 직전 파일 존재 및 open 가능 여부 재검증 | 6단계 |
| 10 | `Content-Disposition: attachment` 강제 | 7단계 |
| 11 | 파일명 sanitize 및 `filename*` 지원 | 7단계 |
| 12 | MIME 타입은 서버 기준으로 안전하게 결정 | 7단계 |
| 13 | `ETag` / `If-Range` 지원 | 7단계 |
| 14 | 다운로드 로그 및 감사 로그 기록 | 8단계 |
| 15 | 동시 다운로드 수 / rate limit / 대역폭 보호 정책 적용 | 7~8단계 |
---
## 설계 요약
이 설계는 다음 **6가지 목표**를 동시에 만족한다.
| 목표 | 달성 방법 |
|------|-----------|
| **파일 존재 여부 노출 최소화** | 권한 없음 / 파일 없음 모두 `404` 통일 처리 |
| **권한 검증 강화** | 인증 사용자·공유 링크 모두 다단계 검증 |
| **불필요한 세션 발급 방지** | 세션 발급 전 다운로드 가능 상태 사전 검증 |
| **브라우저 호환성 확보** | 세션 TTL 내 GET / Range / resume / 병렬 요청 허용 |
| **재생 공격 위험 완화** | 단명 세션 토큰 + 파일·요청자 바인딩 |
| **운영 환경 자원 보호** | 동시 다운로드 제한, rate limit, 대역폭 제어 |