【Fastapi学习笔记(6)】—— Fastapi文件上传、请求头自动转换

文件上传

python 复制代码
# 原生写法(仅演示功能,生产有安全&性能隐患)
@app.post("/uploadfile/")
async def create_upload_file(file: UploadFile):
    with open(f"uploads/{file.filename}", "wb") as buffer:
        shutil.copyfileobj(file.file, buffer)
    return {"filename": file.filename, "message": "文件上传成功"}

实际项目中,上传文件时应注意:1) 校验文件类型和大小;2) 使用安全的文件名(避免路径遍历攻击);3) 限制上传目录的权限;4) 对于大文件使用流式处理而非一次性读取。

下面按 4 个要点逐一讲解。


一、校验文件类型 & 文件大小

风险

  1. 文件类型不校验
    用户可上传 .exe.php.sh 等可执行脚本,一旦被执行,直接造成服务器入侵、木马植入
  2. 文件大小不限制
    恶意用户上传超大文件,占满服务器磁盘、耗尽带宽,引发磁盘溢出、DOS 攻击

解决方案 & 代码实现

1)限制文件大小

FastAPI 可结合请求体限制,也可读取时判断字节数;推荐全局/接口级限制最大体积。

2)校验文件后缀 + MIME 类型

双重校验(只判断后缀可被绕过,必须结合 content_type)。

python 复制代码
import os
import shutil
from fastapi import FastAPI, UploadFile, HTTPException

app = FastAPI()

# 配置项
UPLOAD_DIR = "uploads"
# 允许的文件后缀
ALLOWED_SUFFIX = {".jpg", ".jpeg", ".png", ".gif", ".pdf"}
# 允许的 MIME 类型
ALLOWED_CONTENT_TYPE = {"image/jpeg", "image/png", "image/gif", "application/pdf"}
# 单文件最大 10MB
MAX_FILE_SIZE = 10 * 1024 * 1024

os.makedirs(UPLOAD_DIR, exist_ok=True)

@app.post("/uploadfile/")
async def create_upload_file(file: UploadFile):
    # -------- 1. 校验文件大小 --------
    # UploadFile 可通过 file.size 获取大小(FastAPI 新版支持)
    if file.size > MAX_FILE_SIZE:
        raise HTTPException(status_code=400, detail="文件过大,最大支持 10MB")

    # -------- 2. 校验文件类型 --------
    # 校验 MIME 类型
    if file.content_type not in ALLOWED_CONTENT_TYPE:
        raise HTTPException(status_code=400, detail="不支持的文件类型")
    
    # 校验文件后缀
    file_suffix = os.path.splitext(file.filename)[1].lower()
    if file_suffix not in ALLOWED_SUFFIX:
        raise HTTPException(status_code=400, detail="文件后缀不合法")

    # 后续保存逻辑...
    save_path = os.path.join(UPLOAD_DIR, file.filename)
    with open(save_path, "wb") as buffer:
        shutil.copyfileobj(file.file, buffer)

    return {"filename": file.filename, "message": "文件上传成功"}

二、安全文件名,防止 路径遍历攻击(路径穿越)

什么是路径遍历攻击?

用户恶意构造文件名,跳出 uploads 目录,写入服务器任意位置

举个恶意示例:

客户端上传文件,文件名填写:

复制代码
../../etc/passwd

你的原代码拼接路径:

python 复制代码
f"uploads/{file.filename}"
# 最终路径: uploads/../../etc/passwd
# 等价于: 服务器根目录 /etc/passwd

攻击者可以覆盖系统配置文件、写入恶意脚本,服务器直接沦陷。

问题根源

直接使用前端传来的 file.filename 拼接路径,信任客户端输入

安全方案(二选一,生产常用)

方案1:提取纯文件名,剔除路径(基础防护)

os.path.basename() 只保留文件名,丢掉所有 ..//\ 路径字符:

python 复制代码
# 恶意文件名 ../../etc/passwd → 只得到 passwd
safe_filename = os.path.basename(file.filename)
save_path = os.path.join(UPLOAD_DIR, safe_filename)
方案2:生成随机唯一文件名(生产最优,彻底杜绝风险)

不再使用前端文件名,用 uuid / 时间戳生成新名称,保留原后缀:

  • 彻底防路径遍历
  • 避免同名文件覆盖
python 复制代码
import uuid

# 拆分原文件名与后缀
suffix = os.path.splitext(file.filename)[1].lower()
# 生成唯一文件名
safe_filename = f"{uuid.uuid4()}{suffix}"
save_path = os.path.join(UPLOAD_DIR, safe_filename)

