轻量私有云存储 · 说明 (v6)
基于 Cloudflare Workers + R2 + D1 的轻量级私有云盘,支持双 Token 无感认证、安全短路径分享、游标分页、Range 下载与 /init 自动迁移。
管理员密码以 PBKDF2 哈希存入 D1。access JWT 有效期 15 分钟且只驻留页面内存,浏览器通过 7 天设备令牌自动续期,主动退出会在服务端撤销当前设备与 access JWT。
v6 主要更新
- 双 Token 认证:access JWT 固定 15 分钟且只保存在内存;设备令牌固定 7 天并通过
HttpOnly + Secure + SameSite=Strict Cookie 保存,刷新时轮换并检测复用。
- 服务端撤销:主动退出删除当前设备令牌,并将仍有效的 access JWT 写入
pan_token_blacklist;新增黑名单时顺带清理过期项。
- D1 单一凭证源:管理员密码以 PBKDF2 哈希存入
pan_config,认证状态存 pan_device_tokens 与 pan_token_blacklist,不提供普通用户账号。
- 认证表收敛:schema v6 会强制规范化设备令牌表约束并撤销旧设备会话,避免旧角色字段或隐藏约束影响登录。
- 自动初始化迁移:访问
/init 自动识别空数据库、旧版 v2 或最新结构,所有网盘表统一迁移到 pan_*。
- 页面配置:仅使用
INIT_SECRET、JWT_SECRET、DOWNLOAD_SECRET 三个 Worker Secret,运行参数统一保存在 pan_config。
- 对象隔离:新上传对象统一写入 R2 的
pan/ 目录;初始化不复制或移动旧 R2 对象。
- 短路径分享:永久分享与临时下载统一使用
https://域名/key,分享页面直接显示并复制完整链接。
- 安全加固:密码读取失败时拒绝登录;JWT 和下载令牌均采用 HMAC-SHA256;每次受保护请求校验设备与黑名单;登录失败按 IP 限流。
- 统一分片上传:所有非空文件均使用 R2 Multipart Upload;32MiB 以内为单分片,较大文件按 32MiB 切片并发上传。单文件默认上限 1GiB,可在管理面板中调整,硬上限 10GiB。
- 全屏拖拽上传:文件拖入页面任意位置都会显示全屏投放提示,松开后自动进入统一分片上传流程。
- 上传一致性:强制校验
Content-Length,容量与每日限额原子预留,对象 key 使用 UUID;取消分片会释放容量,删除使用 D1 outbox 重试。
- 列表扩展:文件、分享和隐藏列表采用游标分页;R2 同步按页暂存,不再一次性把整个桶加载到内存。
- 下载断点续传:支持
Range、206、Content-Range、Accept-Ranges、ETag 和 HEAD;支持 Range 的下载器可继续未完成下载。
- 部署可控:
/init 使用迁移锁与 schema 版本记录,重复执行只校验结构,不覆盖现有数据。
使用场景
- 个人文件备份与同步:随时上传、下载文件,跨设备访问,数据安全存储于 Cloudflare R2。
- 文件分享与分发:生成
域名/key 形式的独立分享链接,可自定义 key、随时隐藏,适合临时或长期文件分享。
- 团队协作分发:通过独立分享链接向小团队分发文档、图片、软件包,无需开放管理权限。
- 静态资源托管:绑定自定义域名至 R2 存储桶,作为图床或静态资源 CDN,加速网站访问。
- 临时文件中转:上传大文件生成分享链接,突破邮件附件限制,轻松传递资料。
核心功能
- 文件管理:单文件默认 1GiB、最大 10GiB 的自动分片上传,多文件队列、断点续传、下载、删除,以及图片、视频、文本三类 UI 在线预览;各类扩展名和预览大小上限可在管理面板中独立配置。
- 分享系统:生成 128 位随机永久短路径,列表展示完整 URL,可修改路径、复制或隐藏分享。
- 权限控制:仅管理员可登录,管理面板包含密码修改、网站设置、列表同步、隐藏管理。
- 隐藏功能:管理员可在管理面板恢复或删除隐藏文件;隐藏分享仅从常规列表移除,完整链接仍可复制、访问或下载。
- 存储同步:自动对比 R2 的
pan/ 目录与数据库,补全缺失文件并清理新目录中的无效元数据。
- 智能下载:预览页保留
/key 短路径,支持下载源文件和文本一键复制;访问 /key?d=1 可直接下载,视频与下载继续支持 Range 分段读取。
- 用量监控:实时显示存储空间、每日操作限额。
主要资源操作
| 操作场景 | D1 读 | D1 写 | R2 A类 | R2 B类 |
| 首页加载 | 站点配置、密码状态与用量 | 0 | 0 | 0 |
| 登录 / 续期 | 密码、限流或设备查询 | 设备登记或活跃时间 | 0 | 0 |
| 统一分片上传 | 认证、会话与分片查询 | 按分片登记 | 单分片约 3 次;1GiB 约 34 次;10GiB 约 322 次 | 0 |
| 下载或预览 | 最多 1 条文件查询 | 0 | 0 | 1 次 GET |
| 删除文件 | 认证与文件查询 | outbox 与元数据清理 | 1 次 DELETE | 0 |
| 游标列表 | 认证及每页最多 21 行 | 0 | 0 | 0 |
| 列表同步 | 认证并按 R2 页查询 | 每批最多 50 条 | 每 500 对象 1 次 LIST | 0 |
每次受保护 API 会用一次 D1 查询确认设备仍有效且 jti 未被拉黑。修改管理员密码会删除全部设备令牌,所有设备必须重新登录。
Cloudflare Free 关键限制
| 资源 | 免费额度或限制 | 本项目影响 |
| Workers 请求 | 100,000 次/天 | 每个分片均计一次 Worker 请求 |
| Workers 请求体 | Free 计划上限 100MB | 1GB 必须由浏览器拆成多个请求 |
| Workers CPU / 内存 | 每次 10ms CPU / 128MB 内存 | 分片直接流入 R2,禁止在 Worker 中聚合 |
| R2 存储 | 10 GB-month/月 | 默认总容量设置为 6GB |
| R2 Class A | 1,000,000 次/月 | 创建上传、上传分片、完成上传、DELETE、LIST |
| R2 Class B | 10,000,000 次/月 | 下载与预览 GET |
加密与认证机制
当前实现使用密码派生、HMAC 签名、随机设备凭证和服务端撤销共同保护管理入口。各机制职责独立,不把签名误当成内容加密。
当前安全基线:管理员密码不可逆存储,access JWT 只驻留页面内存,设备令牌只通过 HttpOnly Cookie 交给浏览器,所有认证状态统一落入 D1 的 pan_* 表。
100,000PBKDF2 派生迭代次数
15 分钟access JWT 固定有效期
7 天设备令牌绝对有效期
30 秒令牌轮换并发宽限
管理员密码保护
PBKDF2-HMAC-SHA512
每次设置密码都会生成独立的 16-byte 随机盐,按 Cloudflare Workers 单次 PBKDF2 上限执行 100,000 次派生并得到 32-byte 结果。D1 只保存算法、迭代次数、盐和派生值。
pbkdf2_sha512$100000$salt$hash
验证与旧数据升级
登录时重新派生候选密码并进行固定时序比较。新密码要求 6-128 位;旧版明文密码仅在首次成功登录时读取一次,随后立即替换为 PBKDF2 记录。
PBKDF2 是单向密码派生,不是可逆加密。Cloudflare workerd 会拒绝单次超过 100,000 次的 PBKDF2,因此当前使用平台允许的最大迭代次数并选择 SHA-512;弱密码仍可能遭受离线猜测,应使用足够长且唯一的密码。
双 Token 无感认证
登录签发
密码验证成功后,服务器创建独立设备记录,同时签发短期 access JWT。
接口访问
客户端使用 Authorization: Bearer。每次受保护请求都检查设备状态和 jti 黑名单。
无感续期
access JWT 到期前通过设备 Cookie 请求新凭证。刷新不会延长设备令牌原有的 7 天绝对期限。
主动退出
服务器删除当前设备,并把仍有效 access JWT 的 jti 拉黑到其自然过期时间。
access JWT
使用 HS256 和独立 JWT_SECRET 签名,包含 type、jti、deviceId、iat、exp。它只保存在 JS 内存中,刷新页面后通过设备 Cookie 恢复。
JWT 载荷只是 Base64URL 编码,客户端可以读取,不能在其中放置密码或 Secret。
设备令牌
原始值由 32-byte 安全随机数生成,仅存在于 __Host-pan_device Cookie。Cookie 使用 HttpOnly、Secure、SameSite=Strict,且不设置 Domain。
D1 只保存 SHA-256 摘要。令牌每次刷新都会轮换,前一个摘要仅保留 30 秒用于并发请求;宽限结束后再次使用会撤销该设备。
临时下载令牌
下载链接使用独立 DOWNLOAD_SECRET 执行 HMAC-SHA256 签名,载荷只包含对象 key 和过期时间。默认时效由 pan_config.download_ttl 控制。
链接本身是 bearer credential,持有完整 URL 的人可在过期前访问对应文件。
浏览器侧防护
HTML 响应为内联脚本生成逐页 CSP nonce,script-src 不允许普通内联脚本。刷新和退出接口还会同时检查 Origin 与 Sec-Fetch-Site。
access JWT 不写入 localStorage 或 sessionStorage,降低持久化泄露风险。
撤销与黑名单
- 设备撤销:退出登录或修改管理员密码时删除相应设备记录,后续受保护 API 会立即拒绝该设备签发的 access JWT。
- access JWT 拉黑:主动退出时把当前
jti 写入 pan_token_blacklist,防止尚未到期的 JWT 继续使用。
- 触发式清理:收到新的拉黑请求时顺带删除已经过期的黑名单记录,不依赖 KV、Queue 或定时任务。
- 密码变更:成功修改管理员密码后清空全部设备令牌,所有浏览器都必须重新登录。
Secret 职责隔离
INIT_SECRET
只验证 /init 请求。首次初始化成功后锁定管理员密码入口,后续仅允许执行 schema 迁移。
Worker Secret,不写入 D1
JWT_SECRET
只用于 access JWT 的 HMAC-SHA256 签名与验证。
Worker Secret,不返回客户端
DOWNLOAD_SECRET
只用于临时下载链接的 HMAC-SHA256 签名与验证。
Worker Secret,不返回客户端
三个 Secret 都要求至少 32 位,必须分别随机生成。任何一个 Secret 都不应复用管理员密码,也不应提交到源码仓库。
保护边界
当前不是端到端文件加密
文件内容按原始字节写入 R2,本项目没有在浏览器端使用用户密钥加密文件。传输安全依赖 HTTPS/TLS,静态存储保护依赖 Cloudflare 平台;拥有 Worker、R2 或有效分享链接访问权限的一方仍可读取文件内容。
- JWT 和下载令牌提供完整性与真实性校验,不隐藏载荷内容。
- SHA-256 设备摘要依赖原始令牌的高随机性,不能替代低强度用户密码的慢速派生。
- 分享链接属于持有即授权的访问凭证,应通过不可预测路径、最小传播范围和及时删除控制风险。
- 若需要零知识或端到端加密,需要增加浏览器侧文件加密、密钥派生、恢复机制和分片密文格式,这不属于当前实现。
详细配置教程 (v6)
从零部署当前 Worker。绑定 R2 与 D1,并配置 INIT_SECRET、JWT_SECRET、DOWNLOAD_SECRET;数据库由 /init 自动初始化或迁移。
0. 准备工作
- 一个 Cloudflare 账号(注册)
- 一个可绑定的 域名(推荐腾讯云 .cn 域名)
- 域名的 DNS 服务器已修改为 Cloudflare 提供的地址
1. 创建 Cloudflare 资源
1.1 创建 R2 存储桶并绑定域名
- 登录 Cloudflare Dashboard → 左侧菜单【R2】。
- 点击“创建存储桶”,名称如
my-cloud-pan,区域选择“自动”。
- 创建后进入桶设置 → 公开访问 → 自定义域,绑定子域名(如
files.yourdomain.com)(可选,Worker 可直接访问私有桶)。
1.2 创建 D1 数据库
- 左侧菜单【Workers & Pages】→【D1】。
- 点击“创建数据库”,名称如
cloud-pan-db。
- 创建空数据库即可,不需要在控制台手工执行 SQL。
Worker 部署后访问 https://你的域名/init。页面会自动创建 pan_* 表,或迁移旧版 v2 的无前缀表;migrations/ 仅作为人工兜底。
2. 创建 Worker 并绑定资源
- 进入【Workers & Pages】,点击“创建应用程序”→“创建 Worker”。
- 命名 Worker(如
cloud-pan-worker),点击“部署”。
- 进入 Worker 编辑页面 → “设置”→“变量和机密”。
- 在“R2 存储桶绑定”中添加:变量名
R2,选择你的桶。
- 在“D1 数据库绑定”中添加:变量名
DB,选择你的 D1。
3. 绑定自定义域名到 Worker
- 进入 Worker 详情页 →“触发器”→“添加自定义域”。
- 输入你的域名(如
pan.yourdomain.com),Cloudflare 自动配置 DNS。
- 等待几分钟后通过该域名访问云盘。
4. Secret 与 /init 配置
在 Worker 设置 → 变量和机密中,只添加以下三个 Secret:
/init 初始化口令:使用独立的 INIT_SECRET。首次初始化成功后入口会锁定管理员密码,后续 /init 只能执行结构迁移,密码需登录后修改。
安全提醒:
INIT_SECRET、JWT_SECRET 与 DOWNLOAD_SECRET 均必须设置,长度至少 32 位,分别生成并保存为 Secret。
- 不要添加密码或上传限额等普通 Worker 变量;管理员密码与运行参数统一保存在 D1。
- 任一签名密钥缺失或过短时 Worker 会拒绝签发令牌,不会回退到内置默认值。
- 部署 Worker 后访问
https://你的域名/init。
- “初始化口令”填写
INIT_SECRET,并在 pan_config 初始化管理员密码。
- 首次初始化需输入 6-128 位管理员密码,不限制字符种类。
- 页面会创建或迁移全部
pan_* 表,并写入默认 pan_config。
- 管理员登录后在“站点配置”修改容量、上传上限、每日限额、下载时效,以及图片、视频、文本三类预览格式与 MiB 大小上限。
5. 纯 Worker 大文件分片上传
当前版本所有文件统一使用 R2 Multipart Upload,不再保留单请求上传 API,也不再配置单请求上传上限。浏览器固定按最大 32MiB 分片,经多个 Worker 请求流式写入 R2;不需要独立服务器、Pages Functions、Queues 或 Durable Objects。
- 客户端:使用原生
Blob.slice(),最大 32MiB/片、单文件分片并发 3。文件之间按队列依次上传,小文件使用一个分片,1GiB 文件共 32 片,10GiB 文件共 320 片。
- 初始化:
POST /api/multipart/create 校验 JWT、文件名和总大小,原子预留容量,在 R2 的 pan/ 目录创建 multipart upload,并把会话写入 pan_multipart_uploads。
- 传片:
PUT /api/multipart/part?session=...&part=... 将 request.body 直接传给 uploadPart(),Worker 不调用 arrayBuffer(),只把返回的 etag 写入 D1。
- 断点续传:
GET /api/multipart/status?session=... 返回已完成 part number;浏览器通过 localStorage 记住同一文件的会话,只重传缺片。
- 完成与取消:
POST /api/multipart/complete 按 part number 合并并写入文件表;POST /api/multipart/abort 终止 R2 会话并释放预留容量。
- 清理:会话保留 7 天。创建新会话或执行列表同步时会清理过期记录;R2 端仍建议保留 7 天自动终止未完成 multipart 的生命周期规则。
R2 要求除最后一片外每片至少 5MiB,最多 10,000 片。32MiB 方案上传 1GiB 约产生 34 次 R2 Class A 操作,10GiB 约产生 322 次;使用 10GiB 上限时,请同时将总容量调整到至少 10GiB,并将每日 A 类操作上限调整到至少 322。完整限制见
R2 Upload Objects。
6. 部署与验证
- 将 Worker 代码完整粘贴并部署,再访问
/init 完成 D1 初始化或旧版 v2 迁移。
- 确认必选的
R2、DB 绑定及 INIT_SECRET、JWT_SECRET、DOWNLOAD_SECRET 已生效。
- 使用
/init 写入 pan_config 的管理员密码,并确认所有 D1 表均以 pan_ 开头。
- 确认新上传对象 key 以
pan/ 开头;/init 不会复制或移动旧 R2 对象。
- 确认 access JWT 每 15 分钟到期前无感续期,设备令牌 Cookie 最长保留 7 天;点击退出后旧 access JWT 与设备 Cookie 都不能继续使用。
- 创建分享后,确认列表显示
https://你的域名/key 格式的完整链接,并测试复制和访问。
- 用音视频测试
Range 请求;隐藏分享应从常规列表消失,但原链接仍返回文件,并验证列表的上一页、下一页与“更多”。