Skill Index

cc-use-exp/

storage-url-safety

community[skill]

当使用 MinIO/OSS/S3 等对象存储、设计文件上传下载功能时触发。提供存储 URL 策略选择规范,防止 URL 过期、访问失败等问题。

$/plugin install cc-use-exp

details

存储 URL 策略选择规范

当使用 MinIO/OSS/S3 等对象存储时,正确选择 URL 生成策略。


陷阱 #1: 头像等长期资源使用预签名 URL

场景: 头像、Logo、商品图等需要长期访问的资源

问题根因

预签名 URL 有时效限制(MinIO/S3 最大 7 天),头像等长期资源会过期导致无法访问。

错误示例

// ❌ 错误: 预签名 URL 最大 7 天,头像会过期
String avatarUrl = minioService.getPresignedUrl(filePath, 60 * 24 * 365);
// IllegalArgumentException: expiry must be minimum 1 second to maximum 7 days

// ❌ 错误: 即使设置 7 天,头像也会在 7 天后失效
String avatarUrl = minioService.getPresignedUrl(filePath, 60 * 24 * 7);
// 7 天后用户头像显示"图片加载失败"

正确做法

// ✅ 方案1: 公开 URL(需配置 bucket 公开读)
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);
// 返回: http://minio:9000/bucket/avatars/xxx.jpeg

// ✅ 方案2: CDN URL(生产环境推荐)
String avatarUrl = cdnService.getCdnUrl(filePath);
// 返回: https://cdn.example.com/avatars/xxx.jpeg

URL 策略选择表

资源类型推荐策略有效期适用场景示例
头像/Logo公开 URL / CDN永久需长期访问用户头像、企业 Logo
商品图片公开 URL / CDN永久需长期访问电商商品图、文章配图
公开文档公开 URL / CDN永久需长期访问用户手册、API 文档
临时文件预签名 URL1h-7d下载凭证导出的 Excel、临时分享
私密文档预签名 URL15min-1h临时授权合同、财务报表
上传凭证预签名 URL5min-30min客户端直传前端直传 OSS

陷阱 #2: 公开 URL 的 Bucket 未配置公开读

场景: 使用公开 URL 但 bucket 策略未配置

错误示例

// ✅ 代码正确生成公开 URL
String avatarUrl = minioService.getPublicUrl(filePath);
// 返回: http://minio:9000/bucket/avatars/xxx.jpeg

// ❌ 但 bucket 未配置公开读,访问返回 403 Forbidden

正确做法

MinIO 配置公开读:

# 方案1: 使用 mc 命令配置(推荐)
mc anonymous set download minio/bucket/avatars

# 方案2: 使用 bucket policy
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 配置:

# 设置 bucket 公共读
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 分钟安全性要求高

错误示例

// ❌ 错误: 客户端直传凭证有效期 7 天,安全风险高
String uploadUrl = minioService.getPresignedUrl(filePath, 60 * 24 * 7);

// ❌ 错误: 导出文件下载链接只有 5 分钟,用户可能来不及下载
String downloadUrl = minioService.getPresignedUrl(filePath, 5);

正确做法

// ✅ 客户端直传凭证: 15 分钟
String uploadUrl = minioService.getPresignedUrl(filePath, 15);

// ✅ 导出文件下载: 24 小时
String downloadUrl = minioService.getPresignedUrl(filePath, 60 * 24);

// ✅ 私密文档查看: 30 分钟
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("不支持的文件类型");
    }

    // 校验文件大小(5MB)
    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 加速但回源配置不正确

错误示例

// ✅ 代码正确返回 CDN URL
String avatarUrl = "https://cdn.example.com/avatars/xxx.jpeg";

// ❌ 但 CDN 回源配置错误:
// 1. 回源 Host 未设置为 MinIO endpoint
// 2. 回源协议未设置为 HTTP
// 3. 回源路径未包含 bucket 名称
// 导致 CDN 返回 404 或 403

正确做法

阿里云 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 排查,形成"散弹式修复"

错误示例

// ❌ 错误: 每个 Service 各写一份
// ProductService.java
private String resolveImageUrl(String imageUrl) {
    if (imageUrl == null || imageUrl.startsWith("http")) return imageUrl;
    return minioService.getPresignedUrl(imageUrl, 60 * 24 * 7);
}

// MiniProductService.java — 又写一份
private String resolveImageUrl(String imageUrl) { /* 同样逻辑 */ }

// CartService.java — 又写一份
private String resolveImageUrl(String imageUrl) { /* 同样逻辑 */ }

// MiniOrderService.java — 又写一份...

正确做法

// ✅ 正确: 抽成共享工具类,所有 Service 复用
@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 注入后直接用
@Service
public class ProductService {
    private final ImageUrlResolver imageUrlResolver;

    private ProductDTO convertToDTO(Product product) {
        return ProductDTO.builder()
            .mainImageUrl(imageUrlResolver.resolve(mainImagePath))
            .build();
    }
}

检查清单

  • 项目中是否有统一的图片 URL 补全工具类
  • 新增返回图片字段的接口时,是否经过了统一补全
  • 是否存在 3+ 个 Service 各自写了相同的 URL 补全逻辑
  • 修复图片显示问题时,是否先做全局扫描(grep getImageUrl)再一次性补齐

检查清单(存储 URL 策略)

URL 策略选择:

  • 头像/Logo 是否使用公开 URL 或 CDN
  • 临时文件是否使用预签名 URL
  • 预签名 URL 的有效期是否 ≤ 7 天
  • 预签名 URL 的有效期是否符合业务场景

Bucket 配置:

  • 公开 URL 的 bucket 是否配置了公开读策略
  • 公开读策略是否只针对特定目录(如 avatars/)
  • 是否配置了 CORS(前端直传需要)

CDN 配置:

  • 生产环境是否使用 CDN 加速
  • CDN 回源 Host 是否正确
  • CDN 回源路径是否包含 bucket 名称
  • CDN 是否配置了缓存规则

安全性:

  • 前端直传是否校验文件类型和大小
  • 文件名是否使用 UUID 防止路径遍历
  • 私密文件是否使用预签名 URL 而非公开 URL

响应层 URL 补全:

  • 是否有统一的图片 URL 补全工具类(而非各 Service 各写一份)
  • 新增图片字段接口是否经过统一补全
  • 修复图片问题时是否先全局扫描再一次性补齐

适用范围

  • MinIO
  • 阿里云 OSS
  • 腾讯云 COS
  • AWS S3
  • 七牛云 Kodo
  • 华为云 OBS

规则溯源

> 📋 本回复遵循:`storage-url-safety` - 存储 URL 策略选择规范

technical

github
doccker/cc-use-exp
stars
755
license
NOASSERTION
contributors
5
last commit
2026-05-29T03:21:43Z
file
.claude/skills/storage-url-safety/SKILL.md

related