1903
10 分钟
Python 安全使用 MinIO 对象存储

MinIO 提供兼容 Amazon S3 API 的对象存储能力。Python 应用通常负责上传文件、读取对象、生成临时下载地址和维护对象元数据,而不是把 Bucket 直接改成匿名公开。

本文以“TLS、私有 Bucket、最小权限凭据”为默认姿态。示例不会使用默认账号,也不会把 Access Key 和 Secret Key 写入源码。

安装 SDK#

Terminal window
pip install -U minio

部署环境提供以下变量:

Terminal window
export MINIO_ENDPOINT="storage.example.com:9000"
export MINIO_ACCESS_KEY="<managed-access-key>"
export MINIO_SECRET_KEY="<managed-secret-key>"
export MINIO_SECURE="true"

环境变量只用于演示配置入口。生产环境应由密钥管理服务注入,并确保进程日志、错误报告和诊断页面不会输出完整凭据。

初始化客户端#

import os
from minio import Minio
def parse_bool(value: str) -> bool:
normalized = value.strip().lower()
if normalized in {"1", "true", "yes", "on"}:
return True
if normalized in {"0", "false", "no", "off"}:
return False
raise ValueError(f"invalid boolean value: {value!r}")
client = Minio(
endpoint=os.environ["MINIO_ENDPOINT"],
access_key=os.environ["MINIO_ACCESS_KEY"],
secret_key=os.environ["MINIO_SECRET_KEY"],
secure=parse_bool(os.getenv("MINIO_SECURE", "true")),
)

endpoint 只包含主机和端口,不应带 http://https://。协议由 secure 决定。

生产环境不要为了“先跑起来”而关闭证书校验。内部 CA 应通过系统信任链或受控 HTTP Client 配置提供,而不是长期使用明文 HTTP。

一个职责清晰的轻量封装#

下面的封装只处理对象存储 API,不负责数据库事务、业务权限或文件内容安全检查。

from collections.abc import Iterator
from datetime import timedelta
from pathlib import Path
from minio import Minio
from minio.datatypes import Object
from minio.error import S3Error
class ObjectStorage:
def __init__(self, client: Minio) -> None:
self.client = client
def ensure_bucket(self, bucket_name: str) -> None:
if not self.client.bucket_exists(bucket_name):
self.client.make_bucket(bucket_name)
def upload_file(
self,
bucket_name: str,
object_name: str,
file_path: Path,
*,
content_type: str,
) -> None:
self.client.fput_object(
bucket_name=bucket_name,
object_name=object_name,
file_path=str(file_path),
content_type=content_type,
)
def download_file(
self,
bucket_name: str,
object_name: str,
destination: Path,
) -> None:
self.client.fget_object(
bucket_name=bucket_name,
object_name=object_name,
file_path=str(destination),
)
def read_bytes(
self,
bucket_name: str,
object_name: str,
) -> bytes:
response = self.client.get_object(bucket_name, object_name)
try:
return response.read()
finally:
response.close()
response.release_conn()
def list_objects(
self,
bucket_name: str,
*,
prefix: str = "",
) -> Iterator[Object]:
return self.client.list_objects(
bucket_name,
prefix=prefix,
recursive=True,
)
def stat_object(self, bucket_name: str, object_name: str):
return self.client.stat_object(bucket_name, object_name)
def delete_object(self, bucket_name: str, object_name: str) -> None:
self.client.remove_object(bucket_name, object_name)
def presigned_download(
self,
bucket_name: str,
object_name: str,
*,
valid_for: timedelta = timedelta(minutes=15),
) -> str:
return self.client.presigned_get_object(
bucket_name,
object_name,
expires=valid_for,
)

使用方式:

storage = ObjectStorage(client)
storage.ensure_bucket("private-documents")
storage.upload_file(
"private-documents",
"reports/example.txt",
Path("example.txt"),
content_type="text/plain; charset=utf-8",
)
url = storage.presigned_download(
"private-documents",
"reports/example.txt",
)
print(url)

示例 Bucket 和对象名都是通用名称。真实系统中应使用不可猜测的业务对象 ID,并在数据库中保存对象与租户、权限和状态的关系。

为什么必须释放流式响应#

get_object() 返回的是流式 HTTP Response。异常通常可能在真正读取数据时才出现。

正确结构是:

response = client.get_object(bucket_name, object_name)
try:
for chunk in response.stream(32 * 1024):
consume(chunk)
finally:
response.close()
response.release_conn()

如果不调用 response.close()response.release_conn(),底层连接可能不能及时回到连接池,持续流量下会逐渐耗尽可用连接。

不需要自己处理流时,优先使用 fget_object()download_file() 等更高层方法。

Bucket 创建不是请求级操作#

