브라우저 업로드 Origin 구성

CloudflareBeginner
지금 연습하기

소개

presigned URL 이 curl 에서 정상적으로 작동해도 브라우저 업로드 페이지는 실패할 수 있습니다. 브라우저는 스토리지 서비스가 페이지의 Origin 을 허용하는지도 확인하기 때문입니다. 이 실습에서는 버킷을 비공개로 유지하면서 제공된 합성 업로드 페이지에 대해 독립적인 CORS 경계를 구성합니다.

먼저 Authorize Temporary File Access 를 완료합니다. 이 새 VM 에는 고정된 Node.js 22.22.0, Wrangler 4.131.1, AWS SDK 3.888.0 과 완성된 업로드 페이지 셸이 제공됩니다. 새 비공개 버킷, 수명이 짧은 객체 자격 증명, 일회용 Worker 페이지를 만듭니다. R2 가 이미 활성화되어 있어야 합니다. R2 pricingCORS behavior를 확인합니다. 구매한 도메인은 필요하지 않습니다. 제공된 합성 텍스트만 사용하고, 모든 리소스를 정리한 뒤 실습 토큰을 폐기합니다.

애플리케이션 버킷 연결

이 단계에서는 이 VM 을 인증하고 애플리케이션용 독립 비공개 버킷을 만듭니다. 디바이스 인증은 학습 계정을 확인합니다. R2 버킷 관리는 해당 계정으로 제한된 별도의 API 토큰을 사용합니다.

아래 명령 구문에 사용할 Bash 를 시작한 다음 준비된 프로젝트로 이동하여 도구를 확인합니다. 리소스 이름 변수가 유지되도록 같은 터미널을 계속 사용합니다.

bash
cd /home/labex/project/r2-lab
export PATH="$PWD/.tools/node-v22.22.0-linux-x64/bin:$PATH"
node --version
npx wrangler --version

표시된 디바이스 코드를 본인의 브라우저에서 승인합니다. 동의하기 전에 학습 계정과 요청된 account 및 user 읽기 범위를 확인합니다.

npx wrangler login --device --browser=false --scopes account:read user:read workers_scripts:write workers_kv:write
npx wrangler whoami --json

loggedIn: true인지 확인합니다. 계정이 하나만 표시되더라도 계정 이름을 확인합니다. 아래의 YOUR_ACCOUNT_ID를 해당 계정의 실제 32 자 ID 로 바꿉니다. openssl rand -hex 6은 12 개의 임의 16 진수 문자를 생성하므로 이 실습이 이전 실행과 충돌하지 않습니다. 여기 문서 (here-document) 는 표준 구성 파일을 작성하며 셸은 변수 값을 파일에 삽입합니다.

ACCOUNT_ID=YOUR_ACCOUNT_ID
RUN_ID=$(openssl rand -hex 6)
NAME="labex-c05-r05-$RUN_ID"
BUCKET="$NAME-docs"
cat > wrangler.jsonc <<JSON
{"name":"$NAME","account_id":"$ACCOUNT_ID","main":"src/index.js","workers_dev":true,"compatibility_date":"2026-07-30","r2_buckets":[{"binding":"DOCUMENTS","bucket_name":"$BUCKET"}]}
JSON

버킷 관리를 위해 Cloudflare 프로필의 API Tokens 페이지를 열고 이 실습의 이름을 포함한 사용자 지정 토큰을 만듭니다. Account → Workers R2 Storage → Edit 권한을 부여하고, Account Resources를 저장해 둔 학습 계정으로 제한합니다. 만료 기간은 짧게 설정합니다. 다른 계정이나 관련 없는 권한은 포함하지 않습니다. 이 계정 수준 권한으로 버킷을 만들고 삭제할 수 있습니다. 다음 단계의 객체 전용 토큰으로는 이 작업을 수행할 수 없습니다.

