| name | storage-url-safety |
| description | 当使用 MinIO/OSS/S3 等对象存储、设计文件上传下载功能时触发。提供存储 URL 策略选择规范,防止 URL 过期、访问失败等问题。 |
存储 URL 策略选择规范
当使用 MinIO/OSS/S3 等对象存储时,正确选择 URL 生成策略。
陷阱 #1: 头像等长期资源使用预签名 URL
场景: 头像、Logo、商品图等需要长期访问的资源
问题根因
预签名 URL 有时效限制(MinIO/S3 最大 7 天),头像等长期资源会过期导致无法访问。
错误示例
String avatarUrl = minioService.getPresignedUrl(filePath, 60 * 24 * 365);
String avatarUrl = minioService.getPresignedUrl(filePath, 60 * 24 * 7);
正确做法
public String getPublicUrl(String filePath) {
String endpoint = minioConfig.getEndpoint();
if (endpoint.endsWith("/")) {
endpoint = endpoint.substring(0, endpoint.length() - 1);
}
return endpoint + "/" + minioConfig.getBucketName() + "/" + filePath;
}
String avatarUrl = minioService.getPublicUrl(filePath);
String avatarUrl = cdnService.getCdnUrl(filePath);
URL 策略选择表
| 资源类型 | 推荐策略 | 有效期 | 适用场景 | 示例 |
|---|
| 头像/Logo | 公开 URL / CDN | 永久 | 需长期访问 | 用户头像、企业 Logo |
| 商品图片 | 公开 URL / CDN | 永久 | 需长期访问 | 电商商品图、文章配图 |
| 公开文档 | 公开 URL / CDN | 永久 | 需长期访问 | 用户手册、API 文档 |
| 临时文件 | 预签名 URL | 1h-7d | 下载凭证 | 导出的 Excel、临时分享 |
| 私密文档 | 预签名 URL | 15min-1h | 临时授权 | 合同、财务报表 |
| 上传凭证 | 预签名 URL | 5min-30min | 客户端直传 | 前端直传 OSS |
陷阱 #2: 公开 URL 的 Bucket 未配置公开读
场景: 使用公开 URL 但 bucket 策略未配置
错误示例
String avatarUrl = minioService.getPublicUrl(filePath);
正确做法
MinIO 配置公开读:
mc anonymous set download minio/bucket/avatars
mc admin policy attach minio readonly --user=public
Bucket Policy 示例:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {"AWS": ["*"]},
"Action": ["s3:GetObject"],
"Resource": ["arn:aws:s3:::bucket/avatars/*"]
}
]
}
阿里云 OSS 配置:
ossutil64 set-acl oss://bucket-name public-read
ossutil64 set-acl oss://bucket-name/avatars/ public-read --recursive
陷阱 #3: 预签名 URL 的有效期设置不当
场景: 临时文件下载链接有效期过长或过短
规范
| 场景 | 推荐有效期 | 说明 |
|---|
| 客户端直传凭证 | 5-30 分钟 | 上传时间通常很短 |
| 临时分享链接 | 1-24 小时 | 用户可能稍后下载 |
| 导出文件下载 | 1-7 天 | 用户可能多次下载 |
| 私密文档查看 | 15-60 分钟 | 安全性要求高 |
错误示例
String uploadUrl = minioService.getPresignedUrl(filePath, 60 * 24 * 7);
String downloadUrl = minioService.getPresignedUrl(filePath, 5);
正确做法
String uploadUrl = minioService.getPresignedUrl(filePath, 15);
String downloadUrl = minioService.getPresignedUrl(filePath, 60 * 24);
String viewUrl = minioService.getPresignedUrl(filePath, 30);
陷阱 #4: 前端直传时未校验文件类型和大小
场景: 前端直传 OSS,后端生成上传凭证
错误示例
@PostMapping("/upload/token")
public ApiResponse<String> getUploadToken(@RequestParam String filename) {
String uploadUrl = minioService.getPresignedUrl("uploads/" + filename, 15);
return ApiResponse.success(uploadUrl);
}
正确做法
@PostMapping("/upload/token")
public ApiResponse<UploadToken> getUploadToken(
@RequestParam String filename,
@RequestParam String contentType,
@RequestParam Long fileSize) {
List<String> allowedTypes = Arrays.asList("image/jpeg", "image/png", "image/gif");
if (!allowedTypes.contains(contentType)) {
return ApiResponse.error("不支持的文件类型");
}
if (fileSize > 5 * 1024 * 1024) {
return ApiResponse.error("文件大小不能超过 5MB");
}
String safeFilename = UUID.randomUUID() + getExtension(filename);
String filePath = "avatars/" + LocalDate.now() + "/" + safeFilename;
String uploadUrl = minioService.getPresignedUrl(filePath, 15);
return ApiResponse.success(new UploadToken(uploadUrl, filePath));
}
陷阱 #5: CDN 回源配置错误
场景: 使用 CDN 加速但回源配置不正确
错误示例
String avatarUrl = "https://cdn.example.com/avatars/xxx.jpeg";
正确做法
阿里云 CDN 回源配置:
回源 Host: minio.example.com
回源协议: HTTP
回源地址: minio.example.com:9000
回源路径: /bucket${uri}
腾讯云 CDN 回源配置:
源站类型: 自有源
源站地址: minio.example.com:9000
回源协议: HTTP
回源 Host: minio.example.com
回源路径: /bucket${uri}
陷阱 #6: 响应层 URL 补全逻辑散落在各 Service
场景: 数据库存储相对路径(如 images/4/2026-04/xxx.png),多个 Service 各自写一份 resolveImageUrl 方法将 path 转为可访问 URL
问题根因
当"数据库存 path、响应时补全 URL"成为项目约定后,每个返回图片字段的 Service 都需要做 URL 补全。如果没有统一工具方法,就会出现:
- 7+ 个 Service 各写一份几乎相同的
resolveImageUrl
- 新增接口时容易忘记补全,导致前端拿到相对路径无法显示图片
- 修复时逐个 Service 排查,形成"散弹式修复"
错误示例
private String resolveImageUrl(String imageUrl) {
if (imageUrl == null || imageUrl.startsWith("http")) return imageUrl;
return minioService.getPresignedUrl(imageUrl, 60 * 24 * 7);
}
private String resolveImageUrl(String imageUrl) { }
private String resolveImageUrl(String imageUrl) { }
正确做法
@Component
public class ImageUrlResolver {
private final MinioService minioService;
public String resolve(String imageUrl) {
if (imageUrl == null || imageUrl.isBlank()) return imageUrl;
if (imageUrl.startsWith("http://") || imageUrl.startsWith("https://")) {
return imageUrl;
}
return minioService.getPresignedUrl(imageUrl, 60 * 24 * 7);
}
public List<String> resolveAll(List<String> urls) {
if (urls == null) return List.of();
return urls.stream().map(this::resolve).toList();
}
}
@Service
public class ProductService {
private final ImageUrlResolver imageUrlResolver;
private ProductDTO convertToDTO(Product product) {
return ProductDTO.builder()
.mainImageUrl(imageUrlResolver.resolve(mainImagePath))
.build();
}
}
检查清单
检查清单(存储 URL 策略)
URL 策略选择:
Bucket 配置:
CDN 配置:
安全性:
响应层 URL 补全:
适用范围
- MinIO
- 阿里云 OSS
- 腾讯云 COS
- AWS S3
- 七牛云 Kodo
- 华为云 OBS
规则溯源
> 📋 本回复遵循:`storage-url-safety` - 存储 URL 策略选择规范