构建安全的文件上传系统:FastAPI + JWT 认证 + Streamlit 前端 + SQLite 数据库

【学习记录】构建安全的文件上传系统:FastAPI + JWT 认证 + Streamlit 前端 + SQLite 数据库

在企业级应用中,文件上传功能通常需要集成用户认证数据隔离------每个用户只能管理自己的文件,并且操作需要经过身份验证。本文基于 FastAPI、JWT、Streamlit 和 SQLite 构建一个完整的文件上传系统,涵盖用户登录(Token 签发)、安全上传、文件记录存储与查询,并详细讲解其中的关键技术点。代码完整可直接运行,适合作为文档管理、RAG 知识库上传组件的原型。


📌 系统架构与技术栈

组件 技术 职责
后端框架 FastAPI 提供 RESTful API,处理认证、文件上传、记录查询
认证机制 JWT (JSON Web Token) 无状态身份验证,签发与验证 Token
前端界面 Streamlit 提供用户登录、文件上传、历史记录查看的交互界面
数据库 SQLite + aiosqlite 存储上传记录(用户 ID、文件名、时间等),支持异步操作
文件存储 本地磁盘 保存文件,使用 UUID 重命名,防止冲突和路径遍历攻击

核心流程

  1. 用户通过前端输入用户名,调用后端 /login 接口获取 JWT Token。
  2. 前端将 Token 保存在 st.session_state 中,后续请求附加在 Authorization: Bearer <token> 头。
  3. 后端通过依赖 Depends(get_current_user) 验证 Token 并提取 user_id,只有验证通过才能上传文件或查询记录。
  4. 上传文件时,后端检查文件大小,生成唯一文件名保存到磁盘,并将记录写入 SQLite 数据库。
  5. 用户可以查询自己的上传历史,数据按用户隔离。

🔐 JWT 认证原理与实现

JWT 结构

JWT 由三部分组成:HeaderPayloadSignature,用 . 分隔。

  • Header:指定签名算法(如 HS256)。
  • Payload :存放声明(claims),如用户 ID (sub)、过期时间 (exp) 等。
  • Signature:对前两部分使用密钥签名,防止篡改。

后端 JWT 工具函数

python 复制代码
def create_access_token(user_id: str):
    payload = {"sub": user_id, "exp": datetime.utcnow() + timedelta(hours=24)}
    return jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM)

security = HTTPBearer()

async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)):
    token = credentials.credentials
    try:
        payload = jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM])
        user_id = payload.get("sub")
        if not user_id:
            raise HTTPException(401, "无效Token")
        return user_id
    except jwt.ExpiredSignatureError:
        raise HTTPException(401, "Token已过期")
    except jwt.InvalidTokenError:
        raise HTTPException(401, "Token无效")

关键点

  • create_access_token:生成有效期 24 小时的 Token,sub 字段存放用户名。
  • HTTPBearer:FastAPI 提供的安全工具,自动从请求头提取 Authorization: Bearer <token>
  • get_current_user:作为依赖项,可被任何需要认证的路由使用,验证失败时抛出 401。

🗄️ 数据库设计(SQLite)

upload_records 结构:

字段 类型 说明
id INTEGER 自增主键
original_filename TEXT 用户上传时的原始文件名
saved_filename TEXT 实际保存的文件名(UUID)
saved_path TEXT 文件存储的绝对路径
file_size INTEGER 文件大小(字节)
user_id TEXT 上传者 ID(来自 JWT)
upload_time TIMESTAMP 上传时间,默认当前时间

异步操作 :使用 aiosqlite 实现非阻塞数据库写入和查询,避免阻塞 FastAPI 事件循环。

python 复制代码
async def insert_upload_record(...):
    async with aiosqlite.connect(DATABASE_PATH) as db:
        await db.execute(...)
        await db.commit()

📂 文件上传流程

  1. 接收文件 :FastAPI 使用 UploadFile 类型,自动解析 multipart/form-data
  2. 大小检查 :通过 file.file.seek(0, 2) 获取文件总大小,然后 seek(0) 复位指针。
  3. 唯一命名uuid.uuid4().hex 生成 32 位随机字符串,保留原扩展名。
  4. 保存文件 :使用 shutil.copyfileobj 以块形式写入磁盘,内存友好。
  5. 记录入库 :调用 insert_upload_record 异步写入数据库。
  6. 返回响应:JSON 格式返回成功信息及保存的文件名。

