跳到主要内容
版本:3.0.0

S3 文件存储(Aegis.FileManager.S3)

Aegis.FileManager.S3Aegis.FileManager 的 S3 协议兼容存储实现,支持 AWS S3、MinIO、Ceph 等所有兼容 S3 协议的对象存储服务。

组件概览

字段说明
组件名称S3 文件存储
真实类库Aegis.FileManager.S3
组件定位基于 S3 协议的对象存储文件管理实现
引入方式安装 NuGet,在 Startup 中手动调用 AddS3SourceAddDefaultS3FileManager
核心能力IFileManager 实现、IFileStorage 容器管理、Presigned URL、跨 Bucket 复制
主要配置S3StorageFileValidation(可选)
典型配套文件管理基础层(Aegis.FileManager)

什么时候要用它

适合场景:

  • 你要把文件存到 AWS S3 或兼容 S3 协议的对象存储(MinIO、Ceph、DigitalOcean Spaces 等)
  • 你需要 Presigned URL 让客户端直接下载,不经过业务服务器中转
  • 你需要跨 Bucket / 跨存储组复制文件
  • 你的部署环境是多节点、需要共享文件存储,且不想依赖 NAS 挂载

不适合场景:

  • 单机部署、只需要本地文件读写(用 NAS 文件存储 更简单)

最小可运行路径

第一步:安装组件

dotnet add package Aegis.FileManager.S3

第二步:准备配置

appsettings.json 中添加 S3Storage 配置节:

{
"S3Storage": {
"Endpoint": "http://localhost:9000",
"Region": "us-east-1",
"AccessKey": "minioadmin",
"SecretKey": "minioadmin",
"DefaultBucket": "aegis-default",
"PresignedUrlExpiryMinutes": 15,
"ForcePathStyle": true,
"AutoCreateBucket": true
}
}

第三步:在 Startup 中注册

using Aegis.FileManager.S3;
using Aegis.FileManager.S3.Utils;

// 方式一:注册默认 S3 文件管理器(推荐入门用法)
services.AddDefaultS3FileManager(ConfigManager.Get<S3Options>("S3Storage"));

// 方式二:注册自定义 S3 源(支持扩展)
// services.AddS3Source<S3FileManager>(ConfigManager.Get<S3Options>("S3Storage"));
NAS 和 S3 可以同时注册

如果你同时需要 NAS 和 S3,可以分别注册不同命名的源。但注意 IFileManager 只能有一个默认注入绑定,多源场景建议直接注入具体的实现类(如 S3FileManager)。

第四步:在业务里使用

public class AttachmentService
{
private readonly S3FileManager _s3FileManager;

public AttachmentService(S3FileManager s3FileManager)
{
_s3FileManager = s3FileManager;
}

public async Task<bool> Upload(string fileName, Stream stream)
{
return await _s3FileManager.UploadFileAsync(fileName, stream, FileType.File, "attachments");
}

public async Task<string> GetDownloadUrl(string fileName)
{
return await _s3FileManager.GetFileUriAsync(fileName, "attachments");
}
}

配置项说明(S3Options)

配置项类型默认值说明
EndpointstringS3 服务端点 URL。AWS S3 可留空;自部署服务(MinIO 等)必填
Regionstring"us-east-1"AWS Region 标识。自部署服务通常填 "us-east-1" 即可
AccessKeystringAccess Key ID
SecretKeystringSecret Access Key
DefaultBucketstring默认 Bucket 名称。当 groupName 参数为空时使用此值
PresignedUrlExpiryMinutesint15Presigned URL 过期时间(分钟)
ForcePathStyleboolfalse是否使用 Path Style 寻址。MinIO 必须设为 true;AWS S3 通常设为 false
AutoCreateBucketboolfalse上传文件时,如果目标 Bucket 不存在是否自动创建

AWS S3 配置示例

{
"S3Storage": {
"Region": "ap-southeast-1",
"AccessKey": "AKIAIOSFODNN7EXAMPLE",
"SecretKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
"DefaultBucket": "my-app-files",
"PresignedUrlExpiryMinutes": 30,
"ForcePathStyle": false,
"AutoCreateBucket": false
}
}

AWS S3 不需要填写 Endpoint,SDK 会根据 Region 自动推导。

MinIO 配置示例

{
"S3Storage": {
"Endpoint": "http://192.168.1.100:9000",
"Region": "us-east-1",
"AccessKey": "minioadmin",
"SecretKey": "minioadmin",
"DefaultBucket": "aegis-default",
"PresignedUrlExpiryMinutes": 15,
"ForcePathStyle": true,
"AutoCreateBucket": true
}
}

MinIO 必须设置 ForcePathStyle: true,因为 MinIO 不支持 Virtual-Hosted Style 寻址。

Ceph RADOS 配置示例

{
"S3Storage": {
"Endpoint": "http://ceph-gateway.example.com:7480",
"Region": "default",
"AccessKey": "your-access-key",
"SecretKey": "your-secret-key",
"DefaultBucket": "my-ceph-bucket",
"ForcePathStyle": true,
"AutoCreateBucket": false
}
}

