介绍
支持代理需要短暂分享一个私有的合成导出文件,但不能将存储桶设为公开,也不能直接提供存储凭据。你将生成一个预签名下载 URL,观察它过期的过程,并验证用于签名的凭据无法访问另一个由本实验拥有的存储桶。
请先完成「整理文档存储桶」和「通过 Worker 流式传输文档」实验。本独立 VM 已固定使用 Node.js 22.22.0、Wrangler 4.131.1 和 AWS SDK 3.888.0。账户必须已启用 R2,并且有权创建具有范围限制的用户对象令牌。本实验只使用两个很小的测试文件。请查看 R2 定价 和 预签名访问;不需要购买域名。不要分享生成的链接或凭据。离开实验前,请清空两个存储桶,并仅撤销本实验创建的令牌。
创建隔离的访问测试存储桶
在本步骤中,为此 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
在你自己的浏览器中授权显示的设备代码。授予同意前,确认学习账户以及请求的账户和用户读取权限:
npx wrangler login --device --browser=false --scopes account:read user:read
npx wrangler whoami --json
确认输出中包含 loggedIn: true。即使列表中只有一个账户,也要读取账户名称。将下面的 YOUR_ACCOUNT_ID 替换为该账户实际的 32 位 ID。openssl rand -hex 6 会生成 12 个随机十六进制字符,避免本次实验与之前的运行发生资源名称冲突。此处的 Here 文档会写入标准配置文件,Shell 会将变量值替换到文件中。
ACCOUNT_ID=YOUR_ACCOUNT_ID
RUN_ID=$(openssl rand -hex 6)
NAME="labex-c05-r04-$RUN_ID"
BUCKET="$NAME-docs"
cat > wrangler.jsonc <<JSON
{"name":"$NAME","account_id":"$ACCOUNT_ID","compatibility_date":"2026-07-30","r2_buckets":[{"binding":"DOCUMENTS","bucket_name":"$BUCKET"}]}
JSON
要管理存储桶,请打开 Cloudflare 个人资料中的 API Tokens 页面,并创建一个以本实验命名的自定义令牌。授予 Account → Workers R2 Storage → Edit 权限,并将 Account Resources 限制为你保存了 ID 的学习账户。设置较短的过期时间。不要包含其他账户或无关权限。此账户级权限可以创建和删除存储桶;下一步使用的仅限对象操作的令牌无法执行这些操作。
将令牌复制一次到此隐藏 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,选择这个准确的存储桶,并检查其空对象列表。在设置中保持公开开发 URL 和自定义域名禁用。Dashboard 中的存储桶名称可以确认资源身份;后续的下载检查会验证实际存储的字节内容。
创建第二个由本实验拥有的存储桶。它代表一个无关的存储边界;不要用其他实验中的存储桶替代它。使用管理凭据分别向两个存储桶上传一个测试文件:
CONTROL="$NAME-control"
npx wrangler r2 bucket create "$CONTROL" --env-file=.env.management
npx wrangler r2 object put "$BUCKET/documents/report.txt" --remote --file document.txt --content-type text/plain --env-file=.env.management
npx wrangler r2 object put "$CONTROL/retained/handbook.txt" --remote --file retained.txt --content-type text/plain --env-file=.env.management
保持两个存储桶的公开访问均处于禁用状态。下一步只允许新的凭据读取文档存储桶。
限制签名凭据的访问范围
在本步骤中,创建一个仅限读取、且只允许访问一个存储桶的凭据。预签名 URL 能授予的访问权限不会超过用于签名的凭据。管理令牌可以管理本实验的两个存储桶,因此本课程中不能用它作为签名身份。
兼容 S3 的 API 让标准存储 SDK 可以访问 R2。它使用独立的访问密钥对,而不是 Wrangler 的设备令牌。在 R2 Overview 中,选择 Account Details → API Tokens → Manage,然后创建一个以本实验生成的资源名称命名的 User API token。选择 Object Read only,将其限制为刚创建的这个存储桶;如果表单提供较短的过期时间,请选择较短的时间。不要选择所有存储桶或 Admin 权限。在保存一次性密钥之前,请保持此令牌页面打开。
在 VM 中使用下面的 Bash 提示符。read -s 会隐藏输入内容;umask 077 会让凭据文件仅对当前用户可读。这些名称是 AWS SDK 使用的标准环境变量。将 Access Key ID 和 Secret Access Key 分别粘贴到对应提示符中,然后按 Enter。不要粘贴通用 API token 的值。
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",
endpoint: `https://${config.account_id}.r2.cloudflarestorage.com`,
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
}
});
JS
只选择 -docs 存储桶;不要将 -control 存储桶纳入此令牌的资源范围。现有的 storage.mjs 客户端只有在使用 --env-file=.env.s3 时才会读取 .env.s3。不要使用 Admin 权限或所有存储桶范围。
分享任何 URL 前,确认令牌表单中选择的是正确的存储桶。下一步对控制存储桶发起的真实请求会独立验证此边界是否生效。

