news 2026/6/10 19:49:14

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【Fastapi学习笔记(6)】—— Fastapi文件上传、请求头自动转换

文件上传

# 原生写法(仅演示功能,生产有安全&性能隐患)@app.post("/uploadfile/")asyncdefcreate_upload_file(file:UploadFile):withopen(f"uploads/{file.filename}","wb")asbuffer: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)。

importosimportshutilfromfastapiimportFastAPI,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"}# 单文件最大 10MBMAX_FILE_SIZE=10*1024*1024os.makedirs(UPLOAD_DIR,exist_ok=True)@app.post("/uploadfile/")asyncdefcreate_upload_file(file:UploadFile):# -------- 1. 校验文件大小 --------# UploadFile 可通过 file.size 获取大小(FastAPI 新版支持)iffile.size>MAX_FILE_SIZE:raiseHTTPException(status_code=400,detail="文件过大,最大支持 10MB")# -------- 2. 校验文件类型 --------# 校验 MIME 类型iffile.content_typenotinALLOWED_CONTENT_TYPE:raiseHTTPException(status_code=400,detail="不支持的文件类型")# 校验文件后缀file_suffix=os.path.splitext(file.filename)[1].lower()iffile_suffixnotinALLOWED_SUFFIX:raiseHTTPException(status_code=400,detail="文件后缀不合法")# 后续保存逻辑...save_path=os.path.join(UPLOAD_DIR,file.filename)withopen(save_path,"wb")asbuffer:shutil.copyfileobj(file.file,buffer)return{"filename":file.filename,"message":"文件上传成功"}

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

什么是路径遍历攻击?

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

举个恶意示例:
客户端上传文件,文件名填写:

../../etc/passwd

你的原代码拼接路径:

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

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

问题根源

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

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

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

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

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

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

  • 彻底防路径遍历
  • 避免同名文件覆盖
importuuid# 拆分原文件名与后缀suffix=os.path.splitext(file.filename)[1].lower()# 生成唯一文件名safe_filename=f"{uuid.uuid4()}{suffix}"save_path=os.path.join(UPLOAD_DIR,safe_filename)

改造后安全代码片段

# 安全处理文件名suffix=os.path.splitext(file.filename)[1].lower()safe_filename=f"{uuid.uuid4()}{suffix}"save_path=os.path.join(UPLOAD_DIR,safe_filename)withopen(save_path,"wb")asbuffer:shutil.copyfileobj(file.file,buffer)

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

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

风险

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

目录权限配置(Linux)

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

  2. 设置目录权限

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

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

原代码问题分析

shutil.copyfileobj(file.file,buffer)

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

补充:

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

流式处理原理

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

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

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

超大文件(GB 级),单纯流式不够,业界标准:前端分块上传 + 后端合并
FastAPI 原生UploadFile配合流式读写就是标准方案,示例:

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

避坑提醒

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

# 大文件直接 OOM,生产禁止!content=awaitfile.read()withopen(save_path,"wb")asf:f.write(content)

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

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

importosimportuuidimportshutilfromfastapiimportFastAPI,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/")asyncdefcreate_upload_file(file:UploadFile):# 1. 校验文件大小iffile.size>MAX_FILE_SIZE:raiseHTTPException(status_code=400,detail="文件超过最大限制(10MB)")# 2. 校验文件类型iffile.content_typenotinALLOWED_CONTENT_TYPE:raiseHTTPException(status_code=400,detail="非法文件类型")file_suffix=os.path.splitext(file.filename)[1].lower()iffile_suffixnotinALLOWED_SUFFIX:raiseHTTPException(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:withopen(save_path,"wb")asbuffer:shutil.copyfileobj(file.file,buffer)exceptExceptionase:raiseHTTPException(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. 前端/客户端请求头
X-Token: abc123456
2. FastAPI 代码写法

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

fromfastapiimportFastAPI,Header app=FastAPI()@app.get("/")asyncdefread_header(x_token:str|None=Header(None)):return{"X-Token 值":x_token}
效果

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


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

HTTP 头:User-Agent
代码变量:user_agent

@app.get("/ua")asyncdefget_ua(user_agent:str|None=Header(None)):return{"User-Agent":user_agent}

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

HTTP 头:X-App-Version
代码变量:x_app_version

@app.get("/version")asyncdefget_version(x_app_version:str|None=Header(None)):return{"版本":x_app_version}

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

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

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

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

asyncdefdemo(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
  1. 误区:能不能直接用带-的变量名?
    ❌ 绝对不行
# 语法错误!Python 不允许变量名含 -asyncdefdemo(x-token:str=Header(None)):pass

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


六、完整可运行示例汇总

fromfastapiimportFastAPI,Header app=FastAPI()# 1. 自动转换:X-Token → x_token@app.get("/token")asyncdefget_token(x_token:str|None=Header(None)):return{"x_token":x_token}# 2. 自动转换:User-Agent → user_agent@app.get("/agent")asyncdefget_agent(user_agent:str|None=Header(None)):return{"user_agent":user_agent}# 3. 手动指定 header 名称(不依赖自动转换)@app.get("/custom")asyncdefcustom_header(app_key:str|None=Header(None,name="X-App-Key")):return{"app_key":app_key}
测试请求
curlhttp://127.0.0.1:8000/token-H"X-Token: 123abc"curlhttp://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

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

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

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

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

@app.get("/")asyncdefread_header(x_token:str|None=Header(None)):return{"X-Token 值":x_token}

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


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

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

示例1:变量名改为 token

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

示例2:变量名改为 auth_token

@app.get("/")asyncdefread_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手动绑定,灵活度更高。
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/10 19:47:29

第一讲:C语言的常见概念

一、初识C语言 1.C语言是什么 C语言是一种计算机语言,有它自己的语法规则。 2.C语言的历史 1969年,贝尔实验室的肯汤普森与丹尼斯里奇开发了Unix操作系统。Unix是用汇编写的,为了可移植性,汤普森在BCPL语言的基础上发明了B语言…

作者头像 李华
网站建设 2026/6/10 19:33:55

Word公式排版救星:MathType 7.4.8安装避坑与右编号公式实战指南

MathType 7.4.8高效排版指南:从零安装到专业公式右编号实战第一次在Word里插入带编号的数学公式时,相信很多人都有过这样的经历:好不容易写好的公式,编号却总是对不齐;想要修改格式,却发现菜单选项深藏不露…

作者头像 李华
网站建设 2026/6/10 19:31:02

S32K3安全机制实战:手把手教你用EIM模块注入ECC错误(附MCAL配置)

S32K3安全机制实战:EIM模块ECC错误注入与MCAL配置详解引言在汽车电子功能安全开发中,内存错误检测机制的验证是ASIL D认证的关键环节。S32K3系列MCU作为NXP面向汽车安全应用的主力产品,其内置的EIM(Error Injection Module)模块为工程师提供了…

作者头像 李华
网站建设 2026/6/10 19:03:11

SNP亮相2026思爱普中国峰会,助力企业加速数据价值兑现

6月3日思爱普中国峰会在北京顺利召开,吸引来自全国千余位伙伴和客户到场,共同聚焦当下企业最关心的话题:AI如何真正落地并创造业务价值。SAP 首次面向中国市场系统阐述了“自主运营企业”(Autonomous Enterprise)战略愿…

作者头像 李华