安全措施

  • 文件大小限制(默认 50 MB)。
  • 文件名完全由系统生成,忽略用户提供的文件名(仅记录元数据),防止路径遍历攻击。
  • 上传目录 uploaded_files 不在 Web 可访问路径下(若需要下载需额外接口)。

🎨 Streamlit 前端实现

Streamlit 前端负责:

  • 登录表单,获取 Token 并存入 st.session_state
  • 展示文件上传组件,发送携带 Token 的请求。
  • 查询当前用户的上传记录并展示。
  • 退出登录,清空 Session。

Session 状态管理

python 复制代码
if "token" not in st.session_state:
    st.session_state.token = None
if "user_id" not in st.session_state:
    st.session_state.user_id = None

登录组件

python 复制代码
if st.session_state.token is None:
    username = st.text_input("用户名", value="test_user")
    if st.button("登录"):
        resp = requests.post(LOGIN_URL, params={"username": username})
        if resp.status_code == 200:
            data = resp.json()
            st.session_state.token = data["access_token"]
            st.session_state.user_id = data["user_id"]
            st.success("登录成功")
            st.rerun()
    st.stop()

已登录界面上传与查询

python 复制代码
headers = {"Authorization": f"Bearer {st.session_state.token}"}

uploaded_file = st.file_uploader("选择文件")
if uploaded_file and st.button("上传文件"):
    files = {"file": (uploaded_file.name, uploaded_file.getvalue(), uploaded_file.type)}
    resp = requests.post(UPLOAD_URL, files=files, headers=headers)
    # 处理响应...

if st.button("查看我的上传记录"):
    resp = requests.get(RECORDS_URL, headers=headers)
    # 展示记录...

🚀 完整代码(修正版)

后端代码(backend.py

python 复制代码
import os
import shutil
import uuid
import sqlite3
from datetime import datetime, timedelta

import aiosqlite
from fastapi import (
    FastAPI,
    File,
    UploadFile,
    HTTPException,
    Depends
)
from fastapi.responses import JSONResponse
from fastapi.middleware.cors import CORSMiddleware

from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials

import jwt

# =====================================================
# 配置
# =====================================================

UPLOAD_DIR = "./uploaded_files"
DATABASE_PATH = "./upload_records.db"

MAX_FILE_SIZE = 50 * 1024 * 1024

JWT_SECRET = "replace_with_your_secret_key"
JWT_ALGORITHM = "HS256"

os.makedirs(UPLOAD_DIR, exist_ok=True)

# =====================================================
# 初始化数据库
# =====================================================

def init_db():
    conn = sqlite3.connect(DATABASE_PATH)

    cursor = conn.cursor()

    cursor.execute("""
        CREATE TABLE IF NOT EXISTS upload_records (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            original_filename TEXT NOT NULL,
            saved_filename TEXT NOT NULL,
            saved_path TEXT NOT NULL,
            file_size INTEGER NOT NULL,
            user_id TEXT NOT NULL,
            upload_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        )
    """)

    conn.commit()
    conn.close()


# =====================================================
# JWT 工具
# =====================================================

def create_access_token(user_id: str):
    payload = {
        "sub": user_id,
        "exp": datetime.utcnow() + timedelta(hours=24)
    }

    return jwt.encode(
        payload,
        JWT_SECRET,
        algorithm=JWT_ALGORITHM
    )


security = HTTPBearer()


async def get_current_user(
    credentials: HTTPAuthorizationCredentials = Depends(security)
):
    token = credentials.credentials

    try:
        payload = jwt.decode(
            token,
            JWT_SECRET,
            algorithms=[JWT_ALGORITHM]
        )

        user_id = payload.get("sub")

        if not user_id:
            raise HTTPException(
                status_code=401,
                detail="无效Token"
            )

        return user_id

    except jwt.ExpiredSignatureError:
        raise HTTPException(
            status_code=401,
            detail="Token已过期"
        )

    except jwt.InvalidTokenError:
        raise HTTPException(
            status_code=401,
            detail="Token无效"
        )


# =====================================================
# 数据库操作
# =====================================================

async def insert_upload_record(
    original_filename,
    saved_filename,
    saved_path,
    file_size,
    user_id
):
    async with aiosqlite.connect(DATABASE_PATH) as db:

        await db.execute(
            """
            INSERT INTO upload_records
            (
                original_filename,
                saved_filename,
                saved_path,
                file_size,
                user_id,
                upload_time
            )
            VALUES (?, ?, ?, ?, ?, ?)
            """,
            (
                original_filename,
                saved_filename,
                saved_path,
                file_size,
                user_id,
                datetime.now()
            )
        )

        await db.commit()


# =====================================================
# FastAPI
# =====================================================

app = FastAPI(title="文档上传服务")

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)