此示例选择 Object Read only,且仅包含生成的 -docs 桶,排除了 -control 桶。你生成的名称会不同。令牌的 24 小时 TTL 与各签名 URL 的五分钟或五秒有效期相互独立。表单显示所选范围;成功和被拒绝的远程请求才证明实际权限。
对临时下载进行签名和测试
在本步骤中,为特定对象和时间窗口内的 GET 请求生成签名。签名过程在本地执行,不会联系 R2;生成 URL 并不能证明权限有效。客户端使用 URL 时,R2 才会验证签名。任何获得 URL 的人都可以在 URL 过期前使用其中授予的操作,因此不要将这些链接放入日志、消息或截图中。
编写一个小型签名程序。expiresIn 的单位是秒。该文件会保存一个五分钟有效的 URL、一个五秒钟有效的 URL,以及一个有意超出访问范围的 URL,但不会打印这些 URL:
cat > sign.mjs <<'JS'
import { GetObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import { readFileSync, writeFileSync } from "node:fs";
import { s3, Bucket } from "./storage.mjs";
const config = JSON.parse(readFileSync("wrangler.jsonc", "utf8"));
const command = new GetObjectCommand({ Bucket, Key: "documents/report.txt" });
const allowed = await getSignedUrl(s3, command, { expiresIn: 300 });
const short = await getSignedUrl(s3, command, { expiresIn: 5 });
const outside = await getSignedUrl(s3, new GetObjectCommand({
Bucket: config.name + "-control", Key: "retained/handbook.txt"
}), { expiresIn: 300 });
const unsigned = new URL(allowed); unsigned.search = "";
writeFileSync("signed.json", JSON.stringify({ allowed, short, outside, unsigned: unsigned.href }), { mode: 0o600 });
console.log("Stored temporary request URLs in private signed.json");
JS
node --env-file=.env.s3 sign.mjs
立即使用允许访问的 URL。此 curl 请求不包含 Authorization 标头,因为签名已包含在 URL 中。node -p 会直接将保存的 URL 提供给 curl,不会在终端输出 URL。
curl -fsS "$(node -p "require('./signed.json').allowed")" -o signed-download.txt
cmp document.txt signed-download.txt
确认两个文件的字节内容完全一致。接着等待十秒,使五秒有效的 URL 过期,然后检查拒绝响应。下面的请求有意不使用 --fail,以便显示 HTTP 错误:
sleep 10
curl -sS -o expired.xml -w "%{http_code}\n" "$(node -p "require('./signed.json').short")"
curl -sS -o unsigned.xml -w "%{http_code}\n" "$(node -p "require('./signed.json').unsigned")"
curl -sS -o outside.xml -w "%{http_code}\n" "$(node -p "require('./signed.json').outside")"
确认过期请求和超出权限范围的请求返回 403。对此 R2 S3 端点的无签名请求会返回 400,错误代码为 InvalidArgument,消息为 Authorization:请求缺少必需的签名信息。这与有效签名凭据被拒绝访问另一个桶不同。检查错误名称,但不要打印完整响应文档,因为其中可能包含请求详细信息:
python3 - <<'PYXML'
from xml.etree import ElementTree
for path in ["expired.xml", "unsigned.xml", "outside.xml"]:
root = ElementTree.parse(path).getroot()
print(path, root.findtext("Code"))
PYXML
过期响应应标识为过期,而控制存储桶应返回 AccessDenied。网络故障不能作为授权结果。如果在测试控制存储桶之前已经超过五分钟,请重新生成 URL,重复允许访问和超出范围的请求,然后再次等待短时 URL 过期。
区分 URL 权限与公开访问
在本步骤中,将观察到的请求与访问模型联系起来。成功的链接只为一个键授权了一次带签名的 GET 请求。它没有启用存储桶的公开开发 URL,也没有授权 PUT 操作,更不会让用户登录你的应用程序。
在 Dashboard 中打开两个由本实验拥有的存储桶的设置。保持公开开发 URL 处于禁用状态,并且不要配置自定义域名。在对象令牌页面中,确认只读令牌仅涵盖 -docs 存储桶。不要在截图中暴露一次性密钥。
现在运行平台检查。它会自行生成带签名的请求,要求返回准确的允许字节内容,并验证未签名、已过期以及超出访问范围的请求均被拒绝。这是真实的远程 API 证据;已保存的 signed.json 不是完成标记。
在生产应用中,应在生成签名链接之前决定用户是否可以访问文档。链接生成后,持有该链接的人可以复制它。后续的应用授权实验会直接讨论这一决策。带签名的 S3 URL 针对的是账户的 R2 S3 端点,而不是公开的 r2.dev 地址或自定义域名。
清理两个由本实验拥有的存储桶
在本步骤中,先删除两个明确指定的文件对象,然后删除它们所属的存储桶。保持管理凭据有效,直到平台确认两个存储桶名称都已不存在。
BUCKET=$(node -p "JSON.parse(require('fs').readFileSync('wrangler.jsonc')).r2_buckets[0].bucket_name")
CONTROL=$(node -p "JSON.parse(require('fs').readFileSync('wrangler.jsonc')).name + '-control'")
npx wrangler r2 object delete "$BUCKET/documents/report.txt" --remote --env-file=.env.management
npx wrangler r2 object delete "$CONTROL/retained/handbook.txt" --remote --env-file=.env.management
npx wrangler r2 bucket delete "$BUCKET" --env-file=.env.management
npx wrangler r2 bucket delete "$CONTROL" --env-file=.env.management
npx wrangler r2 bucket list --env-file=.env.management
出现提示时,确认每个准确的名称。成功的存储桶列表中不能出现这两个名称。不要为了让全局列表为空而删除其他存储桶。
撤销实验凭据并退出登录
在本步骤中,关闭本实验留下的访问权限。在 R2 API Tokens 页面中,仅撤销以本实验命名的对象令牌。在个人资料的 API Tokens 页面中,撤销为本实验创建的独立 R2 管理令牌。删除存储桶不会撤销令牌,Wrangler 退出登录也不会撤销 S3 凭据。
撤销后,删除本地凭据文件,并让此 VM 退出登录:
rm .env.s3 .env.management signed.json
npx wrangler logout
检查结构化身份信息。退出登录时出现非零状态码是预期行为:
npx wrangler whoami --json || true
确认输出中包含 loggedIn: false;保留你在 Dashboard 中的普通登录状态。平台会检查本地凭据是否已删除以及 Wrangler 是否已退出登录。撤销两个令牌需要在 Dashboard 中手动完成;在本实验候选方案中,平台不会仅根据文件删除情况推断令牌是否已撤销。
总结
创建具有范围限制的私有文件访问权限,验证预签名访问的成功与拒绝边界,区分公开访问与授权,并撤销实验凭据。



