지도 타일 보안 방법 상세

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 보안을 완전히 우회할 수 있다

목차

  1. 기반 설정: 스토리지 Private 유지
  2. CORS 도메인 제한
  3. Referrer(Hotlink) 제한
  4. IP 화이트리스트
  5. Rate Limiting
  6. Presigned URL
  7. API Key + Proxy 서버 ★권장
  8. JWT + Proxy 서버
  9. OAuth 2.0
  10. Cloudflare Access (Zero Trust)
  11. HMAC Signed URL
  12. Cloudflare Tunnel + Private Network
  13. CDN 캐싱 전략 (Cache API)
  14. 방식별 비교 및 권장 조합

기반 설정

모든 보안 방법의 전제 조건이다.

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[보안 완성]
설정R2S3권장
기본 상태PrivatePrivate✅ 유지
우회 경로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
}

보안 수준: 낮음 | 구현 난이도: 쉬움 | 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 KeyJWT
만료 시간 제어❌ (별도 로직 필요)✅ (토큰 내장)
사용자별 권한✅ (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 TokenOne-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 URLHMAC 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.jsonpublic, max-age=300 (5분)타일셋 구조 변경 시 빠른 갱신 필요
.b3dm, .i3dmpublic, max-age=86400 (1일)타일 데이터는 변경 빈도 낮음
.glb, .gltfpublic, 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 화이트리스트 + 감사 로그

문서 탐색


참고 자료