토큰을 한 번만 복사하여 이 숨겨진 VM 프롬프트에 입력합니다. umask 077은 파일을 본인 사용자만 읽을 수 있도록 제한하고, read -s는 입력을 화면에 표시하지 않습니다. 이 파일은 Wrangler 의 표준 토큰 변수를 사용하며 Git 에서 제외됩니다.

umask 077
read -r -s -p 'R2 management API token: ' R2_MANAGEMENT_TOKEN; printf '\n'
printf 'CLOUDFLARE_API_TOKEN=%s\n' "$R2_MANAGEMENT_TOKEN" > .env.management
unset R2_MANAGEMENT_TOKEN

R2 관리 명령에만 --env-file=.env.management를 사용합니다. 일반적인 whoami 명령은 계속 VM 의 디바이스 인증을 확인합니다.

--env-file을 각 Wrangler 명령의 끝에 배치하여 파일 인수 목록에 명령 이름까지 포함되지 않게 합니다. 각 버킷을 만든 후 Wrangler 가 구성에 바인딩을 추가할지 물으면 n을 입력하고 Enter 를 누릅니다. 필요한 바인딩은 이미 구성에 포함되어 있습니다.

npx wrangler r2 bucket create "$BUCKET" --env-file=.env.management

버킷을 나열하여 정확히 생성된 이름을 찾습니다. 다른 버킷은 다른 작업에 속하므로 변경하지 않습니다.

npx wrangler r2 bucket list --env-file=.env.management

Dashboard 에서 Storage & databases → R2 → Overview를 열고 이 버킷을 선택한 뒤, 객체 목록이 비어 있는지 확인합니다. 버킷 설정에서 public development URL 과 custom domains 는 비활성화된 상태로 둡니다. Dashboard 에 표시된 버킷 이름은 대상을 확인하는 데 사용합니다. 저장된 바이트는 이후 다운로드 검사로 확인합니다.

Worker 스크립트 권한은 배포에 사용됩니다. KV 권한은 Wrangler 의 삭제 기록 관리에 사용되지만, 이 실습에서는 KV 네임스페이스를 만들지 않습니다. R2 관리 토큰은 별도의 계정 범위 자격 증명으로 유지됩니다.

업로드 페이지 준비 및 서명 권한 설정

이 단계에서는 제공된 페이지를 배포하고 이 버킷으로 범위가 제한된 자격 증명을 터미널에 제공합니다. 페이지에는 고정된 합성 페이로드와 비어 있는 URL 필드만 포함되며, 장기 서명 비밀은 받지 않습니다.

S3 호환 API 를 사용하면 표준 스토리지 SDK 로 R2 에 접근할 수 있습니다. 이 API 는 Wrangler 의 디바이스 토큰이 아니라 별도의 액세스 키 쌍을 사용합니다. R2 Overview 에서 Account Details → API Tokens → Manage를 선택한 다음, 이 실습에서 생성한 리소스 이름을 포함한 User API token을 만듭니다. Object Read & Write를 선택하고 이 단계에서 만든 새 버킷 하나로만 범위를 제한합니다. 양식에서 제공한다면 만료 기간을 짧게 설정합니다. 모든 버킷 또는 Admin 접근 권한은 선택하지 않습니다. 일회성 비밀을 저장할 때까지 이 토큰 페이지를 열어 둡니다.

VM 에서 다음 Bash 프롬프트를 사용합니다. read -s는 입력을 숨기고, umask 077은 자격 증명 파일을 본인 사용자만 읽을 수 있게 합니다. 아래 이름은 AWS SDK 의 표준 환경 변수입니다. Access Key ID 와 Secret Access Key 를 각각 해당 프롬프트에 붙여 넣고 Enter 키를 누릅니다. 일반 API 토큰 값을 붙여 넣지 않습니다.