ensure_bucket() 适合部署初始化或明确的管理流程,不应在每次普通上传请求中都无条件创建 Bucket。

需要考虑:

  • 创建 Bucket 的凭据权限通常高于上传对象;
  • 多个实例同时创建会产生竞争;
  • Region、对象锁定和版本控制需要在创建时确定;
  • 业务请求不应该因为管理 API 短暂失败而创建未知状态。

更安全的做法是由基础设施或部署任务预先创建 Bucket,应用运行凭据只具备必要的对象读写权限。

私有 Bucket、公开策略和预签名 URL#

私有 Bucket#

默认选择。只有持有有效凭据且通过策略授权的服务才能访问对象。

适合:

  • 用户上传文件;
  • 报告和导出结果;
  • 模型、数据集和中间产物;
  • 内部备份;
  • 受权限控制的图片与附件。

公开 Bucket Policy#

公开策略会允许匿名访问某些对象。它适合真正公开、可永久缓存的静态资源,但发布前要确认对象中不包含:

  • 用户标识;
  • 私有文档;
  • EXIF 或其他隐藏元数据;
  • 内部路径和文件名;
  • 后续不应公开的新对象。

不要为了让前端“能打开链接”就把整个 Bucket 设为公开。

预签名 URL#

presigned_get_object() 为私有对象生成带过期时间的临时 URL。它适合浏览器或移动端直接下载对象,服务端无需代理完整文件流。

预签名 URL 本质上是临时凭证:

  • 使用 HTTPS 传输;
  • 有效期尽量短;
  • 不写入公开日志和分析系统;
  • 生成前重新校验用户权限;
  • 对高敏感文件考虑一次性令牌或服务端代理;
  • 不要认为删除页面上的 URL 就能撤销已经签发的链接。

上传安全#

对象存储只负责保存字节,不会自动判断文件是否安全。

上传接口还应处理:

  • 文件大小上限;
  • Content-Type 白名单与实际内容检测;
  • 文件名规范化;
  • 对象路径不能由用户直接拼接;
  • 恶意文件扫描;
  • 压缩炸弹;
  • 图片和文档的元数据清理;
  • 服务端加密要求;
  • 上传完成后的数据库状态更新。

不要只相信客户端提供的 Content-Type 和扩展名。

大文件与内存#

read_bytes() 会把整个对象读入内存,只适合明确限制大小的小对象。

大文件应使用:

  • fget_object() 直接写文件;
  • 分块流式处理;
  • 浏览器直传的预签名 PUT;
  • SDK 的分片上传能力;
  • 明确的最大对象大小和超时。

Web API 代理大文件时,要避免同时在内存中保留上传请求体和对象存储响应。

对象名与覆盖#

S3 兼容对象存储使用 Bucket + Object Name 定位对象。相同名称再次上传通常会覆盖当前对象或产生新版本,具体取决于版本控制配置。

推荐对象名:

<tenant-id>/<resource-type>/<stable-object-id>/<revision>

不要直接使用未经处理的用户文件名作为完整对象路径。文件名可以作为受控元数据保存,真实对象名使用服务端生成的 ID。

错误处理#

from minio.error import S3Error
def safe_stat(
storage: ObjectStorage,
bucket_name: str,
object_name: str,
):
try:
return storage.stat_object(bucket_name, object_name)
except S3Error as error:
if error.code == "NoSuchKey":
return None
raise

不要对所有 S3Error 返回“文件不存在”。权限不足、证书错误、Bucket 不存在和服务不可用需要不同的告警与响应。

重试上传前要确认操作是否可能已经成功。使用稳定对象名覆盖写入可能是幂等的,但如果每次重试都生成新对象名,就会制造孤儿对象。

生产环境检查清单#

1. Endpoint 和凭据是否由密钥管理服务注入
2. 是否默认启用 TLS 并验证证书
3. 应用账号是否只拥有必要 Bucket 和对象权限
4. Bucket 是否由部署流程预创建
5. 是否默认保持 Bucket 私有
6. 生成预签名 URL 前是否重新校验权限
7. 预签名 URL 是否使用较短有效期且避免进入日志
8. get_object 的响应是否总在 finally 中 close 和 release_conn
9. 上传是否限制大小并校验实际文件类型
10. 对象名是否由服务端生成并隔离租户
11. 大文件是否避免一次性读入内存
12. 是否监控错误率、延迟、连接池、容量和孤儿对象

官方参考#

本文根据早期个人笔记重新整理,并结合当前官方文档进行了校对。

Python 安全使用 MinIO 对象存储
https://zh19990906.github.io/fuwari/posts/python-minio-object-storage/
作者
Henson
发布于
2026-07-30
许可协议
CC BY-NC-SA 4.0