改造后安全代码片段

python 复制代码
# 安全处理文件名
suffix = os.path.splitext(file.filename)[1].lower()
safe_filename = f"{uuid.uuid4()}{suffix}"
save_path = os.path.join(UPLOAD_DIR, safe_filename)

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

三、限制上传目录权限(服务器系统层面防护)

这是 Linux / 服务器运维层面 的安全配置,和代码无关,但项目必须做。

风险

如果 uploads 目录权限过宽(比如 777),攻击者上传脚本后可直接执行、篡改文件。

目录权限配置(Linux)

  1. 上传目录单独隔离

    不要把上传目录放在网站根目录、程序运行目录,单独划分。

  2. 设置目录权限

bash 复制代码
# 目录仅允许 读写,禁止执行权限(关键!禁止执行脚本)
chmod 755 uploads/
# 所属用户设为运行程序的账号,不要用 root
chown -R appuser:appgroup uploads/
  • 755:所有者可读写执行,其他人仅可读,无写入、无执行
  • 核心原则:上传目录永远禁止脚本执行权限
  1. Nginx/Apache 额外防护
    配置上传目录禁止解析 PHP / Python / Shell 等脚本,即使上传了脚本,也无法运行。

四、大文件:流式处理,不要一次性读取

原代码问题分析

python 复制代码
shutil.copyfileobj(file.file, buffer)

UploadFile 内部是文件流shutil.copyfileobj 本身是流式分块读写,不会一次性把整个文件载入内存

补充:

  • 错误写法(大坑):content = await file.read() 一次性读全部内容到内存
  • 大文件(几百MB/GB)会直接撑爆内存、服务卡死。

流式处理原理

UploadFile.file 是类文件对象,分块读取、分块写入

  1. 从客户端接收一小块数据
  2. 直接写入磁盘
  3. 内存只保留一小块缓冲区,不加载完整文件

大文件优化写法(断点续传/分块上传 拓展)

超大文件(GB 级),单纯流式不够,业界标准:前端分块上传 + 后端合并

FastAPI 原生 UploadFile 配合流式读写就是标准方案,示例:

python 复制代码
# 标准流式保存(适合大文件,内存安全)
with open(save_path, "wb") as f_out:
    # 分块迭代读取流,默认块大小性能足够
    for chunk in iter(lambda: file.file.read(1024 * 8), b""):
        f_out.write(chunk)
  • 每次只读 8KB 块
  • 内存占用极低,支持超大文件

避坑提醒

❌ 绝对不要写这种一次性读取:

python 复制代码
# 大文件直接 OOM,生产禁止!
content = await file.read()
with open(save_path, "wb") as f:
    f.write(content)

五、整合:生产环境完整安全上传代码

把以上 4 点全部落地,可直接用于项目:

python 复制代码
import os
import uuid
import shutil
from fastapi import FastAPI, UploadFile, HTTPException

app = FastAPI()

# 全局配置
UPLOAD_DIR = "uploads"
ALLOWED_SUFFIX = {".jpg", ".jpeg", ".png", ".gif", ".pdf"}
ALLOWED_CONTENT_TYPE = {"image/jpeg", "image/png", "image/gif", "application/pdf"}
MAX_FILE_SIZE = 10 * 1024 * 1024  # 10MB

# 创建上传目录
os.makedirs(UPLOAD_DIR, exist_ok=True)

@app.post("/uploadfile/")
async def create_upload_file(file: UploadFile):
    # 1. 校验文件大小
    if file.size > MAX_FILE_SIZE:
        raise HTTPException(status_code=400, detail="文件超过最大限制(10MB)")

    # 2. 校验文件类型
    if file.content_type not in ALLOWED_CONTENT_TYPE:
        raise HTTPException(status_code=400, detail="非法文件类型")
    
    file_suffix = os.path.splitext(file.filename)[1].lower()
    if file_suffix not in ALLOWED_SUFFIX:
        raise HTTPException(status_code=400, detail="文件后缀不允许")

    # 3. 安全文件名,防路径遍历
    safe_suffix = file_suffix
    safe_filename = f"{uuid.uuid4()}{safe_suffix}"
    save_path = os.path.join(UPLOAD_DIR, safe_filename)

    # 4. 流式写入,适配大文件
    try:
        with open(save_path, "wb") as buffer:
            shutil.copyfileobj(file.file, buffer)
    except Exception as e:
        raise HTTPException(status_code=500, detail="文件保存失败")

    return {
        "origin_filename": file.filename,
        "save_filename": safe_filename,
        "message": "文件上传成功"
    }