umask 077
read -r -s -p 'Access Key ID: ' AWS_ACCESS_KEY_ID; printf '\n'
read -r -s -p 'Secret Access Key: ' AWS_SECRET_ACCESS_KEY; printf '\n'
printf 'AWS_ACCESS_KEY_ID=%s\nAWS_SECRET_ACCESS_KEY=%s\n' "$AWS_ACCESS_KEY_ID" "$AWS_SECRET_ACCESS_KEY" > .env.s3
unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY

재사용할 수 있는 표준 SDK 클라이언트를 작성합니다. SDK 에는 리전 문자열이 필요하며 R2 에서는 auto를 사용합니다. 기존 구성을 읽으면 CLI 와 SDK 작업이 같은 계정과 버킷을 대상으로 하게 됩니다.

cat > storage.mjs <<'JS'
import { S3Client } from "@aws-sdk/client-s3";
import { readFileSync } from "node:fs";
const config = JSON.parse(readFileSync("wrangler.jsonc", "utf8"));
export const Bucket = config.r2_buckets[0].bucket_name;
export const s3 = new S3Client({
  region: "auto",
  requestChecksumCalculation: "WHEN_REQUIRED",
  endpoint: `https://${config.account_id}.r2.cloudflarestorage.com`,
  credentials: {
    accessKeyId: process.env.AWS_ACCESS_KEY_ID,
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
  }
});
JS

requestChecksumCalculation: "WHEN_REQUIRED" 는 브라우저가 본문을 제공하기 전에 빈 본문의 체크섬에 서명하는 것을 방지합니다. URL 은 여전히 객체와 작업에 서명하며, 아래 서명 호출은 Content-Type 을 명시적으로 포함합니다. 최종 검사는 실제 저장된 바이트를 비교합니다.

제공된 페이지를 배포합니다.

npx wrangler deploy

HTTPS 배포 URL 을 경로나 끝의 슬래시 없이 ORIGIN에 복사합니다. Origin은 scheme, hostname, port 로 구성됩니다. https://example.comhttp://example.com은 서로 다른 Origin 입니다. 이후 독립 검사에서 사용할 수 있도록 저장합니다.

ORIGIN=https://YOUR_WORKER.YOUR_SUBDOMAIN.workers.dev
printf "%s\n" "$ORIGIN" > origin.txt

브라우저에서 해당 URL 을 엽니다. Synthetic export upload 페이지가 표시되고 임시 URL 필드가 비어 있는지 확인합니다. 아직 업로드하지 않습니다. 현재 버킷은 이 브라우저 Origin 을 아직 허용하지 않습니다.

브라우저 Origin 하나 허용

이 단계에서는 **Cross-Origin Resource Sharing (CORS)**을 구성합니다. 브라우저는 교차 Origin PUT 을 보내기 전에 버킷에 해당 페이지의 Origin, 메서드, 헤더가 허용되는지 묻습니다. 이 OPTIONS 요청을 preflight라고 합니다. CORS 는 브라우저 접근을 제어하며, 서명된 작업의 스토리지 인증을 대신하지 않습니다.

실제 페이지 Origin 을 사용하여 정책을 작성합니다. 터미널은 $ORIGIN을 JSON 에 삽입합니다. Wrangler 는 AWS 의 CORSRules 형식이 아니라 Cloudflare 의 소문자 rules/allowed 형식을 사용합니다.

cat > cors.json <<JSON
{"rules":[{"allowed":{"origins":["$ORIGIN"],"methods":["PUT"],"headers":["content-type"]},"exposeHeaders":["ETag"],"maxAgeSeconds":60}]}
JSON

이 페이지에는 PUT 과 Content-Type 만 필요합니다. exposeHeaders를 사용하면 JavaScript 가 반환된 ETag 를 읽을 수 있고, 짧은 maxAgeSeconds 값은 테스트 중 preflight 캐시 시간을 줄입니다.

npx wrangler r2 bucket cors set "$BUCKET" --file cors.json --env-file=.env.management
npx wrangler r2 bucket cors list "$BUCKET" --env-file=.env.management

