业务和公共分桶方案
现状分析
关于 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)级别 进行设置。
-
桶策略 (Bucket Policy):这是最主要的方式。你可以为整个桶或桶内特定目录前缀(Prefix)设置详细的访问规则(Allow/Deny),规定谁(Principal)能对哪些资源(Resource)执行什么操作(Action,如
s3:GetObject
)。 - 访问控制列表 (ACL):一种相对传统的权限控制方式,可以为匿名用户或特定用户设置路径级权限。
- 预签名 URL (Presigned URL):这是访问私有资源最安全的方式。后端应用使用自己的 Access Key 和 Secret Key 生成一个有时效性的临时网址,分享给前端。在有效期内,即使是没有凭证的用户也能通过此链接访问或上传特定对象,过期后自动失效。
以下是三种常见的桶权限级别对比:
权限类型 | 描述 | 适用场景 | 风险等级 |
---|---|---|---|
private |
完全私有,需签名 URL | 敏感数据、业务附件 | 低 |
download |
只读公开,允许列出和下载文件 | 公开文档、静态资源 | 中 |
public |
完全公开,允许读写删 | 公共上传区(谨慎使用) | 高 |
对应的方案是:创建一个 public
(或 download
) 桶存放公共资源,一个 private
桶存放业务附件。
目前实施方案
目前实施的方案是为平台新增一个公有桶,平台业务涉及到的对象需存储到公有桶中,其他非业务依然沿用原有桶。 但是需要引擎和平台app做相应的调整。
-
- 新增两个配置项,分别是后端配置和前端配置;
# 后端配置项 minio.bucket.edition=v2 minio.iidpBucket=iidp-bucket # 前端配置项 bucketType = public/private
- 新增两个配置项,分别是后端配置和前端配置;
-
- 为什么要有两个配置项
-
2.1. 后端配置功能开关,是为了兼容已有的系统,如果没有配置
minio.bucket.edition=v2
则所有逻辑都保持原来一致,如果有配置则按照新逻辑执行; -
2.2. 前端配置是为了给业务一个选项,因为暴露给前端的接口可能由平台调用也可能由业务调用,所以需要传递一个标识来表明是否需要存储到共有桶,默认行为与原有逻辑保持一致。
-
- 调整
-
3.1. 引擎在
/file/upload
接口需要处理两个配置项,即前端和后端,只有两个配置项都满足的情况下才执行新逻辑,否则依然是原来逻辑。 -
3.2. 类似地,平台app 也需要处理两个配置项,但是如果只有一个配置项那就只需要处理一个,比如不需要前端页面来上传。
-
- 样板代码
// 获取 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: 规划与创建存储桶
-
创建两个桶:
-
public-bucket
: 用于存放可以公开访问的静态资源,如网站图片、CSS、JS、公共文档等。 -
private-bucket
: 用于存放所有业务附件、用户上传的私人文件等敏感数据。
-
-
使用
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: 配置桶权限策略
-
将公共桶设置为
download
(只读) 权限:# 设置整个 public-bucket 为只读(允许匿名下载) mc anonymous set download myminio/public-bucket
这样,任何人都可以通过直接的 URL(如
http://your-minio-server:9000/public-bucket/image.jpg
)访问这个桶里的对象,但不能上传、修改或删除。 -
确保私有桶权限为
private
:# 确保 private-bucket 是私有状态(默认通常是 private) mc anonymous set none myminio/private-bucket
此举确保了任何未经认证的请求(包括直接通过浏览器访问对象URL)都会被拒绝,返回
Access Denied
。
步骤 3: 实现安全的私有对象访问机制
这是最关键的一步。对于私有桶中的业务附件,绝对不能直接暴露 URL,必须通过后端应用生成预签名 URL。
以下是一个 Spring Boot 集成 MinIO 并生成预签名 URL 的示例:
-
后端服务配置与代码示例:
-
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"); } } }
-
application.yml 配置:
-
前端访问流程:
- 用户点击“下载附件”按钮。
- 前端调用后端 API (
/api/files/download-url?filename=xxx
)。 - 后端校验用户身份和权限后,调用 MinIO 服务生成一个临时的预签名 URL 并返回给前端。
- 前端接收到这个临时 URL 后,自动重定向或使用
window.location.href
打开它,即可下载文件。 - 这个临时 URL 过期即失效,即使被他人获取,风险也在可控范围内。
💎 总结
通过 “公共桶只读 + 私有桶完全隔离 + 后端预签名” 的组合方案,可以完美解决需求中的安全问题。
- 对于公共资源,利用 MinIO 的
download
策略提供高效便捷的直接访问。 - 对于业务附件等敏感数据,通过
private
策略彻底关闭公网直连,所有访问必须经由后端应用授权后,通过有时效的预签名 URL 进行。
这样既满足了业务功能,又极大地提升了系统的安全性。实施时,请务必记得结合网络隔离和 HTTPS 等额外安全措施。
在通过后端生成预签名 URL(Presigned URL)提供 MinIO 资源访问权限之前,必须对请求的用户进行身份校验。这个过程的核心是:业务后端的用户身份体系 与 MinIO 的对象存储权限 是两套独立的系统,需要通过后端应用来“桥接”和“翻译”。
下面详细分析这个身份校验的过程和权限控制的原理。
🔐 一、 身份校验与权限控制流程
整个流程的核心在于,最终用户不直接拥有 MinIO 的权限,而是通过后端应用这个“可信中介”来间接获得临时、可控的资源访问权。下图描绘了预签名 URL 生成和使用的完整流程,其中后端对用户的身份校验是发起一切操作的起点:
这个过程的关键在于:
- 最终用户与后端应用之间,通过你熟悉的业务身份认证体系(如 Cookie/Session 或 JWT)来建立信任。
- 后端应用与 MinIO 之间,通过 MinIO 的 Access Key 和 Secret Key 来建立信任。
- 预签名 URL 则是后端应用用自己的 MinIO 凭证,为特定操作(如 GetObject)生成的一个“临时门票”。最终用户拿着这张“门票”可以直接去找 MinIO“兑换”服务,而无需再经过后端应用。
📝 二、 后端身份校验的具体内容
当后端应用收到用户请求后,它会进行一系列校验,这部分属于业务逻辑,与 MinIO 无关:
- 身份认证 (Authentication): 确认用户是谁。通常通过验证用户提交的 Token(如 JWT) 或 Session Cookie 来实现。
- 权限授权 (Authorization): 判断该用户是否有权执行其请求的操作。这通常需要通过查询数据库或缓存来确认用户的角色、权限等级或是否拥有该文件的访问权。
-
业务规则校验: 根据业务需求进行更细致的控制。例如:
- 该文件是否属于该用户?
- 用户是否处于试用期,禁止下载原始大小图片?
- 用户的下载次数是否已用完?
只有通过了所有这些校验,后端应用才会动用其高级权限,向 MinIO 请求生成预签名 URL。
🔗 三、 用户身份与 MinIO 权限的关联方式
后端应用如何将“用户”这个概念映射到 MinIO 的权限上,通常有两种模式:
-
代理模式(最常见):
- 设置:后端应用使用一个高权限的 MinIO 客户端(拥有读取整个存储桶的权限)。
- 工作方式:所有用户对文件的访问请求,都通过这个高权限客户端来生成预签名 URL。MinIO 只知道请求来自后端应用,不知道最终用户是谁。
- 权限控制:权限控制完全由你的后端业务逻辑决定。MinIO 只是无条件地信任来自后端应用的请求。
- 优点:实现简单,无需在 MinIO 中维护复杂的用户体系。
- 缺点:权限控制粒度完全依赖于后端应用的逻辑。
-
映射模式(更精细):- 设置:为每个应用用户或在 MinIO 中创建一个对应的 IAM 用户或策略(Policy)。
- 工作方式:后端应用在校验完用户身份后,使用对应该用户的 MinIO 凭证来生成预签名 URL。
- 权限控制:权限控制由 MinIO 和业务后端共同完成。MinIO 能知道请求来自于哪个具体用户,并会根据为该用户设置的策略来限制其访问范围(例如,只能访问某个前缀下的文件)。
- 优点:权限控制粒度更细,安全性更高,可以实现不同用户访问不同文件的需求。
- 缺点:需要在 MinIO 中维护大量用户和策略,系统更复杂。
对于绝大多数应用,代理模式 已经完全足够且是推荐的做法。
🛡️ 四、 安全性与最佳实践
- 预签名 URL 的本质是“临时授权”:它解决了让一个无权限的用户临时获得特定资源访问权的问题,但其安全性建立在后端严格的身份校验和 URL 的短暂有效期上。
- 设置短暂的有效期:这是最重要的安全措施。根据场景,将有效期设置为 5 到 15 分钟,即使 URL 被泄露,风险窗口也很短。
-
最小权限原则:用于生成签名的 MinIO 客户端凭证,其权限应被严格限制。如果后端只需要生成下载链接,那么它的权限应只有
s3:GetObject
,绝不能赋予s3:PutObject
或s3:DeleteObject
等权限。 - 监控与审计:记录所有预签名 URL 的生成和使用日志,以便在出现安全事件时进行追踪。
💎 总结
可以明确的是:必须在生成预签名 URL 前进行身份校验。
这个过程可以概括为:
- 用户向后台管理员(后端应用)申请参观证(请求预签名 URL)。
- 管理员(后端应用)首先核查你的身份和权限(业务逻辑校验)。
- 核查通过后,管理员用自己的高级门禁卡(MinIO 凭证)为你生成一张有时效的参观证(预签名 URL)。
- 你拿着这张参观证可以直接进入展厅(MinIO)访问特定展品(对象),门口的保安(MinIO 服务)只认参观证,不再核查你的原始身份。
以上是业界常用方案,对于iidp平台可以结合多租户权限来结合考虑。