文件上传
# 原生写法(仅演示功能,生产有安全&性能隐患)@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 个要点逐一讲解。
一、校验文件类型 & 文件大小
风险
- 文件类型不校验
用户可上传.exe、.php、.sh等可执行脚本,一旦被执行,直接造成服务器入侵、木马植入。 - 文件大小不限制
恶意用户上传超大文件,占满服务器磁盘、耗尽带宽,引发磁盘溢出、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)
上传目录单独隔离
不要把上传目录放在网站根目录、程序运行目录,单独划分。设置目录权限
# 目录仅允许 读写,禁止执行权限(关键!禁止执行脚本)chmod755uploads/# 所属用户设为运行程序的账号,不要用 rootchown-Rappuser:appgroup uploads/755:所有者可读写执行,其他人仅可读,无写入、无执行- 核心原则:上传目录永远禁止脚本执行权限
- Nginx/Apache 额外防护
配置上传目录禁止解析 PHP / Python / Shell 等脚本,即使上传了脚本,也无法运行。
四、大文件:流式处理,不要一次性读取
原代码问题分析
shutil.copyfileobj(file.file,buffer)UploadFile内部是文件流,shutil.copyfileobj本身是流式分块读写,不会一次性把整个文件载入内存。
补充:
- 错误写法(大坑):
content = await file.read()一次性读全部内容到内存- 大文件(几百MB/GB)会直接撑爆内存、服务卡死。
流式处理原理
UploadFile.file是类文件对象,分块读取、分块写入:
- 从客户端接收一小块数据
- 直接写入磁盘
- 内存只保留一小块缓冲区,不加载完整文件
大文件优化写法(断点续传/分块上传 拓展)
超大文件(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 条注意事项极简总结(背诵版)
- 校验类型&大小:拦截恶意脚本、超大文件,防入侵和磁盘攻击。
- 安全文件名:不用前端原始名称,用
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. 前端/客户端请求头
X-Token: abc1234562. 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可以自定义,不受转换规则限制
五、关键补充细节
为什么能自动转?
FastAPI + Starlette 底层做了封装:拿到原始请求头后,自动把所有-替换成_,再绑定到函数变量。大小写完全不敏感
以下请求头都会被x_token接收:
X-Token x-token X-TOKEN- 误区:能不能直接用带
-的变量名?
❌ 绝对不行
# 语法错误!Python 不允许变量名含 -asyncdefdemo(x-token:str=Header(None)):passPython 解释器会把-当成减号运算符,直接报错。
六、完整可运行示例汇总
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,想自己定义变量名(比如token、auth_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-TokenFastAPI 内部会统一做小写匹配。
四、总结对照表
| 写法 | 变量名能否自定义 | 要求 |
|---|---|---|
xxx = Header(None)(无 name) | ❌ 不能自定义 | 变量名必须按规则:头名-连字符→变量名_下划线 |
xxx = Header(None, name="X-Token")(带 name) | ✅ 完全自由 | name填真实请求头名,变量名随便起 |
五、使用建议
- 简单场景、头名不长:直接用自动映射(
x_token),代码简洁; - 变量名想语义化/简化、头名很长:用
name手动绑定,灵活度更高。