六、4 条注意事项极简总结(背诵版)

  1. 校验类型&大小:拦截恶意脚本、超大文件,防入侵和磁盘攻击。
  2. 安全文件名 :不用前端原始名称,用 basename / UUID,防路径遍历攻击
  3. 目录权限 :服务器给上传目录设最小权限,禁止执行脚本
  4. 大文件流式读写 :用文件流分块写入,禁止一次性 read() 全量加载,防内存溢出。


请求头自动转换

一、先讲核心矛盾

  1. HTTP 规范 :请求头字段习惯用 连字符 - ,例:X-TokenUser-AgentAuthorization
  2. Python 语法 :变量名不允许 出现 -x-token 会被解析成「变量 x 减变量 token」,直接语法报错。

所以 FastAPI 做了自动映射转换,解决两边命名规则冲突。


二、FastAPI 转换规则(固定规则,直接记)

规则总览

HTTP 请求头:短横线分隔(kebab-case)

→ FastAPI 代码里:下划线分隔(snake_case)

同时统一转为小写匹配,大小写不敏感。

具体映射逻辑
  1. HTTP 头里的 -(连字符) → 代码变量里换成 _(下划线)
  2. 头部名称大小写无关,HTTP 头本身就不区分大小写

三、分步举例演示

示例1:自定义请求头 X-Token

1. 前端/客户端请求头
http 复制代码
X-Token: abc123456
2. FastAPI 代码写法

X-Token 中的 - 换成 _,变量名写成:x_token

python 复制代码
from fastapi import FastAPI, Header

app = FastAPI()

@app.get("/")
async def read_header(x_token: str | None = Header(None)):
    return {"X-Token 值": x_token}
效果

客户端传 X-Token,代码里用 x_token 正常取值,不用手动解析、不用额外处理


示例2:标准请求头 User-Agent

HTTP 头:User-Agent

代码变量:user_agent

python 复制代码
@app.get("/ua")
async def get_ua(user_agent: str | None = Header(None)):
    return {"User-Agent": user_agent}

示例3:多段连字符 X-App-Version

HTTP 头:X-App-Version

代码变量:x_app_version

python 复制代码
@app.get("/version")
async def get_version(x_app_version: str | None = Header(None)):
    return {"版本": x_app_version}

四、两种等价写法(显式指定名称)

有时候你不想靠自动转换,可以手动指定头部原名,两种写法等价:

写法1:依赖自动转换(推荐、简洁)
python 复制代码
# X-Token → x_token
async def demo(x_token: str = Header(...)):
    ...
写法2:手动设置 name 参数(明确指定头名称)

当变量名不想跟着转换、或者特殊场景时使用:

python 复制代码
async def demo(token: str = Header(..., name="X-Token")):
    ...
  • name="X-Token":明确告诉 FastAPI,这个参数对应 HTTP 头 X-Token
  • 变量名 token 可以自定义,不受转换规则限制

五、关键补充细节

  1. 为什么能自动转?

    FastAPI + Starlette 底层做了封装:拿到原始请求头后,自动把所有 - 替换成 _,再绑定到函数变量。

  2. 大小写完全不敏感

    以下请求头都会被 x_token 接收:

    X-Token
    x-token
    X-TOKEN

  3. 误区:能不能直接用带 - 的变量名?
    ❌ 绝对不行

python 复制代码
# 语法错误!Python 不允许变量名含 -
async def demo(x-token: str = Header(None)):
    pass

Python 解释器会把 - 当成减号运算符,直接报错。


六、完整可运行示例汇总

python 复制代码
from fastapi import FastAPI, Header

app = FastAPI()

# 1. 自动转换:X-Token → x_token
@app.get("/token")
async def get_token(x_token: str | None = Header(None)):
    return {"x_token": x_token}

# 2. 自动转换:User-Agent → user_agent
@app.get("/agent")
async def get_agent(user_agent: str | None = Header(None)):
    return {"user_agent": user_agent}

# 3. 手动指定 header 名称(不依赖自动转换)
@app.get("/custom")
async def custom_header(
    app_key: str | None = Header(None, name="X-App-Key")
):
    return {"app_key": app_key}
测试请求
bash 复制代码
curl http://127.0.0.1:8000/token -H "X-Token: 123abc"
curl http://127.0.0.1:8000/custom -H "X-App-Key: mykey666"