정책을 새로 만든 이 버킷에만 덮어쓰는지 확인합니다. 결과 목록에 정확한 Origin 이 표시되어야 합니다. 버킷의 Dashboard 설정에서 CORS 정책을 읽기 전용으로 확인하고 public access 는 비활성화된 상태로 둡니다.

업로드 페이지의 CORS 정책

예시: 정책은 이 페이지 출처, PUT 및 content-type 을 허용합니다. 생성된 호스트 이름은 달라집니다. CLI 출력으로 공개된 ETag 와 캐시 시간도 확인합니다.

브라우저를 통해 업로드

이 단계에서는 서명된 PUT 권한과 브라우저 Origin 정책을 함께 사용합니다. 앞에서 배운 서명 스크립트는 정확히 uploads/browser.txt를 대상으로 하는 URL 을 만들고, 페이지는 서명할 때 사용한 것과 동일한 Content-Type 을 전송합니다.

cat > sign-upload.mjs <<'JS'
import { PutObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import { writeFileSync } from "node:fs";
import { s3, Bucket } from "./storage.mjs";
const url = await getSignedUrl(s3, new PutObjectCommand({
  Bucket, Key: "uploads/browser.txt", ContentType: "text/plain"
}), { expiresIn: 300, signableHeaders: new Set(["content-type"]) });
writeFileSync("put-url.txt", url, { mode: 0o600 });
console.log("Prepared a five-minute PUT URL");
JS
node --env-file=.env.s3 sign-upload.mjs

이 임시 링크는 제공된 페이지에 복사할 때까지만 표시합니다. 링크를 공유하거나 스크린샷에 포함하거나 S3 비밀을 대신 붙여 넣지 않습니다.

cat put-url.txt

링크를 Temporary PUT URL에 붙여 넣은 다음 5 분 이내에 Upload synthetic text를 선택합니다. 2xx HTTP 결과가 성공적으로 반환되고 ETag 가 표시되는지 확인합니다. 업로드가 끝나면 URL 필드를 지웁니다. 만료된 URL 은 오류 응답에 CORS 헤더가 없어서 일반적인 브라우저 CORS 오류처럼 보일 수 있습니다. 필요한 경우 새 링크를 만듭니다.

Wrangler 를 통해 실제 객체를 다운로드하고 고정된 페이로드와 비교합니다.

npx wrangler r2 object get "$BUCKET/uploads/browser.txt" --remote --file browser-download.txt --env-file=.env.management
printf "Synthetic browser export.\n" > expected-browser.txt
cmp expected-browser.txt browser-download.txt

플랫폼 검사는 바이트와 Content-Type 을 독립적으로 읽습니다. 이 실습에서는 실제 브라우저 상호 작용이 반드시 필요합니다. curl 만 사용하면 브라우저 CORS 가 적용되지 않습니다.

브라우저 업로드 성공

실제 브라우저가 HTTP 200 과 읽을 수 있는 ETag 를 반환했으며 임시 URL 입력란은 비웠습니다. 페이지에는 \n이 문자 그대로 표시되지만 실제 줄바꿈이 업로드됩니다. 독립적인 다운로드 검사로 정확히 26 바이트인 내용을 확인합니다.

Origin 정책과 서명 권한 구분

이 단계에서는 다른 객체를 만들지 않고 허용된 preflight 와 허용되지 않은 preflight 를 확인합니다. 다음 curl 요청은 브라우저가 수행할 수 있는 작업을 묻는 것입니다. curl 자체는 그 응답을 강제하지 않습니다.

ACCOUNT_ID=$(node -p "JSON.parse(require('fs').readFileSync('wrangler.jsonc')).account_id")
OBJECT_URL="https://$ACCOUNT_ID.r2.cloudflarestorage.com/$BUCKET/uploads/browser.txt"
curl -i -X OPTIONS -H "Origin: $ORIGIN" -H "Access-Control-Request-Method: PUT" -H "Access-Control-Request-Headers: content-type" "$OBJECT_URL"

Access-Control-Allow-Origin 값이 페이지의 정확한 Origin 과 일치해야 합니다. 이제 관련 없는 Origin 에서 요청합니다.

curl -i -X OPTIONS -H "Origin: https://outside.example" -H "Access-Control-Request-Method: PUT" -H "Access-Control-Request-Headers: content-type" "$OBJECT_URL"

응답이 해당 Origin 을 허용하지 않아야 하며 *도 반환해서는 안 됩니다. 거부 응답의 정확한 상태 코드는 다를 수 있습니다. 브라우저 접근을 막는 것은 허용 응답이 없다는 점입니다. 마지막으로 서명 없이 읽기를 시도합니다.

curl -sS -o unsigned.xml -w "%{http_code}\n" "$OBJECT_URL"

이 R2 S3 엔드포인트의 서명 없는 요청에서 관찰된 대로 400, XML Code InvalidArgument, Message Authorization 을 확인합니다. 아래 두 필드를 검사하세요. 임의의 오류나 네트워크 실패는 서명 거부의 증거가 아닙니다.

python3 - <<'PYXML'
from xml.etree import ElementTree
root = ElementTree.parse("unsigned.xml").getroot()
print("Code:", root.findtext("Code"))
print("Message:", root.findtext("Message"))
PYXML

CORS 를 활성화해도 버킷이 공개되거나 서명 검사가 제거되지는 않습니다. 유효한 서명이 있는 비브라우저 클라이언트는 Origin 헤더와 관계없이 허용된 작업을 수행할 수 있습니다. Origin 문자열은 신원 자격 증명이 아닙니다.

업로드 리소스와 자격 증명 삭제

이 단계에서는 UI Worker, 정확한 객체, 버킷을 삭제합니다. 자격 증명을 폐기하기 전에 리소스가 사라졌는지 확인합니다.

npx wrangler delete
npx wrangler r2 object delete "$BUCKET/uploads/browser.txt" --remote --env-file=.env.management
npx wrangler r2 bucket delete "$BUCKET" --env-file=.env.management
npx wrangler r2 bucket list --env-file=.env.management

이 실습에서 생성한 이름만 남아 있지 않은지 확인합니다. Dashboard 에서 Worker 목록과 버킷 목록을 새로 고칩니다. 관리 인증이 아직 활성화된 상태에서 플랫폼 정리 검사를 실행합니다. 버킷과 함께 해당 버킷의 CORS 구성도 삭제됩니다.

실습 자격 증명 폐기 및 로그아웃

이 단계에서는 이 실습으로 남은 접근 권한을 닫습니다. R2 API Tokens 페이지에서 이 실습의 이름을 포함하는 객체 토큰만 폐기합니다. 프로필의 API Tokens 페이지에서 이 실습을 위해 만든 별도의 R2 관리 토큰을 폐기합니다. 버킷을 삭제해도 토큰은 폐기되지 않으며, Wrangler 에서 로그아웃해도 S3 자격 증명은 폐기되지 않습니다.

폐기한 뒤 로컬 자격 증명 파일을 삭제하고 이 VM 에서 로그아웃합니다.

rm .env.s3 .env.management put-url.txt
npx wrangler logout

구조화된 신원 정보를 확인합니다. 로그아웃 상태에서는 종료 상태가 0 이 아닌 것이 정상입니다.

npx wrangler whoami --json || true

loggedIn: false인지 확인합니다. 일반적인 Dashboard 로그인은 유지합니다. 플랫폼은 로컬 자격 증명 삭제와 Wrangler 로그아웃을 검사합니다. 두 토큰의 폐기는 이 실습에서 Dashboard 를 통해 수동으로 수행하는 확인 항목이며, 파일 삭제만으로 추론되지 않습니다.

요약

정확한 Origin 기반 CORS 를 구성하고, presigned URL 로 합성 바이트를 업로드하며, 브라우저 권한과 인증을 구분한 뒤 리소스를 정리합니다.