groupName 与 Bucket 的映射规则

S3 实现中,groupName 参数直接映射为 S3 Bucket 名称:

groupName 参数实际 Bucket说明
"" (空字符串)DefaultBucket 配置值使用默认 Bucket
"attachments"attachments直接使用 groupName 作为 Bucket 名
"my-bucket"my-bucket同上
Bucket 命名约束

S3 Bucket 命名有严格约束(3-63 字符、只允许小写字母/数字/连字符、不能以连字符开头结尾等)。传入 groupName 时请确保符合 S3 Bucket 命名规则

与 NAS 后端的区别:NAS 后端中 groupNameRootPath 下的子目录名;S3 后端中 groupName 直接就是 Bucket 名。

IFileManager 接口方法与 S3 API 对应关系

文件操作

IFileManager 方法S3 API说明
DoesFileExistAsyncHeadObject通过获取元数据判断文件是否存在
UploadFileAsync (byte[])PutObject字节数组包装为 MemoryStream 后上传
UploadFileAsync (Stream)PutObject直接使用流上传,自动推断 ContentType
UploadFileAsync (filePath)PutObject读取本地文件后上传
GetFileStreamAsyncGetObject返回 ResponseStream(不可 Seek)
GetFileBytesAsyncGetObject读取完整 ResponseStream 后返回字节数组
GetFileAsyncGetObject下载到本地指定路径
GetFileInfoAsyncHeadObject获取 ContentLength、ETag、LastModified、ContentType、Metadata
GetFileUriAsyncGetPreSignedURL生成 Presigned URL
DeleteFileAsyncDeleteObject删除单个对象
ListFilesAsyncListObjectsV2支持前缀过滤和分页
CopyFileAsyncCopyObject支持跨 Bucket 复制

流的注意事项

GetFileStreamAsync 返回的 ResponseStream不可 Seek 的流。如果你需要对流进行 Seek 操作(例如传给验证管道),FileValidationManager 会自动将其缓冲为 MemoryStream

IFileStorage 容器管理

S3 模块同时实现了 IFileStorage 接口,提供 Bucket 级别的管理能力:

IFileStorage 方法S3 API说明
GroupExistsAsyncGetBucketLocation检查 Bucket 是否存在
CreateGroupAsyncPutBucket创建 Bucket(幂等,已存在时返回 true)
DeleteGroupAsyncDeleteBucket删除 Bucket(必须为空)
ListGroupsAsyncListBuckets列出所有 Bucket
GetGroupInfoAsyncGetBucketLocation获取 Bucket 详情(名称、Region)

使用示例:

public class StorageManagementService
{
private readonly IFileStorage _fileStorage;

public StorageManagementService(IFileStorage fileStorage)
{
_fileStorage = fileStorage;
}

// 确保 Bucket 存在
public async Task EnsureBucketExists(string bucketName)
{
if (!await _fileStorage.GroupExistsAsync(bucketName))
{
await _fileStorage.CreateGroupAsync(bucketName);
}
}

// 列出所有 Bucket
public async Task<List<StorageGroupInfo>> ListAllBuckets()
{
return await _fileStorage.ListGroupsAsync();
}
}

Presigned URL 机制

GetFileUriAsync 返回的不是文件路径,而是一个带签名的临时访问 URL(Presigned URL)。

工作原理

  1. 调用方请求 GetFileUriAsync("report.pdf", "documents")
  2. S3 模块使用 GetPreSignedURL 生成一个包含签名参数的 URL
  3. 客户端可以直接通过该 URL 下载文件,无需经过业务服务器
  4. URL 在 PresignedUrlExpiryMinutes 分钟后失效

典型用途

  • 前端直接下载大文件,减少业务服务器带宽压力
  • 生成临时分享链接
  • 移动端 App 直接从 S3 下载资源

安全注意

  • Presigned URL 本身包含签名凭证,过期前任何人持有该 URL 都能访问文件
  • 敏感文件建议缩短过期时间(如 5 分钟),或改用服务端代理下载
  • 不要将 Presigned URL 持久化存储(如写入数据库),它会过期失效

AutoCreateBucket 行为说明

AutoCreateBucket 设为 true 时,每次上传文件前会检查目标 Bucket 是否存在,不存在则自动创建。这省去了手动创建 Bucket 的步骤,适合以下场景:

  • 开发/测试环境,快速启动
  • 多租户应用,按需创建租户 Bucket

生产环境建议设为 false,由运维团队预先创建和管理 Bucket,避免应用账号拥有 CreateBucket 权限。

自定义扩展

继承 S3FileBase

如果默认的 S3FileManager 不满足需求(例如需要在上传时自动添加元数据、覆盖 ContentType 等),可以继承 S3FileBase

public class CustomS3FileManager : S3FileBase
{
public CustomS3FileManager(IAmazonS3 client, S3Options options)
: base(client, options) { }

public override async Task<bool> UploadFileAsync(
string fileName, Stream fileStream, FileType type,
string groupName = "", CancellationToken cancellationToken = default)
{
// 添加自定义逻辑,如自动打标签、记录审计日志等
return await base.UploadFileAsync(fileName, fileStream, type, groupName, cancellationToken);
}
}