七、一句话总结

HTTP 请求头用 - ,Python 变量不能用 -

FastAPI 自动把头部的 连字符 - 转为下划线 _ ,你只需要把变量名写成下划线风格 就能直接取值;

特殊场景也可以用 Header(name="原始头名") 手动绑定。



解答疑问:这里的x_token不就是一个普通参数名吗?不是任意都可以吗?还是说如果想获取 请求头中的X-Token信息,必须用x_token才行?

先给结论:不是任意取名,默认规则下必须写 x_token;想自定义变量名,就要手动指定 name

下面分两种场景讲清楚,再对比演示。


一、默认规则(不写 name

FastAPI 会按照固定映射规则绑定请求头和函数参数:

HTTP 头名里的 - → 变量名换成 _,字母大小写不敏感

对应关系:

  • 请求头:X-Token → 把 - 换成 _ → 变量名必须写 x_token
  • 请求头:User-Agent → 变量名必须写 user_agent

错误示例(随便起名,拿不到值)

python 复制代码
@app.get("/")
# 变量名写成 token,和映射规则不匹配
async def read_header(token: str | None = Header(None)):
    return {"X-Token 值": token}

客户端传 X-Token: 123,这里 token 拿到的永远是 None匹配失败

正确示例(遵循自动映射)

python 复制代码
@app.get("/")
async def read_header(x_token: str | None = Header(None)):
    return {"X-Token 值": x_token}

客户端传 X-Token: 123 → 正常取值。


二、自由自定义变量名(用 name 参数)

如果你不想用 x_token,想自己定义变量名(比如 tokenauth_token),就在 Header() 里加 name="原始请求头名",显式绑定。

示例1:变量名改为 token

python 复制代码
@app.get("/")
# name 明确指定要读取的请求头是 X-Token
async def read_header(token: str | None = Header(None, name="X-Token")):
    return {"X-Token 值": token}
  • 变量名:token(自定义)
  • 实际读取的请求头:X-Token
  • 可以正常拿到数据。

示例2:变量名改为 auth_token

python 复制代码
@app.get("/")
async def read_header(auth_token: str | None = Header(None, name="X-Token")):
    return {"X-Token 值": auth_token}

完全没问题。


三、补充:大小写无关

HTTP 请求头本身不区分大小写 ,下面这些头,用 x_token 都能正常接收:

复制代码
X-Token
x-token
X-TOKEN
x-Token

FastAPI 内部会统一做小写匹配。


四、总结对照表

写法 变量名能否自定义 要求
xxx = Header(None) (无 name) ❌ 不能自定义 变量名必须按规则:头名-连字符变量名_下划线
xxx = Header(None, name="X-Token")(带 name) ✅ 完全自由 name 填真实请求头名,变量名随便起

五、使用建议

  1. 简单场景、头名不长:直接用自动映射(x_token),代码简洁;
  2. 变量名想语义化/简化、头名很长:用 name 手动绑定,灵活度更高。
相关推荐
嘶哈哈哈1 小时前
嘉立创 EDA 入门实操笔记:从原理图到 PCB 布线、差分对、覆铜与 DRC 检查
开发语言·笔记·php
一口吃俩胖子1 小时前
【脉宽调制DCDC功率变换学习笔记024】频域性能
笔记·学习
吃着火锅x唱着歌1 小时前
深度探索C++对象模型 学习笔记 第五章 构造、解构、拷贝语意学(2)
c++·笔记·学习
中小企业实战军师刘孙亮2 小时前
快消纺织五金怎么融合?三大业态协同发展战略思路-佛山鼎策创局破局增长咨询
学习·面试·创业创新·制造·学习方法
Upsy-Daisy2 小时前
Hermes Agent 学习笔记 04:工具调用系统,让 Agent 从“会说”变成“会做”
java·笔记·学习
楼田莉子2 小时前
C++20新特性:协程
开发语言·c++·后端·学习·c++20
weixin_428005302 小时前
C#调用 AI学习从0开始-第2阶段(Function Calling+工具调用智能体)-第9天实战-实现计算器工具
开发语言·学习·c#·functioncalling·ai实现计算器工具
Deepoch2 小时前
Deepoc VLA开发板:除草机器人的持续学习与协同作业系统
人工智能·学习·机器人·开发板·具身模型·deepoc
John_ToDebug2 小时前
在 Windows 上搭建 Chromium 148 内核编译环境:一份实战笔记
chrome·经验分享·笔记