Minio存储分桶方案设计文档 (V2)

1. 概述

为解决原有单一公有存储桶配置下,业务敏感数据(如用户个人信息、业务报告、内部文件等)存在的潜在泄露风险,同时兼顾非敏感静态资源(如Logo、图标、公开文档)的便捷访问需求,特设计此Minio存储分桶方案。本方案通过引入私有桶公有桶的分离配置,使业务能根据数据敏感性灵活选择存储策略,并通过严格的向后兼容机制,确保存量业务平滑过渡。

2. 设计目标

  1. 安全隔离:为敏感数据提供私有桶存储,防止未经授权的直接访问。
  2. 灵活访问:为非敏感静态资源保留公有桶,支持前端直接引用,无需后端代理。
  3. 业务无感升级(核心):通过版本开关控制,确保所有现有业务(V1)无需任何修改即可正常运行。
  4. 配置清晰:新增配置项意图明确,与原有配置易于区分。
  5. 使用便捷:为后端业务提供清晰的API接口,用于获取对应类型的桶;为前端上传提供明确的参数标识。

3. 配置说明

3.1 原有配置 (V1兼容模式)

此配置保持完全不变,用于兼容所有现有业务。当 minio.bucket.edition 未配置或不为 v2 时,系统运行于此模式。

# Minio服务地址
minio.endpoint=http://192.168.175.54:9000
# 访问密钥
minio.accessKey=snest
# 私密密钥
minio.secretKey=12345678
# 默认存储桶名称(在此模式下,此桶被视为公有桶)
minio.bucketName=apps

3.2 新增V2版本配置

当开启V2版本后,系统启用公有、私有分桶模式。需在原有配置基础上增加如下两项:

# ========== 原有配置 ==========
minio.endpoint=http://192.168.175.54:9000
minio.accessKey=snest
minio.secretKey=12345678
minio.bucketName=apps
# ========== 新增V2配置 ==========
# 功能版本开关,必须显式设置为 `v2` 才能启用新特性
minio.bucket.edition=v2
# 公有桶名称,用于存放可公开访问的资源(如Logo,图标)
minio.iidpBucket=iidp-bucket
配置解读:
  • minio.bucket.edition=v2:此配置为总开关。仅当应用读取到此配置且值为v2时,才会激活下文所述的所有V2新特性(包括分桶逻辑和新接口)。
  • minio.iidpBucket:此配置定义了一个公有桶的名称。
  • 原有的 minio.bucketName 所指向的桶(apps),在V2模式下,其语义变更为私有桶

4. 接口变更与使用说明

4.1 后端接口

引擎层将提供以下接口供业务代码调用:

接口方法 版本 返回桶类型 说明
getBucket() V1 & V2 V1:公有桶
V2:私有桶
兼容性核心接口。V1模式下,返回原minio.bucketName(公有桶);V2模式下,返回原minio.bucketName,但其性质为私有桶。现有业务代码无需任何修改。
getIIDPBucket() 仅 V2 公有桶 新增接口。仅在配置了minio.bucket.edition=v2时有效,返回由minio.iidpBucket配置指定的公有桶对象。

后端业务代码调整示例:

// 场景1:访问敏感业务数据(如用户上传的身份证照),应使用私有桶
MinioClient privateBucketClient = minioService.getBucket();
privateBucketClient.putObject(...); // 此文件默认无法通过URL直接访问

// 场景2:访问或存储公开资源(如网站logo),应使用公有桶
// 注意:此调用在V1配置下会报错或返回null,需确保在V2模式下使用
if (isV2Mode()) { // 业务可根据需要判断版本
    MinioClient publicBucketClient = minioService.getIIDPBucket();
    publicBucketClient.putObject(...); // 此文件生成URL可直接被前端访问
}

4.2 前端上传联动

前端文件上传组件需根据业务场景,在调用后端通用上传接口时,增加 bucketType 参数。

  • 上传到公有桶bucketType = 'public'
  • 上传到私有桶bucketType = 'private' 或 不传/传其他值(由后端定义默认逻辑,建议默认私有桶以保证安全)。

前端示例(伪代码):