// 注册自定义实现
services.AddS3Source<CustomS3FileManager>(s3Options);

DI 注册方式对比

注册方法注入方式适用场景
AddDefaultS3FileManagerIFileManager + S3FileManager 均可注入只有一个文件存储后端
AddS3Source<T>只能通过具体类型注入(如 S3FileManager多文件源、需要区分 NAS 和 S3
多源共存

当项目同时使用 NAS 和 S3 时,推荐用 AddS3Source<S3FileManager>(options) 注册 S3,用 AddNasSource<NasFileManager>(options) 注册 NAS,然后在业务代码中按需注入具体的实现类。

与 NAS 后端的差异对比

特性NAS 后端S3 后端
存储类型本地文件系统 / NAS 挂载对象存储(S3 协议)
groupName 含义RootPath 下的子目录名Bucket 名称
GetFileUriAsync 返回值本地文件绝对路径Presigned URL(带过期时间)
FileObject.ContentMd5计算的 MD5 哈希S3 ETag(分片上传时可能不是 MD5)
GetFileStreamAsync可 Seek 的 FileStream不可 Seek 的 ResponseStream
文件列举基于目录遍历基于 ListObjectsV2,支持分页和前缀过滤
容器管理(IFileStorage目录的创建/删除Bucket 的创建/删除
大文件支持受磁盘空间限制理论上无限制(S3 单对象最大 5TB)
多节点共享需要共享挂载天然支持
额外依赖AWSSDK.S3
文件校验内置(通过 FileValidationManager内置(自动处理非 Seekable 流)
Bucket 自动创建不适用AutoCreateBucket 控制
跨组复制手动实现CopyFileAsync 原生支持跨 Bucket

安全注意事项

凭证管理

  • 不要AccessKeySecretKey 硬编码在代码中或提交到版本控制
  • 推荐使用环境变量或密钥管理服务注入凭证:
    var options = new S3Options
    {
    Endpoint = Configuration["S3Storage:Endpoint"],
    AccessKey = Environment.GetEnvironmentVariable("S3_ACCESS_KEY"),
    SecretKey = Environment.GetEnvironmentVariable("S3_SECRET_KEY"),
    // ...
    };
  • AWS 环境下推荐使用 IAM Role,避免静态凭证

IAM 策略建议

为应用创建专用的 IAM 用户/角色,遵循最小权限原则:

最小文件操作权限

{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:GetObject",
"s3:DeleteObject",
"s3:HeadObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::my-app-*",
"arn:aws:s3:::my-app-*/*"
]
}
]
}

如果启用 AutoCreateBucket,还需要添加:

{
"Effect": "Allow",
"Action": [
"s3:CreateBucket",
"s3:GetBucketLocation"
],
"Resource": "*"
}

网络安全

  • 自部署的 MinIO / Ceph 建议启用 TLS(HTTPS)
  • 生产环境不要将 S3 端点暴露到公网,使用 VPC / 内网访问
  • Presigned URL 生成后无法撤销,请合理设置过期时间

接入后怎么确认生效

通常用下面几项验收:

  • S3FileManager 可以正常注入
  • 调用 UploadFileAsync 后,在 S3 控制台或 MinIO 管理界面能看到对应文件
  • GetFileStreamAsync / GetFileBytesAsync 能正常读取文件内容
  • GetFileUriAsync 返回的 Presigned URL 可以在浏览器中直接下载
  • DeleteFileAsync 后文件确实被删除
  • ListFilesAsync 返回的文件列表与实际一致

常见问题

连接 MinIO 时报 InvalidBucketNameNoSuchBucket

优先检查:

  • ForcePathStyle 是否设为 true(MinIO 必须开启)
  • Endpoint 是否包含协议前缀(应为 http://host:port,不能省略 http://
  • DefaultBucket 是否已创建,或 AutoCreateBucket 是否为 true

注入 IFileManager 时提示找不到注册

检查 Startup 中是否调用了 AddDefaultS3FileManager(而非 AddS3Source)。AddS3Source 只注册具体类型,不绑定 IFileManager

GetFileStreamAsync 返回的流不能 Seek

这是预期行为。S3 的 ResponseStream 不可 Seek。如果需要 Seek,可以自己缓冲:

var stream = await _s3FileManager.GetFileStreamAsync(fileName);
var ms = new MemoryStream();
await stream.CopyToAsync(ms);
ms.Position = 0;
// 使用 ms...

Presigned URL 访问返回 AccessDenied

  • 检查 AccessKey / SecretKey 是否正确
  • 检查 Bucket 的访问策略是否允许该操作
  • MinIO 环境下检查 Bucket Policy 是否已设置

与 NAS 同时注册时 IFileManager 注入冲突

IFileManager 只能绑定一个默认实现。多源场景下,请直接注入具体的实现类:

// 不要注入 IFileManager,改为注入具体类型
public class MyService
{
private readonly S3FileManager _s3;
private readonly NasFileManager _nas;

public MyService(S3FileManager s3, NasFileManager nas)
{
_s3 = s3;
_nas = nas;
}
}