@app.on_event("startup")
async def startup():
    init_db()


@app.get("/")
async def root():
    return {"message": "服务运行正常"}


# =====================================================
# 示例登录接口
# 实际项目应连接用户系统
# =====================================================

@app.post("/login")
async def login(username: str):

    token = create_access_token(username)

    return {
        "access_token": token,
        "token_type": "bearer",
        "user_id": username
    }


# =====================================================
# 上传文件
# =====================================================

@app.post("/upload")
async def upload_document(
    file: UploadFile = File(...),
    current_user: str = Depends(get_current_user)
):

    file.file.seek(0, 2)
    size = file.file.tell()

    if size > MAX_FILE_SIZE:
        raise HTTPException(
            status_code=413,
            detail=f"文件超过 {MAX_FILE_SIZE // (1024*1024)}MB"
        )

    file.file.seek(0)

    original_filename = file.filename

    ext = os.path.splitext(original_filename)[1]

    unique_filename = f"{uuid.uuid4().hex}{ext}"

    save_path = os.path.join(
        UPLOAD_DIR,
        unique_filename
    )

    try:
        with open(save_path, "wb") as buffer:
            shutil.copyfileobj(
                file.file,
                buffer
            )

    except Exception as e:
        raise HTTPException(
            status_code=500,
            detail=f"保存失败: {str(e)}"
        )

    await insert_upload_record(
        original_filename=original_filename,
        saved_filename=unique_filename,
        saved_path=save_path,
        file_size=size,
        user_id=current_user
    )

    return JSONResponse(
        content={
            "status": "success",
            "message": "上传成功",
            "user_id": current_user,
            "saved_filename": unique_filename,
            "file_size": size
        }
    )


# =====================================================
# 查询当前用户上传记录
# =====================================================

@app.get("/records")
async def get_records(
    current_user: str = Depends(get_current_user),
    limit: int = 50
):

    async with aiosqlite.connect(DATABASE_PATH) as db:

        cursor = await db.execute(
            """
            SELECT *
            FROM upload_records
            WHERE user_id = ?
            ORDER BY upload_time DESC
            LIMIT ?
            """,
            (
                current_user,
                limit
            )
        )

        rows = await cursor.fetchall()

        columns = [
            desc[0]
            for desc in cursor.description
        ]

        return [
            dict(zip(columns, row))
            for row in rows
        ]


if __name__ == "__main__":
    import uvicorn

    uvicorn.run(
        app,
        host="0.0.0.0",
        port=8005
    )

前端代码(frontend.py

python 复制代码
import streamlit as st
import requests

BASE_URL = "http://localhost:8005"

LOGIN_URL = f"{BASE_URL}/login"
UPLOAD_URL = f"{BASE_URL}/upload"
RECORDS_URL = f"{BASE_URL}/records"

st.set_page_config(page_title="文档上传系统", page_icon="📄")
st.title("📄 文档上传系统")

# Session 状态
if "token" not in st.session_state:
    st.session_state.token = None
if "user_id" not in st.session_state:
    st.session_state.user_id = None

# 未登录时显示登录界面
if st.session_state.token is None:
    st.subheader("登录")
    username = st.text_input("用户名", value="test_user")
    if st.button("登录"):
        try:
            resp = requests.post(LOGIN_URL, params={"username": username})
            if resp.status_code == 200:
                data = resp.json()
                st.session_state.token = data["access_token"]
                st.session_state.user_id = data["user_id"]
                st.success("登录成功")
                st.rerun()
            else:
                st.error("登录失败")
        except Exception as e:
            st.error(str(e))
    st.stop()  # 停止后续渲染

# 已登录
st.success(f"当前用户:{st.session_state.user_id}")
headers = {"Authorization": f"Bearer {st.session_state.token}"}

# 文件上传
uploaded_file = st.file_uploader("选择文件")
if uploaded_file:
    st.write("文件名:", uploaded_file.name)
    st.write("大小:", uploaded_file.size)
    if st.button("上传文件"):
        files = {"file": (uploaded_file.name, uploaded_file.getvalue(), uploaded_file.type)}
        try:
            with st.spinner("上传中..."):
                resp = requests.post(UPLOAD_URL, files=files, headers=headers)
            if resp.status_code == 200:
                st.success("上传成功")
                st.json(resp.json())
            else:
                st.error(resp.text)
        except Exception as e:
            st.error(str(e))

# 查询记录
st.markdown("---")
if st.button("查看我的上传记录"):
    try:
        resp = requests.get(RECORDS_URL, headers=headers)
        if resp.status_code == 200:
            records = resp.json()
            if not records:
                st.info("暂无记录")
            else:
                st.subheader("上传历史")
                for rec in records:
                    st.markdown(
                        f"""
                        **文件名:** {rec['original_filename']}
                        **保存名:** `{rec['saved_filename']}`
                        **上传时间:** {rec['upload_time']}
                        **文件大小:** {rec['file_size']} 字节
                        ---
                        """
                    )
        else:
            st.error(resp.text)
    except Exception as e:
        st.error(str(e))

# 退出登录
if st.button("退出登录"):
    st.session_state.token = None
    st.session_state.user_id = None
    st.rerun()

⚙️ 运行与部署

本地运行

  1. 安装依赖requirements.txt):

    复制代码
    fastapi
    uvicorn
    streamlit
    requests
    python-multipart
    aiosqlite
    PyJWT
  2. 启动后端

    bash 复制代码
    python backend.py

    后端默认运行在 http://0.0.0.0:8005

  3. 启动前端

    bash 复制代码
    streamlit run frontend.py --server.address 0.0.0.0 --server.port 6006

    前端将运行在 http://0.0.0.0:6006

  4. 访问 :浏览器打开前端地址,输入任意用户名(如 test_user),上传文件测试。