// 上传公开Logo
uploadFile(file, {bucketType: 'public'}).then(url => {
    // 获取的url可直接用于<img>标签src
});

// 上传用户私有文档
uploadFile(file, {bucketType: 'private'}).then(url => {
    // 此url通常无法直接访问,需通过后端有权限校验的接口代理访问或生成临时签名URL
});

5. 使用场景总结

场景 存储桶类型 配置版本 后端使用接口 前端上传参数 特点
历史存量业务 公有桶 V1 (默认) getBucket() 无要求,按原逻辑 完全兼容,零成本迁移。
新业务-敏感数据
(用户数据、交易记录)
私有桶 V2 getBucket() bucketType: ‘private’ 数据安全,外网不可直接访问。
新业务-公开资源
(Logo、帮助文档、宣传图)
公有桶 V2 getIIDPBucket() bucketType: ‘public’ 访问高效,可直接通过URL访问。

6. 实施步骤建议

  1. 评估与规划:梳理现有业务数据,明确哪些应迁至私有桶,哪些可留在公有桶。
  2. 配置升级:在应用配置中,将 minio.bucket.edition 设置为 v2,并设置 minio.iidpBucket 名称。
  3. Minio服务端准备:确保Minio服务中已创建 iidp-bucket 桶,并检查 apps 桶的访问策略是否符合私有要求。
  4. 后端代码更新:对于新业务逻辑或需要存储公开资源的存量业务,将对应代码中的存储客户端获取方式改为 getIIDPBucket()
  5. 前端代码更新:改造文件上传组件,支持传递 bucketType 参数。
  6. 测试与验证:全面测试V2模式下公有、私有桶的上传、访问(直接访问和签名URL访问)功能,并验证V1兼容模式仍正常工作。

文档版本:V1.0

业务和公共分桶方案

现状分析

关于 minio 对象存储现在是统一在 application-dev.properties 中配置:

minio.endpoint=http://192.168.175.54:9000
minio.accessKey=snest
minio.secretKey=12345678
minio.bucketName=apps
minio.publicBucket=public-bucket # 没用
# minio.region = oss-cn-hangzhou
调用 meta_attachment 元模型的 upload 方法上传文件,所有的业务都统一使用同一个桶apps. 所有的业务都共享同一个桶,导致同一个桶中存储的文件,无法区分是哪个业务上传的。

需求分析

目前了解到的需求是: 为了安全考虑不让公网直接访问minio,我们让他们开放,如果他们开放了就会所有人都能直接访问到这个桶,那业务附件也被访问到了,这明显存在安全问题。 所以要把系统可以公开的公共资源放入到公共桶,可以外部访问,私有桶只能程序通过id、secret访问。

MinIO 的权限主要在 存储桶(Bucket)级别对象(Object)级别 进行设置。

  1. 桶策略 (Bucket Policy):这是最主要的方式。你可以为整个桶或桶内特定目录前缀(Prefix)设置详细的访问规则(Allow/Deny),规定谁(Principal)能对哪些资源(Resource)执行什么操作(Action,如 s3:GetObject)。
  2. 访问控制列表 (ACL):一种相对传统的权限控制方式,可以为匿名用户或特定用户设置路径级权限。
  3. 预签名 URL (Presigned URL):这是访问私有资源最安全的方式。后端应用使用自己的 Access Key 和 Secret Key 生成一个有时效性的临时网址,分享给前端。在有效期内,即使是没有凭证的用户也能通过此链接访问或上传特定对象,过期后自动失效。

以下是三种常见的桶权限级别对比:

权限类型 描述 适用场景 风险等级
private 完全私有,需签名 URL 敏感数据、业务附件
download 只读公开,允许列出和下载文件 公开文档、静态资源
public 完全公开,允许读写删 公共上传区(谨慎使用

对应的方案是:创建一个 public (或 download) 桶存放公共资源,一个 private 桶存放业务附件。

目前实施方案

目前实施的方案是为平台新增一个公有桶,平台业务涉及到的对象需存储到公有桶中,其他非业务依然沿用原有桶。 但是需要引擎和平台app做相应的调整。

    1. 新增两个配置项,分别是后端配置和前端配置;
        # 后端配置项
        minio.bucket.edition=v2
        minio.iidpBucket=iidp-bucket
        
        
        # 前端配置项
        bucketType = public/private
    1. 为什么要有两个配置项
    • 2.1. 后端配置功能开关,是为了兼容已有的系统,如果没有配置 minio.bucket.edition=v2 则所有逻辑都保持原来一致,如果有配置则按照新逻辑执行;

    • 2.2. 前端配置是为了给业务一个选项,因为暴露给前端的接口可能由平台调用也可能由业务调用,所以需要传递一个标识来表明是否需要存储到共有桶,默认行为与原有逻辑保持一致。

    1. 调整
    • 3.1. 引擎在 /file/upload 接口需要处理两个配置项,即前端和后端,只有两个配置项都满足的情况下才执行新逻辑,否则依然是原来逻辑。

    • 3.2. 类似地,平台app 也需要处理两个配置项,但是如果只有一个配置项那就只需要处理一个,比如不需要前端页面来上传。

    1. 样板代码
  // 获取 minioTemplate 实例
  MinioTemplate minioTemplate = MinioTemplate.getInstance();
  // 获取业务所需的桶,有两个选择,如果是非平台的业务则用 getBucket,如果是平台业务则调用 getIIDPBucket
  String minioBucketName = minioTemplate.getBucket();
  String minioBucketName = minioTemplate.getIIDPBucket();

  // 拿到 instance 和 bucket 去使用minio 接口
  String objectName = minioTemplate.putObject(byteArrayInputStream, contentType, originalFilename, bucket);
  // 其他接口等

  // 同时支持获取预签名的url。 presignedGetObjectUrl 签名是的Get方法的url,类似地有 presignedPostObjectUrl
  String url = minioTemplate.presignedGetObjectUrl("apps-bucket", "test.json");
  System.out.println(url);

  // 也可以自行指定具体的method,比如 HttpMethod.PUT,HttpMethod.GET 等,根据业务场景来获取指定的方法权限
  url = minioTemplate.presignedObjectUrl(HttpMethod.PUT, "apps-bucket", "test.json");
  System.out.println(url);

⚙️ 二、方案实施详细步骤

步骤 1: 规划与创建存储桶

  1. 创建两个桶
    • public-bucket: 用于存放可以公开访问的静态资源,如网站图片、CSS、JS、公共文档等。
    • private-bucket: 用于存放所有业务附件、用户上传的私人文件等敏感数据。
  2. 使用 mc 命令行工具创建桶
    # 设置 MinIO 服务器别名(以本地为例)
    mc alias set myminio http://your-minio-server:9000 your-admin-user your-admin-password
    
    # 创建两个桶
    mc mb myminio/public-bucket
    mc mb myminio/private-bucket
    

步骤 2: 配置桶权限策略

  1. 将公共桶设置为 download (只读) 权限
    # 设置整个 public-bucket 为只读(允许匿名下载)
    mc anonymous set download myminio/public-bucket
    

    这样,任何人都可以通过直接的 URL(如 http://your-minio-server:9000/public-bucket/image.jpg)访问这个桶里的对象,但不能上传、修改或删除

  2. 确保私有桶权限为 private
    # 确保 private-bucket 是私有状态(默认通常是 private)
    mc anonymous set none myminio/private-bucket
    

    此举确保了任何未经认证的请求(包括直接通过浏览器访问对象URL)都会被拒绝,返回 Access Denied

步骤 3: 实现安全的私有对象访问机制

这是最关键的一步。对于私有桶中的业务附件,绝对不能直接暴露 URL,必须通过后端应用生成预签名 URL

以下是一个 Spring Boot 集成 MinIO 并生成预签名 URL 的示例:

  1. 后端服务配置与代码示例

    • application.yml 配置:
        minio.public-bucket: public-bucket
        minio.private-bucket: private-bucket
      
    • Service 层:生成预签名 URL:
      @Service
      @RequiredArgsConstructor
      public class MinioService {
          private final MinioClient minioClient;
          private final MinioProperties properties; // 读取配置的类
      
          /**
           * 为私有文件生成一个有时效性的下载链接(例如7天)
           * @param objectName 对象在私有桶中的名称
           * @return 预签名的临时URL
           */
          public String getPresignedDownloadUrl(String objectName) throws Exception {
              return minioClient.getPresignedObjectUrl(
                  GetPresignedObjectUrlArgs.builder()
                          .method(Method.GET) // 指定GET方法用于下载
                          .bucket(properties.getPrivateBucket())
                          .object(objectName)
                          .expiry(7, TimeUnit.DAYS) // 设置链接7天后过期
                          .build()
              );
          }
      
          // ... 其他方法如文件上传等
      }
              
      
    • Controller 层:
      @RestController
      @RequestMapping("/api/files")
      @RequiredArgsConstructor
      public class FileController {
          private final MinioService minioService;
      
          @GetMapping("/download-url")
          public ResponseEntity<String> getDownloadUrl(@RequestParam String filename) {
              try {
                  // 1. 此处可添加业务逻辑:验证当前用户是否有权限下载此文件
                  // 2. 如果有权限,则生成预签名URL
                  String url = minioService.getPresignedDownloadUrl(filename);
                  return ResponseEntity.ok(url);
              } catch (Exception e) {
                  return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Error generating URL");
              }
          }
      }
      
  2. 前端访问流程

    1. 用户点击“下载附件”按钮。
    2. 前端调用后端 API (/api/files/download-url?filename=xxx)。
    3. 后端校验用户身份和权限后,调用 MinIO 服务生成一个临时的预签名 URL 并返回给前端。
    4. 前端接收到这个临时 URL 后,自动重定向或使用 window.location.href 打开它,即可下载文件。
    5. 这个临时 URL 过期即失效,即使被他人获取,风险也在可控范围内。

💎 总结

通过 “公共桶只读 + 私有桶完全隔离 + 后端预签名” 的组合方案,可以完美解决需求中的安全问题。

  • 对于公共资源,利用 MinIO 的 download 策略提供高效便捷的直接访问。
  • 对于业务附件等敏感数据,通过 private 策略彻底关闭公网直连,所有访问必须经由后端应用授权后,通过有时效的预签名 URL 进行。

这样既满足了业务功能,又极大地提升了系统的安全性。实施时,请务必记得结合网络隔离和 HTTPS 等额外安全措施。


在通过后端生成预签名 URL(Presigned URL)提供 MinIO 资源访问权限之前,必须对请求的用户进行身份校验。这个过程的核心是:业务后端的用户身份体系MinIO 的对象存储权限 是两套独立的系统,需要通过后端应用来“桥接”和“翻译”。

下面详细分析这个身份校验的过程和权限控制的原理。

🔐 一、 身份校验与权限控制流程

整个流程的核心在于,最终用户不直接拥有 MinIO 的权限,而是通过后端应用这个“可信中介”来间接获得临时、可控的资源访问权。下图描绘了预签名 URL 生成和使用的完整流程,其中后端对用户的身份校验是发起一切操作的起点:

sequenceDiagram participant U as 最终用户 participant A as 后端应用服务器 participant M as MinIO 服务 U->>A: 1. 请求下载/上传文件(携带Cookie/JWT) Note right of A: 身份校验阶段 A->>A: 2. 验证用户身份与权限 alt 验证失败 A-->>U: 返回错误(401/403) else 验证成功 Note right of A: 预签名URL生成阶段 A->>M: 3. 使用自身凭证请求预签名URL M-->>A: 4. 返回预签名URL A-->>U: 5. 返回预签名URL Note right of U: 直接访问阶段 U->>M: 6. 使用预签名URL直接访问MinIO M->>M: 7. 验证URL签名及有效期 alt URL验证失败 M-->>U: 返回错误(403) else URL验证成功 M-->>U: 8. 返回请求的文件或允许上传 end end

这个过程的关键在于:

  1. 最终用户后端应用之间,通过你熟悉的业务身份认证体系(如 Cookie/Session 或 JWT)来建立信任。
  2. 后端应用MinIO 之间,通过 MinIO 的 Access Key 和 Secret Key 来建立信任。
  3. 预签名 URL 则是后端应用用自己的 MinIO 凭证,为特定操作(如 GetObject)生成的一个“临时门票”。最终用户拿着这张“门票”可以直接去找 MinIO“兑换”服务,而无需再经过后端应用。

📝 二、 后端身份校验的具体内容

当后端应用收到用户请求后,它会进行一系列校验,这部分属于业务逻辑,与 MinIO 无关:

  • 身份认证 (Authentication): 确认用户是谁。通常通过验证用户提交的 Token(如 JWT)Session Cookie 来实现。
  • 权限授权 (Authorization): 判断该用户是否有权执行其请求的操作。这通常需要通过查询数据库或缓存来确认用户的角色、权限等级或是否拥有该文件的访问权。
  • 业务规则校验: 根据业务需求进行更细致的控制。例如:
    • 该文件是否属于该用户?
    • 用户是否处于试用期,禁止下载原始大小图片?
    • 用户的下载次数是否已用完?

只有通过了所有这些校验,后端应用才会动用其高级权限,向 MinIO 请求生成预签名 URL。

🔗 三、 用户身份与 MinIO 权限的关联方式

后端应用如何将“用户”这个概念映射到 MinIO 的权限上,通常有两种模式:

  1. 代理模式(最常见):
    • 设置:后端应用使用一个高权限的 MinIO 客户端(拥有读取整个存储桶的权限)。
    • 工作方式:所有用户对文件的访问请求,都通过这个高权限客户端来生成预签名 URL。MinIO 只知道请求来自后端应用,不知道最终用户是谁
    • 权限控制:权限控制完全由你的后端业务逻辑决定。MinIO 只是无条件地信任来自后端应用的请求。
    • 优点:实现简单,无需在 MinIO 中维护复杂的用户体系。
    • 缺点:权限控制粒度完全依赖于后端应用的逻辑。
  2. 映射模式(更精细):
    • 设置:为每个应用用户或在 MinIO 中创建一个对应的 IAM 用户或策略(Policy)。
    • 工作方式:后端应用在校验完用户身份后,使用对应该用户的 MinIO 凭证来生成预签名 URL。
    • 权限控制:权限控制由 MinIO 和业务后端共同完成。MinIO 能知道请求来自于哪个具体用户,并会根据为该用户设置的策略来限制其访问范围(例如,只能访问某个前缀下的文件)。
    • 优点:权限控制粒度更细,安全性更高,可以实现不同用户访问不同文件的需求。
    • 缺点:需要在 MinIO 中维护大量用户和策略,系统更复杂。

对于绝大多数应用,代理模式 已经完全足够且是推荐的做法。

🛡️ 四、 安全性与最佳实践

  1. 预签名 URL 的本质是“临时授权”:它解决了让一个无权限的用户临时获得特定资源访问权的问题,但其安全性建立在后端严格的身份校验和 URL 的短暂有效期上。
  2. 设置短暂的有效期:这是最重要的安全措施。根据场景,将有效期设置为 5 到 15 分钟,即使 URL 被泄露,风险窗口也很短。
  3. 最小权限原则:用于生成签名的 MinIO 客户端凭证,其权限应被严格限制。如果后端只需要生成下载链接,那么它的权限应只有 s3:GetObject,绝不能赋予 s3:PutObjects3:DeleteObject 等权限。
  4. 监控与审计:记录所有预签名 URL 的生成和使用日志,以便在出现安全事件时进行追踪。

💎 总结

可以明确的是:必须在生成预签名 URL 前进行身份校验

这个过程可以概括为:

  1. 用户向后台管理员(后端应用)申请参观证(请求预签名 URL)。
  2. 管理员(后端应用)首先核查你的身份和权限(业务逻辑校验)。
  3. 核查通过后,管理员用自己的高级门禁卡(MinIO 凭证)为你生成一张有时效的参观证(预签名 URL)。
  4. 你拿着这张参观证可以直接进入展厅(MinIO)访问特定展品(对象),门口的保安(MinIO 服务)只认参观证,不再核查你的原始身份。

以上是业界常用方案,对于iidp平台可以结合多租户权限来结合考虑。