文件上传校验必须重写序列化器字段的to_internal_value方法,因其可获取UploadedFile实例并读取原始字节流,实现Magic Bytes类型识别、大小动态控制及身份绑定,避免依赖不可信的Content-Type或滞后校验导致的安全与性能问题。

文件上传校验不能只靠 FileField 默认行为,否则会漏掉业务侧关键约束(比如文件类型白名单、内容真实性、大小动态限制),甚至引发安全风险。
为什么 to_internal_value 是校验入口而不是字段定义
Django 的 FileField 和 DRF 的 FileField 序列化器字段默认只做基础检查:非空、大小不超过 MAX_UPLOAD_SIZE、文件名长度合法。但它们不验证文件内容——比如用户把恶意脚本改成 .jpg 后缀上传,或用 Postman 伪造 Content-Type 绕过前端限制。
真正可控的校验点在序列化器字段的 to_internal_value 方法里,因为此时已拿到 UploadedFile 实例,能读取原始字节流。
- 必须重写该方法,否则所有深度校验逻辑都会被跳过
- 不要在
validate_<field_name>中做文件头检测——那时文件可能已被临时写入磁盘或内存,且file.read()后指针偏移,后续保存会失败 - 如果用了
Base64FileField(来自drf-extra-fields),它的to_internal_value已解码 Base64,可直接校验二进制内容
校验文件类型:别信 content_type,要看 Magic Bytes
HTTP 请求头里的 Content-Type 完全由客户端控制,request.FILES['file'].content_type 可被任意篡改。真实类型必须通过文件前几个字节(Magic Number)判断。
立即学习“Python免费学习笔记(深入)”;
- 用
python-magic库(非filetype):它依赖系统 libmagic,识别更准,支持复合格式(如 Office 文档、PDF) - 校验时先
file.seek(0),再file.read(2048)取头部,避免读整个大文件 - 白名单应明确到子类型,例如只允许
image/jpeg,而非宽泛的image/* - 对
ImageField也需额外调用PIL.Image.open()尝试解码——很多“图片”只是伪装成 JPEG 的 ZIP
大小与数量限制:用视图层 + 解析器层双控
单靠序列化器字段的 max_length 或模型层的 validators=[FileSizeValidator(...)] 不够:前者只校验字段值长度(对文件无意义),后者在文件已上传完毕后才触发,浪费带宽和 I/O。
- 在
settings.py中配DATA_UPLOAD_MAX_MEMORY_SIZE和FILE_UPLOAD_MAX_MEMORY_SIZE,防内存溢出 - 在视图类中显式指定解析器,并传入
max_files参数(如MultiPartParser(max_files=5)) - 若用
FileUploadParser(纯文件流场景),需在to_internal_value开头就检查file.size,超过阈值立刻raise ValidationError - 注意:Nginx/Apache 反向代理可能有独立的上传限制(如
client_max_body_size),需同步调整
微信小程序等无 Cookie 场景下的鉴权校验链
小程序调用 wx.uploadFile 时不会自动携带 session 或 token,所以文件上传请求天然“匿名”。你不能等到 save() 阶段才查用户,而必须在反序列化阶段就绑定身份。
- 把认证信息放请求头(如
Authorization: Bearer xxx)或 query 参数(?token=xxx),并在序列化器__init__中提取 - 在
to_internal_value开头就调用get_user_from_token(token),失败则直接报错,不继续解析文件 - 校验通过后,把
user存为实例属性(如self.context['user'] = user),供后续create()方法使用 - 切勿在
validate()方法里查数据库——它在所有字段校验完成后才执行,此时文件可能已临时写入磁盘
最易被忽略的是文件指针位置和 Magic Bytes 校验时机:90% 的“类型校验失效”问题,都源于没在 to_internal_value 里 seek(0) 就去读内容,或者把校验逻辑错放到 validate 钩子里。校验不是加个装饰器就能完事,它得嵌进数据流转的关键断点上。


