生产环境注意事项

  • JWT Secret :不要硬编码,使用环境变量 JWT_SECRET
  • HTTPS:生产环境应启用 HTTPS,防止 Token 窃取。
  • 文件存储 :可将 UPLOAD_DIR 改为云存储(如阿里云 OSS、MinIO)。
  • 数据库:SQLite 适合中小规模,高并发可迁移至 PostgreSQL。
  • 认证增强:实际项目应验证用户密码,而不是仅凭用户名签发 Token。

🧠 技术要点总结

模块 核心技术 关键代码片段
JWT 签发 jwt.encode create_access_token
JWT 验证 jwt.decode + HTTPBearer get_current_user 依赖
数据库异步 aiosqlite async with aiosqlite.connect
文件上传 UploadFile + shutil.copyfileobj upload_document
前端状态 Streamlit session_state st.session_state.token
跨域 CORSMiddleware allow_origins=["*"]

📈 扩展方向

  • 文件下载接口:通过保存的路径生成签名 URL 或提供直接下载。
  • 分片上传:支持大文件断点续传。
  • 文件类型过滤:限制上传的扩展名或 MIME 类型。
  • 用户管理:集成真实的用户注册/登录(密码加密、数据库存储用户表)。
  • 日志审计:记录上传/下载行为,便于追溯。

🎯 结语

本文从零构建了一个具备认证授权文件上传记录存储的完整系统,代码清晰、可扩展,适合作为内部工具或知识库管理系统的原型。通过理解 JWT 认证、异步数据库操作以及 Streamlit 会话管理,你可以快速搭建类似的企业级应用。

相关推荐
一条泥憨鱼1 小时前
DTO、VO、PO、BO 到底该怎么区分?
java·数据库·状态模式·对象·印象笔记·对象类型
2601_961845421 小时前
2026四级作文预测26年|英语四级写作范文+模板PDF
java·数据库·spring·eclipse·pdf·tomcat·hibernate
DevOpenClub1 小时前
用 OCR、PDF 转文本和摘要接口构建 RAG 文档入库 Agent
数据库·pdf·ocr
睡不醒男孩0308237 小时前
第二篇:深入探索开源数据库高可用:构建基于CLup的PostgreSQL生产级高可用与读写分离架构
数据库·postgresql·开源·clup
aaaffaewrerewrwer9 小时前
免费在线 AVIF 转 WebP 工具推荐(支持批量转换 + 浏览器本地处理 + 无需上传)
安全·个人开发
zhengfei61110 小时前
【渗透工具】Payloader — 渗透测试辅助平台(payload一键所有)
网络·安全·web安全
Micro麦可乐10 小时前
Spring Boot 实战:从零设计一个短链系统(含完整代码与数据库设计)
数据库·spring boot·后端·哈希算法·雪花算法·短链系统
码农阿豪10 小时前
从零到一:Spring Boot快速接入金仓数据库实战
数据库·spring boot·后端
鼎讯信通10 小时前
风电光缆运维提质增效:G-4000A 光缆故障追踪仪破解风场巡检难题
运维·网络·数据库