临时网盘功能:密码分发 + Token 下载

2026年6月15日 blogTech 6 分钟阅读 3 次阅读
📖 文章摘要

网盘功能支持密码分发 + 5 分钟 Token 下载。本文详解时间窗口算法、无状态 Token 验证、双重上传认证和前端页面的完整实现。

临时网盘功能设计

博客附带了一个临时网盘功能,适用于向读者分发文件。设计原则是安全足够用、代码足够轻量

业务场景

假设你写了一个工具脚本,想分享给读者。传统做法是挂网盘链接或 GitHub Releases,但两者都有问题——网盘链接会过期,GitHub 需要账号。

本博客的网盘方案:上传时设置下载密码和次数限制,生成分享链接;访客输入密码即可下载。

业务流程

上传者:填写管理员密码 + 文件密码 + 文件 → 获取分享链接
              ↓
访客:访问链接 → 输入文件密码 → 获取下载 token(5 分钟有效)
              ↓
        使用 token 下载文件
              ↓
        后台可查看下载记录

核心 API 实现

Token 生成(时间窗口算法)

TOKEN_WINDOW = 300  # 5 分钟

def _make_token(share_id: int, password_hash: str) -> str:
    slot = int(datetime.utcnow().timestamp() / TOKEN_WINDOW)
    raw = f"{share_id}:{password_hash}:{slot}".encode()
    return hashlib.sha256(raw).hexdigest()[:16]

Token 基于当前时间窗口计算,无状态——服务器不需要存储 token,验证时重新计算比较即可。

验证时同时检查当前窗口和上一个窗口,防止窗口切换时刻的临界过期:

expected = _make_token(share.id, share.password_hash)
if token != expected:
    # 试上一个时间窗口
    expected_prev = _make_token(share.id, share.password_hash)
    if token != expected_prev:
        raise HTTPException(403, "token 无效或已过期")

文件上传

@router.post("/upload")
async def upload_pan(
    password: str, admin_password: str,
    download_limit: int = 0, expire_days: int = 0,
    file: UploadFile = File(...),
):
    # 1. 验证管理员身份
    admin = db.query(Admin).first()
    if not admin or not verify_password(admin_password, admin.password_hash):
        raise HTTPException(403, "管理员密码错误")

    # 2. 保存文件
    ext = Path(file.filename).suffix
    filename = f"{uuid.uuid4().hex}{ext}"
    filepath = PAN_DIR / filename
    filepath.write_bytes(content)

    # 3. 创建分享记录
    share = Share(
        file_name=file.filename,
        file_path=str(filepath),
        password_hash=hash_password(password),
        password_plain=password,  # 明文存储,方便管理员查看
        download_limit=download_limit,
    )

两种上传方式:

  • /api/pan/upload — 需提供管理员密码(适用于从前端页面上传)
  • /api/pan/admin-upload — 需 JWT 认证(适用于从后台管理上传)

下载文件

@router.get("/{share_id}/download")
def download_file(share_id: int, token: str, db: Session = Depends(get_db)):
    # 验证 token
    if token != expected:
        raise HTTPException(403, "token 无效或已过期")

    # 记录下载 + 递增计数器
    share.download_count += 1
    db.add(DownloadLog(share_id=share.id))
    db.commit()

    return FileResponse(str(filepath), filename=share.file_name)

数据库设计

两张表:

Share — 文件记录

字段 类型 说明
file_name VARCHAR 原始文件名(用户下载时看到的名字)
file_path VARCHAR 服务器上的实际路径(UUID 重命名)
password_hash VARCHAR SHA256
password_plain VARCHAR 明文(管理员查看用)
download_limit INT 0=不限
download_count INT 当前已下载次数
expire_at DATETIME 过期后自动拒绝下载

DownloadLog — 下载记录

字段 说明
share_id 文件 ID
downloaded_at 下载时间

前端页面

  • /pan/ — 上传页面(输入管理员密码后显示上传表单)
  • /pan/[id] — 下载页面(输入文件密码后显示下载按钮和倒计时)
  • 后台 /admin/pan — 文件管理列表(查看密码明文、复制链接、删除、查看下载记录)

安全措施总结

  1. SHA256 密码存储
  2. 无状态 token(时间窗口算法,5 分钟有效)
  3. 上传权限控制(管理员密码 或 JWT)
  4. 下载次数限制
  5. 文件过期自动失效
  6. 文件物理存储使用 UUID 重命名,防止路径遍历
最后更新:2026年6月29日CC BY-NC-SA 4.0

评论

暂无评论,来写第一条吧

© 2026 My Blog. Built with Nuxt.js + FastAPI.