지도 타일 보안 방법 상세
CORS, Referrer, Rate Limiting부터 API Key·JWT·Proxy·OAuth까지 각 보안 방법의 동작 원리, 구현 방법, 장단점을 상세히 설명한다. 업데이트: 2026-04-03
핵심 요약
| 구분 | 내용 |
|---|---|
| 📖 정의 | 지도 타일 서비스에서 사용 가능한 보안 기술들의 원리와 R2/S3 적용 방법 |
| 💡 핵심 | 3D Tiles 환경에서는 API Key + Proxy 서버 조합이 가장 현실적인 표준 구조다 |
| 🎯 대상 | Cloudflare R2, AWS S3에서 지도 타일을 서비스하는 개발자 |
| ⚠️ 주의 | r2.dev 임시 공개 URL이 활성화되어 있으면 Worker 보안을 완전히 우회할 수 있다 |
목차
- 기반 설정: 스토리지 Private 유지
- CORS 도메인 제한
- Referrer(Hotlink) 제한
- IP 화이트리스트
- Rate Limiting
- Presigned URL
- API Key + Proxy 서버 ★권장
- JWT + Proxy 서버
- OAuth 2.0
- Cloudflare Access (Zero Trust)
- HMAC Signed URL
- Cloudflare Tunnel + Private Network
- CDN 캐싱 전략 (Cache API)
- 방식별 비교 및 권장 조합
기반 설정
모든 보안 방법의 전제 조건이다.
flowchart TD A[스토리지 설정 확인] --> B{Public 버킷?} B -->|Yes — R2| C[r2.dev URL 비활성화<br/>커스텀 도메인으로만 서비스] B -->|Yes — S3| D[Block Public Access 활성화<br/>버킷 정책에서 공개 허용 제거] B -->|Private| E[정상 상태] C --> E D --> E E --> F[Proxy 서버 구성] F --> G[보안 완성]
| 설정 | R2 | S3 | 권장 |
|---|---|---|---|
| 기본 상태 | Private | Private | ✅ 유지 |
| 우회 경로 | r2.dev 공개 URL | 퍼블릭 버킷 정책 | ❌ 비활성화 |
| Proxy 연결 | Worker 바인딩 | CloudFront OAC | ✅ 필수 |
CORS 도메인 제한
보안 수준: 낮음 | 구현 난이도: 쉬움 | 3D Tiles 호환: 투명
HTTP CORS 헤더를 통해 허용 도메인을 제한한다.
허용된 도메인(my-app.example.com) → ✅ 브라우저 허용
허용 안 된 도메인(attacker.com) → ❌ 브라우저 차단
curl / Python requests / 서버 요청 → ✅ CORS 무시하고 접근 가능 (우회됨)
CORS는 브라우저 전용 정책이다. 단독 사용 금지. API Key Proxy와 함께 보조 레이어로만 사용한다.
R2 버킷 CORS 규칙 예시:
{
"AllowedOrigins": ["https://example.com"],
"AllowedMethods": ["GET"],
"AllowedHeaders": ["*"],
"MaxAgeSeconds": 3600
}Referrer(Hotlink) 제한
보안 수준: 낮음 | 구현 난이도: 쉬움 | 3D Tiles 호환: 투명
HTTP Referer 헤더를 검증하여 다른 웹사이트가 우리 URL을 직접 embed하는 것을 차단한다.
요청 출처: my-app.example.com → ✅ 허용
요청 출처: attacker.com → ❌ 차단
curl (Referer 없음 또는 조작) → 우회 가능
한계: Referer 헤더는 쉽게 위조 가능. 브라우저 개인정보 모드에서 생략될 수 있음.
Cloudflare 대시보드 → Security → Scrape Shield → Hotlink Protection 토글로 간단히 활성화 가능.
⚠️ 보조 수단으로만 사용하고, 단독 보안 레이어로 의존하지 않는다.
IP 화이트리스트
보안 수준: 낮음~중간 | 구현 난이도: 쉬움 | 3D Tiles 호환: 투명
특정 IP 주소에서만 접근을 허용한다. 고정 IP 서버 간 통신에만 유효하다.
적합한 환경: 백엔드 서버 → R2 직접 접근, 사내망 전용 뷰어 부적합한 환경: 일반 사용자 공개 서비스 (DHCP, VPN, 모바일 등)
Worker에서 구현:
const ALLOWED_IPS = ["203.0.113.1", "198.51.100.0/24"];
// Worker fetch handler에서
const clientIP = request.headers.get("CF-Connecting-IP");
if (!ALLOWED_IPS.some(ip => clientIP === ip || clientIP.startsWith(ip.split("/")[0]))) {
return new Response("Forbidden", { status: 403 });
}Rate Limiting
보안 수준: 낮음 (보조) | 구현 난이도: 쉬움 | 주목적: 비용 보호
IP당 요청 수를 제한한다. 인증 보안보다 Denial of Wallet 방어가 주목적이다.
Cloudflare WAF Rate Limiting (권장): 대시보드 → Security → WAF → Rate Limiting Rules
규칙 예시:
- 조건: URI Path contains "/tiles/"
- 한도: IP당 10분에 1,000 요청
- 초과 시: 429 Too Many Requests (1분 차단)
Worker에서 KV 스토어로 직접 구현:
const RATE_LIMIT = 500; // 요청/분
const key = `rate:${clientIP}`;
const count = parseInt(await env.RATE_KV.get(key) ?? "0");
if (count >= RATE_LIMIT) {
return new Response("Too Many Requests", { status: 429, headers: { "Retry-After": "60" } });
}
await env.RATE_KV.put(key, String(count + 1), { expirationTtl: 60 });Presigned URL
보안 수준: 중간 | 구현 난이도: 보통 | 3D Tiles 적합성: ❌ 부적합
서버에서 서명된 임시 URL을 생성하여 클라이언트에 전달한다.
flowchart TD A[클라이언트] -->|파일 요청| B[백엔드 서버] B -->|HMAC 서명으로 임시 URL 생성| C[스토리지] B -->|Presigned URL 전달| A A -->|직접 다운로드| C C -->|서명 검증 후 응답| A
R2는 S3 호환 API를 제공하므로 AWS SDK v3를 사용 가능하다.
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
const signedUrl = await getSignedUrl(r2Client, new GetObjectCommand({
Bucket: "my-tileset-bucket", Key: "building/tileset.json"
}), { expiresIn: 300 }); // 최대 7일, 보안상 300~900초 권장3D Tiles에서의 한계: tileset.json 하나가 수백~수천 개 파일을 참조하므로 각 파일마다 개별 Presigned URL 생성이 필요해 현실적으로 불가능하다.
| 용도 | 적합성 |
|---|---|
| 단일 파일 다운로드 (GLB, ZIP 등) | ✅ 적합 |
| 3D Tiles 전체 서비스 | ❌ 부적합 |
API Key + Proxy 서버
보안 수준: 중간~높음 | 구현 난이도: 보통 | 3D Tiles 호환: ✅ 완전
3D Tiles + CesiumJS 환경에서 가장 권장되는 방법이다.
R2/S3 버킷을 Private으로 유지하고, Proxy 서버(Cloudflare Worker 또는 Express)가 인증 게이트웨이 역할을 수행한다.
flowchart TD A[사용자 브라우저<br/>CesiumJS] -->|GET /tileset.json<br/>X-API-Key: KEY| B[Proxy 서버<br/>Worker / Express] B -->|API Key 검증| C{유효한 Key?} C -->|Yes| D[Private 스토리지<br/>R2 / S3] C -->|No| E[401 Unauthorized] D -->|파일 스트리밍| B B -->|응답 전달| A
CesiumJS와의 호환성: Cesium.Resource는 헤더 정보를 tileset.json이 참조하는 모든 하위 타일에 자동 전파한다. 수천 개 타일에 각각 인증을 구현할 필요가 없다.
| 항목 | 내용 |
|---|---|
| ✅ 장점 | 구현 간단, CesiumJS 완전 호환, 서버 부하 없음 (Worker는 Edge 실행) |
| ⚠️ 단점 | API Key가 클라이언트 JS에 노출될 수 있음 |
| ⚠️ 단점 | 사용자별 세분화된 권한 제어 불가 |
API Key 노출 위험은 서비스 단위(프로젝트별)로 Key를 발급하고 주기적으로 교체하는 방식으로 완화한다.
JWT + Proxy 서버
보안 수준: 높음 | 구현 난이도: 어려움 | 3D Tiles 호환: ✅ 완전
API Key의 고정 키를 사용자별 JWT(JSON Web Token)로 교체한다. 토큰에 사용자 ID, 만료 시간, 허용 리소스 경로, 권한 정보를 담을 수 있다.
sequenceDiagram participant C as 클라이언트 participant A as 인증 서버 participant P as Proxy (Worker) participant S as Object Storage C->>A: 로그인 (ID/PW) A-->>C: JWT 발급 (exp: 1h) C->>P: GET /tiles/tileset.json<br/>Authorization: Bearer JWT P->>P: JWT 서명 검증 + 만료 확인 P->>S: 내부 인증으로 파일 조회 S-->>P: 파일 데이터 P-->>C: 타일 응답
API Key 대비 추가 기능
| 기능 | API Key | JWT |
|---|---|---|
| 만료 시간 제어 | ❌ (별도 로직 필요) | ✅ (토큰 내장) |
| 사용자별 권한 | ❌ | ✅ (Claim으로 지정) |
| 특정 경로만 허용 | ❌ | ✅ (allowedPaths 클레임) |
| 토큰 무효화 | ❌ (Key 교체 필요) | ✅ (짧은 만료 또는 블랙리스트) |
OAuth 2.0
보안 수준: 매우 높음 | 구현 난이도: 어려움
엔터프라이즈 지도 서비스(Azure Maps, ArcGIS, Google Earth Engine)가 채택하는 표준 위임 인증 프로토콜이다.
sequenceDiagram participant U as 사용자 participant C as 클라이언트 앱 participant A as 인증 서버 participant R as 지도 API 서버 U->>C: 지도 페이지 접근 C->>A: 인증 요청 (scope: tiles:read) A->>U: 로그인 화면 U->>A: 로그인 A-->>C: Access Token + Refresh Token C->>R: 지도 데이터 요청 (Bearer Access Token) R-->>C: 타일 데이터
Scope 예시: tiles:read (뷰어) / assets:read (관리) / assets:write (업로드) / admin (전체)
Cloudflare Access (Zero Trust)
보안 수준: 높음 | 구현 난이도: 어려움 | 3D Tiles 호환: 제한적
R2 커스텀 도메인에 Zero Trust Access 정책을 적용하여 SSO/OTP 인증된 사용자만 허용한다.
| 환경 | 적합성 |
|---|---|
| 내부 직원 전용 3D 뷰어 | ✅ 적합 |
| SSO 계정이 있는 엔터프라이즈 환경 | ✅ 적합 |
| 일반 사용자 공개 서비스 | ❌ 부적합 |
CesiumJS가 타일을 로드할 때 로그인 페이지로 리디렉션될 수 있다. 이를 해결하려면 Service Token으로 헤더 기반 인증으로 대체해야 한다.
Service Token vs One-Time Pin
| 구분 | Service Token | One-Time Pin (OTP) |
|---|---|---|
| 대상 | 기계 (CesiumJS 앱, 스크립트) | 사람 (직원, 파트너) |
| 인증 방식 | HTTP 헤더에 CF-Access-Client-Id / CF-Access-Client-Secret 포함 | 이메일로 6자리 코드 수신 |
| CesiumJS 적합성 | ✅ 적합 | ❌ 부적합 (자동화 불가) |
| 보안 주의 | Secret 유출 시 전체 버킷 접근 위험, 주기적 rotation 필요 | 코드 10분 유효, 상대적 안전 |
CF_Authorization 쿠키 동작
브라우저 기반 사용자 인증(OTP/IdP) 시 Cloudflare가 CF_Authorization JWT 쿠키를 발급한다. HttpOnly + Secure 속성으로 JavaScript에서 접근할 수 없으며, 이후 모든 요청에 브라우저가 쿠키를 자동 포함한다. 동일 팀 도메인의 다른 Access 앱에도 SSO가 적용된다.
⚠️ R2의
r2.dev공개 URL이 활성화되어 있으면 Access 정책을 우회할 수 있으므로 반드시 비활성화해야 한다.
📎 Cloudflare R2 환경에서의 Access 상세 설정 및 코드 예시는 R2 보안 방식별 상세 비교를 참고한다.
HMAC Signed URL
보안 수준: 중간 | 구현 난이도: 보통 | 3D Tiles 호환: ✅ (prefix 서명 가능)
Worker에서 crypto.subtle(내장 API)을 사용하여 URL에 HMAC-SHA256 서명을 붙이는 방식이다. Authorization 헤더를 사용할 수 없는 환경(iframe, <img> 태그 등)에서 유용하다.
sequenceDiagram participant S as 서버 (URL 생성) participant C as 클라이언트 participant W as Cloudflare Worker participant R as R2 Bucket S->>S: HMAC-SHA256(경로 + 타임스탬프, secret) S-->>C: /tile.glb?verify=timestamp-hmac C->>W: GET /tile.glb?verify=... W->>W: HMAC 재계산 후 일치 확인 W->>W: 타임스탬프 만료 확인 W->>R: R2 Binding으로 파일 조회 R-->>W: 파일 데이터 W-->>C: 200 + 타일 데이터
Presigned URL과의 차이
| 항목 | Presigned URL | HMAC Signed URL |
|---|---|---|
| 서명 주체 | AWS SDK (S3 호환 API) | Worker (crypto.subtle) |
| 외부 라이브러리 | @aws-sdk/client-s3 필요 | 불필요 (내장 API) |
| prefix 단위 서명 | ❌ 불가 (파일 단위만) | ✅ 커스텀 구현으로 가능 |
| 인증 위치 | URL 쿼리 파라미터 | URL 쿼리 파라미터 |
prefix 와일드카드 서명
서명 대상을 파일 경로 대신 prefix로 설정하면 디렉토리 단위 서명이 가능하다. 한 번의 서명으로 prefix 하위 전체 파일에 접근할 수 있으므로 3D Tiles 서빙에도 활용 가능하다. 단, 토큰 유출 시 prefix 하위 전체가 노출되는 위험이 있다.
📎 Worker 코드 전체 예시는 R2 보안 방식별 상세 비교를 참고한다.
Cloudflare Tunnel + Private Network
보안 수준: 매우 높음 | 구현 난이도: 보통 | 대상: 내부 접근 전용
Cloudflare Tunnel로 R2 접근을 사설 네트워크로 구성하고, WARP 클라이언트를 통해서만 접근을 허용하는 방식이다.
flowchart TD USER["내부 사용자 디바이스"] --> WARP["WARP 클라이언트<br/>(Zero Trust 인증)"] WARP --> ZT["Cloudflare Zero Trust Network"] ZT --> TUNNEL["cloudflared Tunnel"] TUNNEL --> PROXY["내부 서버 / R2 연결 프록시"]
| 적합 | 부적합 |
|---|---|
| 개발팀 내부 3D Tiles 데이터 검토 | 일반 외부 사용자 서비스 |
| 운영 환경 배포 전 미리보기 | WARP 설치 불가 환경 |
| R2 버킷 관리 작업 | 50명 초과 시 유료 플랜 필요 |
무료 플랜 기준 50명까지 지원된다. 외부 사용자에게 서비스하는 용도에는 적합하지 않으며, 내부 개발·스테이징 환경에 한정하여 사용한다.
CDN 캐싱 전략 (Cache API)
Worker 내에서 Cloudflare Cache API를 활용하면 인증된 요청의 응답을 CDN 엣지에 캐싱할 수 있다. 동일 타일에 대한 반복 요청이 R2까지 도달하지 않으므로 R2 Class B Operations 비용과 응답 지연이 동시에 감소한다.
flowchart TD REQ["타일 요청"] --> CACHE{"Cache API 조회"} CACHE -- "히트" --> HIT["캐시된 응답 즉시 반환<br/>(R2 요청 없음)"] CACHE -- "미스" --> AUTH["JWT 검증"] AUTH --> R2["R2 Binding으로 파일 조회"] R2 --> STORE["응답 반환 + 캐시 저장<br/>(ctx.waitUntil)"]
파일 유형별 Cache-Control 권장값
| 파일 유형 | Cache-Control | 이유 |
|---|---|---|
tileset.json | public, max-age=300 (5분) | 타일셋 구조 변경 시 빠른 갱신 필요 |
.b3dm, .i3dm | public, max-age=86400 (1일) | 타일 데이터는 변경 빈도 낮음 |
.glb, .gltf | public, max-age=86400 (1일) | 모델 데이터 변경 빈도 낮음 |
캐싱 효과 (비용 절감)
캐싱 미적용: 100명 × 500 타일 = 50,000 R2 요청/씬 전환
캐싱 적용 (히트율 99%): 첫 1명만 R2 요청 → 나머지 99명은 CDN에서 제공
→ R2 비용 약 99% 절감
📎 Cache API를 포함한 Worker 전체 코드는 R2 보안 방식별 상세 비교를 참고한다.
방식별 비교 및 권장 조합
최종 비교표
| 방식 | 구현 난이도 | 보안 수준 | 3D Tiles 적합성 | 권장 상황 |
|---|---|---|---|---|
| API Key + Proxy | ⭐⭐ 보통 | 중간~높음 | ✅ | 대부분 서비스 기본 |
| JWT + Proxy | ⭐⭐⭐ 어려움 | 높음 | ✅ | 사용자 인증 기반 서비스 |
| Presigned URL | ⭐⭐ 보통 | 중간 | ❌ | 단일 파일 다운로드 |
| HMAC Signed URL | ⭐⭐ 보통 | 중간 | ✅ | 헤더 불가 환경 (iframe 등) |
| OAuth 2.0 | ⭐⭐⭐⭐ 매우 어려움 | 매우 높음 | ✅ | 엔터프라이즈·공공기관 |
| Cloudflare Access | ⭐⭐⭐ 어려움 | 높음 | 제한적 | 내부 직원 뷰어 |
| Cloudflare Tunnel | ⭐⭐ 보통 | 매우 높음 | ✅ (내부) | 내부 개발·스테이징 |
| Referrer 제한 | ⭐ 쉬움 | 낮음 | ✅ (보조) | 보조 수단 |
| CORS | ⭐ 쉬움 | 낮음 | ✅ (보조) | 보조 수단 |
권장 심층 방어 조합
flowchart TD A[클라이언트 요청] --> B[레이어 1<br/>Rate Limiting<br/>WAF 규칙] B --> C[레이어 2<br/>CORS 도메인 검증] C --> D[레이어 3<br/>API Key 검증<br/>Worker 프록시] D --> E{모든 레이어 통과?} E -->|Yes| F[Private 스토리지<br/>파일 제공] E -->|No| G[401 / 403 / 429 반환]
소규모 내부 서비스:
Proxy + API Key + CORS + Rate Limiting
상용 서비스 (외부 공개):
Proxy + API Key + 도메인 제한 + Rate Limiting + 정기 Key 교체
엔터프라이즈·공공기관:
Proxy + OAuth 2.0 / JWT + Scope 제한 + IP 화이트리스트 + 감사 로그