add flows
This commit is contained in:
136
설계/flows/다운로드 흐름.md
Normal file
136
설계/flows/다운로드 흐름.md
Normal file
@@ -0,0 +1,136 @@
|
||||
|
||||
![[file_download_flow.svg]]
|
||||
## 단계별 상세
|
||||
|
||||
### 1. 사용자 인증 / 공유 링크 접근
|
||||
|
||||
요청의 진입점으로, 두 가지 접근 경로를 처리한다.
|
||||
|
||||
- **인증 사용자**: `Authorization` 헤더의 JWT Bearer 토큰을 파싱 및 검증
|
||||
- **공유 링크**: URL 쿼리 파라미터의 `share_token`을 파싱
|
||||
|
||||
두 경로 모두 이후 단계에서 동일한 인터페이스로 처리될 수 있도록 내부적으로 정규화한다.
|
||||
|
||||
---
|
||||
|
||||
### 2. 파일 권한 / 공유 상태 검증
|
||||
|
||||
DB를 조회하여 요청자가 해당 파일에 접근할 수 있는지 확인한다.
|
||||
|
||||
- 인증 사용자: 파일의 소유자 또는 공유 대상 여부 확인
|
||||
- 공유 링크: 링크의 활성화 여부, 만료 여부, 비밀번호 보호 여부 확인
|
||||
- 권한이 없으면 즉시 **`403 Forbidden`** 반환 (이후 단계로 진행하지 않음)
|
||||
|
||||
##### Risk
|
||||
권한이 없는 경우 `403`, 파일이 없는 경우 `404`를 분리 반환한다. 이 차이를 이용하면 공격자가 파일 ID를 열거하여 존재하는 파일 목록을 추론할 수 있다.
|
||||
|
||||
---
|
||||
|
||||
### 3. 다운로드 토큰 발급
|
||||
|
||||
실제 스트리밍 요청에 사용할 단명(short-lived) 토큰을 생성한다.
|
||||
|
||||
- TTL은 **약 60초** 권장 (재사용·탈취 위험 최소화)
|
||||
- 토큰에는 파일 ID, 요청자 식별자, 만료 시각을 포함
|
||||
- HMAC 서명 또는 JWT로 위변조 방지
|
||||
|
||||
```
|
||||
# Redis 기반 원자적 무효화
|
||||
result = REDIS.SET(f"dl_token:{token}", "used", NX=True, EX=60)
|
||||
# NX: key가 없을 때만 SET → 원자적 보장
|
||||
# result가 None이면 이미 사용된 토큰 → 401
|
||||
|
||||
```
|
||||
|
||||
> API 서버와 스트리밍 서버를 분리 운영할 때 특히 유용하다.
|
||||
> 클라이언트는 이 토큰을 받아 스트리밍 엔드포인트에 직접 요청한다.
|
||||
|
||||
---
|
||||
|
||||
### 4. 다운로드 토큰으로 파일 스트림 요청
|
||||
|
||||
클라이언트가 발급받은 토큰으로 스트리밍 엔드포인트에 요청한다.
|
||||
|
||||
```
|
||||
GET /files/stream?token={download_token}
|
||||
Range: bytes=0-1048575 (선택, Range 요청 시)
|
||||
```
|
||||
|
||||
- 토큰이 만료되었거나 유효하지 않으면 **`401 Unauthorized`** 반환
|
||||
- 토큰은 1회 사용 후 무효화하는 것을 권장 (재생 공격 방어)
|
||||
|
||||
#### Risk
|
||||
- "토큰은 1회 사용 후 무효화하는 것을 권장"이라고만 기술되어 있으나, 동시에 같은 토큰으로 2개 이상의 요청이 들어오는 경우의 원자적 무효화 전략이 없다.
|
||||
- 토큰을 1회 사용 후 무효화하면, 대용량 파일의 Range 기반 이어받기(resume)가 불가능해진다. 네트워크 끊김 후 클라이언트가 `Range: bytes=10485760-`으로 재요청하면, 이미 무효화된 토큰이므로 `401`이 반환된다.
|
||||
-
|
||||
---
|
||||
|
||||
### 5. storage_key → 로컬 FS 경로 resolve
|
||||
|
||||
`storage_key`를 실제 파일 시스템 경로로 변환한다.
|
||||
|
||||
- 키-경로 맵(DB 또는 설정 파일)을 조회하여 절대 경로 획득
|
||||
- **Path traversal 방어 필수**: `realpath()` 등으로 경로를 정규화하고, 허용된 루트 디렉터리(`/data/uploads/` 등) 내에 있는지 반드시 검증
|
||||
- 파일이 존재하지 않으면 **`404 Not Found`** 반환
|
||||
|
||||
---
|
||||
|
||||
### 6. 파일 스트림 응답 (Range 지원 권장)
|
||||
|
||||
파일을 청크 단위로 스트리밍하여 응답한다.
|
||||
|
||||
**응답 헤더:**
|
||||
|
||||
|헤더|값|
|
||||
|---|---|
|
||||
|`Content-Type`|파일 MIME 타입|
|
||||
|`Content-Disposition`|`attachment; filename="파일명"`|
|
||||
|`Accept-Ranges`|`bytes`|
|
||||
|`Content-Length`|파일 크기 (전체 또는 청크)|
|
||||
|
||||
**Range 지원 시 동작:**
|
||||
|
||||
- `Range` 헤더가 있으면 **`206 Partial Content`** 로 응답
|
||||
- `Range` 헤더가 없으면 **`200 OK`** 로 전체 파일 응답
|
||||
- Range를 지원하면 대용량 파일 resume, 미디어 스트리밍, 멀티파트 다운로드 모두 대응 가능
|
||||
|
||||
|
||||
#### Risk
|
||||
- 응답 헤더에 `Content-Type: 파일 MIME 타입`이라고만 되어 있고, 이 값을 **어디서 가져오는지** 명시되어 있지 않다. 업로드 시 클라이언트가 보낸 값을 DB에 저장하고 그대로 사용하는 경우, 공격자가 `text/html`이나 `image/svg+xml`로 위장한 악성 파일을 업로드할 수 있다.
|
||||
- 로컬 FS에서 직접 스트리밍하므로 API/스트리밍 서버의 egress 대역폭이 병목이 된다. 동시 다운로드 수 제한, 사용자별 대역폭 제한, 전체 서버 대역폭 보호 정책이 없다.
|
||||
|
||||
|
||||
---
|
||||
|
||||
### 7. 로그 기록 / 카운트 증가
|
||||
|
||||
다운로드 완료 후 감사(Audit) 로그를 기록하고, 공유 링크의 경우 카운트를 증가시킨다.
|
||||
|
||||
- **로그 항목**: 파일 ID, 요청자, IP, User-Agent, 다운로드 시각, 파일 크기
|
||||
- **카운트 증가**: 공유 링크에만 적용 (`download_count++`)
|
||||
- **타이밍 주의**: 스트림이 **완전히 전송된 이후**에 카운트를 올린다. 요청 시점에 올리면 중단된 다운로드도 집계된다.
|
||||
|
||||
---
|
||||
|
||||
## 에러 응답 정리
|
||||
|
||||
|상황|HTTP 상태 코드|
|
||||
|---|---|
|
||||
|인증 실패 / 토큰 만료|`401 Unauthorized`|
|
||||
|권한 없음|`403 Forbidden`|
|
||||
|파일 없음|`404 Not Found`|
|
||||
|Range 범위 초과|`416 Range Not Satisfiable`|
|
||||
|정상 전체 응답|`200 OK`|
|
||||
|정상 부분 응답 (Range)|`206 Partial Content`|
|
||||
|
||||
---
|
||||
|
||||
## 보안 체크리스트
|
||||
|
||||
- [ ] JWT / share_token 서명 검증
|
||||
- [ ] 파일 접근 권한 DB 조회 (캐시 사용 시 TTL 주의)
|
||||
- [ ] 다운로드 토큰 단명 발급 (TTL ≤ 60s)
|
||||
- [ ] 토큰 1회 사용 후 무효화
|
||||
- [ ] Path traversal 방어 (`realpath` + 루트 경로 검증)
|
||||
- [ ] `Content-Disposition: attachment` 헤더 명시
|
||||
- [ ] 다운로드 완료 후 감사 로그 기록
|
||||
121
설계/img/file_download_flow.svg
Normal file
121
설계/img/file_download_flow.svg
Normal file
@@ -0,0 +1,121 @@
|
||||
<svg width="100%" viewBox="0 0 680 820" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<path d="M2 1L8 5L2 9" fill="none" stroke="context-stroke" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</marker>
|
||||
<mask id="imagine-text-gaps-hhsrrm" maskUnits="userSpaceOnUse"><rect x="0" y="0" width="680" height="820" fill="white"/><rect x="249.55258178710938" y="41.31908416748047" width="180.89492797851562" height="21.361833572387695" fill="black" rx="2"/><rect x="249.9481964111328" y="60.5247688293457" width="180.10369873046875" height="18.950468063354492" fill="black" rx="2"/><rect x="254.57876586914062" y="125.31908416748047" width="170.84254455566406" height="21.361833572387695" fill="black" rx="2"/><rect x="259.8648986816406" y="144.52476501464844" width="160.2702178955078" height="18.950468063354492" fill="black" rx="2"/><rect x="557.7877807617188" y="129.319091796875" width="104.42449188232422" height="21.361833572387695" fill="black" rx="2"/><rect x="274.4046936035156" y="209.319091796875" width="131.1906509399414" height="21.361833572387695" fill="black" rx="2"/><rect x="256.2780456542969" y="228.5247802734375" width="167.44403076171875" height="18.950468063354492" fill="black" rx="2"/><rect x="253.23367309570312" y="293.319091796875" width="173.53273010253906" height="21.361833572387695" fill="black" rx="2"/><rect x="259.1829528808594" y="312.5247802734375" width="161.63414001464844" height="18.950468063354492" fill="black" rx="2"/><rect x="567.0753173828125" y="297.319091796875" width="85.84944152832031" height="21.361833572387695" fill="black" rx="2"/><rect x="216.82582092285156" y="377.319091796875" width="246.3626251220703" height="21.361833572387695" fill="black" rx="2"/><rect x="242.3297882080078" y="396.5247802734375" width="195.34051513671875" height="18.950468063354492" fill="black" rx="2"/><rect x="556.8533935546875" y="381.319091796875" width="106.29329681396484" height="21.361833572387695" fill="black" rx="2"/><rect x="280.7759704589844" y="461.319091796875" width="118.59143829345703" height="21.361833572387695" fill="black" rx="2"/><rect x="245.60020446777344" y="480.5247802734375" width="188.7996826171875" height="18.950468063354492" fill="black" rx="2"/><rect x="262.80377197265625" y="545.319091796875" width="154.53204345703125" height="21.361833572387695" fill="black" rx="2"/><rect x="238.98402404785156" y="564.5247802734375" width="202.0320587158203" height="18.950468063354492" fill="black" rx="2"/><rect x="48.80827713012695" y="37.31908416748047" width="72.38346862792969" height="21.361833572387695" fill="black" rx="2"/><rect x="44.41883850097656" y="121.319091796875" width="80.76659393310547" height="21.361833572387695" fill="black" rx="2"/><rect x="35.018280029296875" y="285.319091796875" width="99.97607421875" height="21.361833572387695" fill="black" rx="2"/><rect x="35.32346725463867" y="373.319091796875" width="99.38409423828125" height="21.361833572387695" fill="black" rx="2"/><rect x="29.020008087158203" y="457.319091796875" width="111.994140625" height="21.361833572387695" fill="black" rx="2"/><rect x="37.825260162353516" y="541.319091796875" width="93.89728546142578" height="21.361833572387695" fill="black" rx="2"/><rect x="306.45703125" y="640.5247802734375" width="67.37143325805664" height="18.950468063354492" fill="black" rx="2"/><rect x="356.4570007324219" y="658.5247802734375" width="67.37143325805664" height="18.950468063354492" fill="black" rx="2"/></mask></defs>
|
||||
|
||||
<!-- Step 1 -->
|
||||
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||
<rect x="200" y="30" width="280" height="56" rx="8" stroke-width="0.5" style="fill:rgb(230, 241, 251);stroke:rgb(24, 95, 165);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="340" y="52" text-anchor="middle" dominant-baseline="central" style="fill:rgb(12, 68, 124);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">1. 사용자 인증 / 공유 링크 접근</text>
|
||||
<text x="340" y="70" text-anchor="middle" dominant-baseline="central" style="fill:rgb(24, 95, 165);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:central">JWT 토큰 또는 share_token 파싱</text>
|
||||
</g>
|
||||
|
||||
<line x1="340" y1="86" x2="340" y2="114" marker-end="url(#arrow)" style="fill:none;stroke:rgb(115, 114, 108);color:rgb(0, 0, 0);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
|
||||
<!-- Step 2 -->
|
||||
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||
<rect x="200" y="114" width="280" height="56" rx="8" stroke-width="0.5" style="fill:rgb(238, 237, 254);stroke:rgb(83, 74, 183);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="340" y="136" text-anchor="middle" dominant-baseline="central" style="fill:rgb(60, 52, 137);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">2. 파일 권한 / 공유 상태 검증</text>
|
||||
<text x="340" y="154" text-anchor="middle" dominant-baseline="central" style="fill:rgb(83, 74, 183);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:central">DB 조회 → 접근 허용 여부 확인</text>
|
||||
</g>
|
||||
|
||||
<!-- 403 분기 -->
|
||||
<line x1="480" y1="142" x2="560" y2="142" marker-end="url(#arrow)" stroke="var(--color-text-danger)" stroke-width="1" mask="url(#imagine-text-gaps-hhsrrm)" style="fill:none;stroke:rgb(115, 114, 108);color:rgb(0, 0, 0);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||
<rect x="560" y="118" width="100" height="44" rx="8" stroke-width="0.5" style="fill:rgb(252, 235, 235);stroke:rgb(163, 45, 45);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="610" y="140" text-anchor="middle" dominant-baseline="central" style="fill:rgb(121, 31, 31);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">403 Forbidden</text>
|
||||
</g>
|
||||
|
||||
<line x1="340" y1="170" x2="340" y2="198" marker-end="url(#arrow)" style="fill:none;stroke:rgb(115, 114, 108);color:rgb(0, 0, 0);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
|
||||
<!-- Step 3 -->
|
||||
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||
<rect x="200" y="198" width="280" height="56" rx="8" stroke-width="0.5" style="fill:rgb(225, 245, 238);stroke:rgb(15, 110, 86);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="340" y="220" text-anchor="middle" dominant-baseline="central" style="fill:rgb(8, 80, 65);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">3. 다운로드 토큰 발급</text>
|
||||
<text x="340" y="238" text-anchor="middle" dominant-baseline="central" style="fill:rgb(15, 110, 86);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:central">단기 서명 토큰 생성 (TTL ~60s)</text>
|
||||
</g>
|
||||
|
||||
<line x1="340" y1="254" x2="340" y2="282" marker-end="url(#arrow)" style="fill:none;stroke:rgb(115, 114, 108);color:rgb(0, 0, 0);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
|
||||
<!-- Step 4 -->
|
||||
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||
<rect x="200" y="282" width="280" height="56" rx="8" stroke-width="0.5" style="fill:rgb(225, 245, 238);stroke:rgb(15, 110, 86);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="340" y="304" text-anchor="middle" dominant-baseline="central" style="fill:rgb(8, 80, 65);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">4. 토큰으로 파일 스트림 요청</text>
|
||||
<text x="340" y="322" text-anchor="middle" dominant-baseline="central" style="fill:rgb(15, 110, 86);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:central">GET /stream?token=… 호출</text>
|
||||
</g>
|
||||
|
||||
<!-- Token 검증 분기 -->
|
||||
<line x1="480" y1="310" x2="560" y2="310" marker-end="url(#arrow)" stroke="var(--color-text-danger)" stroke-width="1" style="fill:none;stroke:rgb(115, 114, 108);color:rgb(0, 0, 0);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||
<rect x="560" y="286" width="100" height="44" rx="8" stroke-width="0.5" style="fill:rgb(252, 235, 235);stroke:rgb(163, 45, 45);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="610" y="308" text-anchor="middle" dominant-baseline="central" style="fill:rgb(121, 31, 31);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">401 Expired</text>
|
||||
</g>
|
||||
|
||||
<line x1="340" y1="338" x2="340" y2="366" marker-end="url(#arrow)" style="fill:none;stroke:rgb(115, 114, 108);color:rgb(0, 0, 0);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
|
||||
<!-- Step 5 -->
|
||||
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||
<rect x="200" y="366" width="280" height="56" rx="8" stroke-width="0.5" style="fill:rgb(250, 238, 218);stroke:rgb(133, 79, 11);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="340" y="388" text-anchor="middle" dominant-baseline="central" style="fill:rgb(99, 56, 6);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">5. storage_key → 로컬 FS 경로 resolve</text>
|
||||
<text x="340" y="406" text-anchor="middle" dominant-baseline="central" style="fill:rgb(133, 79, 11);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:central">키-경로 맵 조회 / path traversal 방어</text>
|
||||
</g>
|
||||
|
||||
<!-- Not found 분기 -->
|
||||
<line x1="480" y1="394" x2="560" y2="394" marker-end="url(#arrow)" stroke="var(--color-text-danger)" stroke-width="1" mask="url(#imagine-text-gaps-hhsrrm)" style="fill:none;stroke:rgb(115, 114, 108);color:rgb(0, 0, 0);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||
<rect x="560" y="370" width="100" height="44" rx="8" stroke-width="0.5" style="fill:rgb(252, 235, 235);stroke:rgb(163, 45, 45);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="610" y="392" text-anchor="middle" dominant-baseline="central" style="fill:rgb(121, 31, 31);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">404 Not Found</text>
|
||||
</g>
|
||||
|
||||
<line x1="340" y1="422" x2="340" y2="450" marker-end="url(#arrow)" style="fill:none;stroke:rgb(115, 114, 108);color:rgb(0, 0, 0);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
|
||||
<!-- Step 6 -->
|
||||
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||
<rect x="200" y="450" width="280" height="56" rx="8" stroke-width="0.5" style="fill:rgb(234, 243, 222);stroke:rgb(59, 109, 17);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="340" y="472" text-anchor="middle" dominant-baseline="central" style="fill:rgb(39, 80, 10);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">6. 파일 스트림 응답</text>
|
||||
<text x="340" y="490" text-anchor="middle" dominant-baseline="central" style="fill:rgb(59, 109, 17);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:central">206 Partial Content (Range 지원)</text>
|
||||
</g>
|
||||
|
||||
<line x1="340" y1="506" x2="340" y2="534" marker-end="url(#arrow)" style="fill:none;stroke:rgb(115, 114, 108);color:rgb(0, 0, 0);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
|
||||
<!-- Step 7 -->
|
||||
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||
<rect x="200" y="534" width="280" height="56" rx="8" stroke-width="0.5" style="fill:rgb(241, 239, 232);stroke:rgb(95, 94, 90);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="340" y="556" text-anchor="middle" dominant-baseline="central" style="fill:rgb(68, 68, 65);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">7. 로그 기록 / 카운트 증가</text>
|
||||
<text x="340" y="574" text-anchor="middle" dominant-baseline="central" style="fill:rgb(95, 94, 90);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:central">공유 링크인 경우 download_count++</text>
|
||||
</g>
|
||||
|
||||
<!-- Actor labels -->
|
||||
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||
<rect x="30" y="30" width="110" height="36" rx="8" stroke-width="0.5" style="fill:rgb(230, 241, 251);stroke:rgb(24, 95, 165);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="85" y="48" text-anchor="middle" dominant-baseline="central" style="fill:rgb(12, 68, 124);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">클라이언트</text>
|
||||
</g>
|
||||
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||
<rect x="30" y="114" width="110" height="36" rx="8" stroke-width="0.5" style="fill:rgb(238, 237, 254);stroke:rgb(83, 74, 183);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="85" y="132" text-anchor="middle" dominant-baseline="central" style="fill:rgb(60, 52, 137);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">Auth / ACL</text>
|
||||
</g>
|
||||
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||
<rect x="30" y="246" width="110" height="100" rx="8" stroke-width="0.5" style="fill:rgb(225, 245, 238);stroke:rgb(15, 110, 86);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="85" y="296" text-anchor="middle" dominant-baseline="central" style="fill:rgb(8, 80, 65);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">Token Service</text>
|
||||
</g>
|
||||
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||
<rect x="30" y="366" width="110" height="36" rx="8" stroke-width="0.5" style="fill:rgb(250, 238, 218);stroke:rgb(133, 79, 11);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="85" y="384" text-anchor="middle" dominant-baseline="central" style="fill:rgb(99, 56, 6);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">Storage Layer</text>
|
||||
</g>
|
||||
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||
<rect x="30" y="450" width="110" height="36" rx="8" stroke-width="0.5" style="fill:rgb(234, 243, 222);stroke:rgb(59, 109, 17);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="85" y="468" text-anchor="middle" dominant-baseline="central" style="fill:rgb(39, 80, 10);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">Stream Handler</text>
|
||||
</g>
|
||||
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||
<rect x="30" y="534" width="110" height="36" rx="8" stroke-width="0.5" style="fill:rgb(241, 239, 232);stroke:rgb(95, 94, 90);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="85" y="552" text-anchor="middle" dominant-baseline="central" style="fill:rgb(68, 68, 65);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">Audit Logger</text>
|
||||
</g>
|
||||
|
||||
<!-- Legend -->
|
||||
<rect x="200" y="630" width="280" height="60" rx="8" fill="none" stroke="var(--color-border-tertiary)" stroke-width="0.5" stroke-dasharray="4 3" style="fill:none;stroke:rgba(31, 30, 29, 0.15);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-dasharray:4px, 3px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="340" y="650" text-anchor="middle" dominant-baseline="central" style="fill:rgb(61, 61, 58);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:central">정상 흐름 →</text>
|
||||
<line x1="280" y1="668" x2="330" y2="668" stroke="var(--color-text-danger)" stroke-width="1" marker-end="url(#arrow)" style="fill:rgb(0, 0, 0);stroke:rgb(127, 44, 40);color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="390" y="668" text-anchor="middle" dominant-baseline="central" style="fill:rgb(61, 61, 58);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:central">에러 분기 →</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 30 KiB |
Reference in New Issue
Block a user