文件上传
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 个要点逐一讲解。
一、校验文件类型 & 文件大小
风险
- 文件类型不校验
用户可上传.exe、.php、.sh等可执行脚本,一旦被执行,直接造成服务器入侵、木马植入。 - 文件大小不限制
恶意用户上传超大文件,占满服务器磁盘、耗尽带宽,引发磁盘溢出、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)
-
上传目录单独隔离
不要把上传目录放在网站根目录、程序运行目录,单独划分。
-
设置目录权限
bash
# 目录仅允许 读写,禁止执行权限(关键!禁止执行脚本)
chmod 755 uploads/
# 所属用户设为运行程序的账号,不要用 root
chown -R appuser:appgroup uploads/
755:所有者可读写执行,其他人仅可读,无写入、无执行- 核心原则:上传目录永远禁止脚本执行权限
- Nginx/Apache 额外防护
配置上传目录禁止解析 PHP / Python / Shell 等脚本,即使上传了脚本,也无法运行。
四、大文件:流式处理,不要一次性读取
原代码问题分析
python
shutil.copyfileobj(file.file, buffer)
UploadFile 内部是文件流 ,shutil.copyfileobj 本身是流式分块读写,不会一次性把整个文件载入内存。
补充:
- 错误写法(大坑):
content = await file.read()一次性读全部内容到内存- 大文件(几百MB/GB)会直接撑爆内存、服务卡死。
流式处理原理
UploadFile.file 是类文件对象,分块读取、分块写入:
- 从客户端接收一小块数据
- 直接写入磁盘
- 内存只保留一小块缓冲区,不加载完整文件
大文件优化写法(断点续传/分块上传 拓展)
超大文件(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 条注意事项极简总结(背诵版)
- 校验类型&大小:拦截恶意脚本、超大文件,防入侵和磁盘攻击。
- 安全文件名 :不用前端原始名称,用
basename/UUID,防路径遍历攻击。 - 目录权限 :服务器给上传目录设最小权限,禁止执行脚本。
- 大文件流式读写 :用文件流分块写入,禁止一次性 read() 全量加载,防内存溢出。
请求头自动转换
一、先讲核心矛盾
- HTTP 规范 :请求头字段习惯用 连字符
-,例:X-Token、User-Agent、Authorization - Python 语法 :变量名不允许 出现
-,x-token会被解析成「变量 x 减变量 token」,直接语法报错。
所以 FastAPI 做了自动映射转换,解决两边命名规则冲突。
二、FastAPI 转换规则(固定规则,直接记)
规则总览
HTTP 请求头:短横线分隔(kebab-case)
→ FastAPI 代码里:下划线分隔(snake_case)
同时统一转为小写匹配,大小写不敏感。
具体映射逻辑
- HTTP 头里的
-(连字符) → 代码变量里换成_(下划线) - 头部名称大小写无关,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可以自定义,不受转换规则限制
五、关键补充细节
-
为什么能自动转?
FastAPI + Starlette 底层做了封装:拿到原始请求头后,自动把所有
-替换成_,再绑定到函数变量。 -
大小写完全不敏感
以下请求头都会被
x_token接收:X-Token
x-token
X-TOKEN -
误区:能不能直接用带
-的变量名?
❌ 绝对不行
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,想自己定义变量名(比如 token、auth_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 填真实请求头名,变量名随便起 |
五、使用建议
- 简单场景、头名不长:直接用自动映射(
x_token),代码简洁; - 变量名想语义化/简化、头名很长:用
name手动绑定,灵活度更高。