[SpringBoot 对象存储实战]:预签名 URL 直传 OSS 全流程设计与实现

🔥你好我是fengxin_rou这是我的个人主页 fengxin_rou的主页

❄️欢迎查看我的专栏我的专栏

《Java后端学习》《JAVASE基础》《JUC并发》《redis》《JVM虚拟机》《MYSQL》《黑马点评》《rabbitmq》《JavaWeb+AI的talis学习系统》《苍穹外卖》

目录

前言

一、预签名直传:核心原理与优势

[1.1 传统上传 vs 预签名直传](#1.1 传统上传 vs 预签名直传)

[1.2 核心优势](#1.2 核心优势)

[1.3 整体流程](#1.3 整体流程)

二、请求实体设计:标准化上传参数

字段说明

[三、OSS 存储服务:核心工具类实现](#三、OSS 存储服务:核心工具类实现)

[3.1 头像上传(普通上传示例)](#3.1 头像上传(普通上传示例))

[3.2 生成公开访问 URL](#3.2 生成公开访问 URL)

[3.3 生成预签名 PUT URL(核心)](#3.3 生成预签名 PUT URL(核心))

[四、Controller 接口实现:权限校验与业务封装](#四、Controller 接口实现:权限校验与业务封装)

[4.1 权限校验关键点](#4.1 权限校验关键点)

[4.2 返回体结构](#4.2 返回体结构)

五、前端直传执行流程

前端请求示例

结语


前言

在内容社区、自媒体平台等场景中,图片、文件上传是高频需求。 传统后端中转上传会占用大量带宽与内存,高并发下极易成为性能瓶颈。 本文基于 SpringBoot + 阿里云 OSS,实现预签名 URL 客户端直传方案,彻底解放后端服务,兼顾安全、高效与易用。


一、预签名直传:核心原理与优势

预签名 URL 是对象存储提供的临时授权机制。 后端用密钥生成带时效、带权限的签名 URL,前端直接用此 URL 上传 / 下载文件,无需透传密钥。

1.1 传统上传 vs 预签名直传

  • 传统上传:客户端→后端→OSS,后端中转流量,压力大、速度慢。
  • 预签名直传:客户端请求签名→后端返回 URL→客户端直传 OSS→通知后端入库。

1.2 核心优势

  1. 服务无压力:后端不处理文件流,仅做授权与记录。
  2. 上传速度快:客户端直连 OSS,带宽不受后端限制。
  3. 安全可控:URL 有时效、有路径、有权限,防止越权与盗传。
  4. 成本更低:减少服务器出口带宽消耗。

1.3 整体流程

  1. 前端请求获取预签名上传 URL。
  2. 后端校验权限、生成路径、返回带签名 URL 与访问 URL。
  3. 前端直接 PUT 上传到 OSS。
  4. 上传完成后通知后端,保存文件 URL 到业务库。

二、请求实体设计:标准化上传参数

统一请求体,明确场景、文件标识、类型,便于权限校验与路径生成。

复制代码
/**
 * 预签名直传请求实体
 */
public record StoragePresignRequest(
    @NotBlank String scene,      // 上传场景:文章内容/图片
    @NotBlank String postId,     // 帖子ID(字符串避免精度丢失)
    @NotBlank String contentType,// 文件类型:image/jpeg、video/mp4
    String ext                   // 文件扩展名
) {}

字段说明

  • scene:区分业务场景,便于权限控制与目录隔离。
  • postId:关联业务 ID,用于校验归属、防止越权。
  • contentType:必须与前端上传时一致,否则 OSS 验签失败。
  • ext:扩展名,用于生成规范文件路径。

三、OSS 存储服务:核心工具类实现

封装 OSS 基础操作,包括普通上传、生成公开 URL、生成预签名 URL

3.1 头像上传(普通上传示例)

适用于小文件、后端可承接的简单上传场景。

复制代码
/**
 * 头像上传到 OSS
 */
public String uploadAvatar(long userId, MultipartFile file) {
    // 1.校验 OSS 配置
    ensureConfigured();
    // 2.提取文件扩展名
    String original = file.getOriginalFilename();
    String ext = "";
    if (original != null && original.contains(".")) {
        ext = original.substring(original.lastIndexOf("."));
    }
    // 3.生成唯一文件路径
    String objectKey = props.getFolder() + "/" + userId + "-" + System.currentTimeMillis() + ext;
    // 4.创建 OSS 客户端
    OSS client = new OSSClientBuilder().build(
        props.getEndpoint(),
        props.getAccessKeyId(),
        props.getAccessKeySecret()
    );
    try {
        // 5.流式上传
        PutObjectRequest request = new PutObjectRequest(
            props.getBucket(),
            objectKey,
            file.getInputStream()
        );
        client.putObject(request);
    } catch (IOException e) {
        throw new BusinessException(ErrorCode.BAD_REQUEST, "文件读取失败");
    } finally {
        client.shutdown();
    }
    // 6.返回可访问 URL
    return publicUrl(objectKey);
}

3.2 生成公开访问 URL

拼接 CDN 域名,规范路径,避免双斜杠问题。

复制代码
/**
 * 生成公开访问 URL
 */
private String publicUrl(String objectKey) {
    if (props.getPublicDomain() != null && !props.getPublicDomain().isBlank()) {
        return props.getPublicDomain().replaceAll("/$", "") + "/" + objectKey;
    }
    return "https://" + props.getBucket() + "." + props.getEndpoint() + "/" + objectKey;
}

3.3 生成预签名 PUT URL(核心)

生成前端可直接 PUT 上传的临时 URL,必须指定 ContentType

复制代码
/**
 * 生成预签名 PUT 上传 URL
 */
public String generatePresignedPutUrl(
    String objectKey,
    String contentType,
    int expiresInSeconds
) {
    ensureConfigured();
    OSS client = new OSSClientBuilder().build(
        props.getEndpoint(),
        props.getAccessKeyId(),
        props.getAccessKeySecret()
    );
    try {
        // 设置过期时间
        Date expiration = new Date(System.currentTimeMillis() + expiresInSeconds * 1000L);
        GeneratePresignedUrlRequest request = new GeneratePresignedUrlRequest(
            props.getBucket(),
            objectKey,
            HttpMethod.PUT
        );
        request.setExpiration(expiration);
        // 必须设置 ContentType,否则前端上传会验签失败
        if (contentType != null && !contentType.isBlank()) {
            request.setContentType(contentType);
        }
        URL url = client.generatePresignedUrl(request);
        return url.toString();
    } finally {
        client.shutdown();
    }
}

四、Controller 接口实现:权限校验与业务封装

提供对外接口,完成用户认证、权限校验、路径生成、签名返回

复制代码
/**
 * 预签名直传接口
 */
@PostMapping("/presign")
public StoragePresignResponse presign(
    @Valid @RequestBody StoragePresignRequest request,
    @AuthenticationPrincipal Jwt jwt
) {
    // 1.获取当前登录用户ID
    long userId = jwtService.extractUserId(jwt);
    // 2.postId 字符串转数字,防止前端精度丢失
    long postId;
    try {
        postId = Long.parseLong(request.postId());
    } catch (NumberFormatException e) {
        throw new BusinessException(ErrorCode.BAD_REQUEST, "postId 格式非法");
    }
    // 3.校验帖子归属,防止越权上传
    KnowPost post = knowPostMapper.findById(postId);
    if (post == null || !userId.equals(post.getCreatorId())) {
        throw new BusinessException(ErrorCode.BAD_REQUEST, "无权限操作该草稿");
    }
    // 4.按场景生成文件存储路径
    String scene = request.scene();
    String ext = normalizeExt(request.ext(), request.contentType(), scene);
    String objectKey;
    if ("knowpost_content".equals(scene)) {
        objectKey = "posts/" + postId + "/content" + ext;
    } else if ("knowpost_image".equals(scene)) {
        String date = DateTimeFormatter.ofPattern("yyyyMMdd")
            .withZone(ZoneId.of("UTC")).format(Instant.now());
        String rand = UUID.randomUUID().toString().replace("-", "").substring(0, 8);
        objectKey = "posts/" + postId + "/images/" + date + "/" + rand + ext;
    } else {
        throw new BusinessException(ErrorCode.BAD_REQUEST, "不支持的上传场景");
    }
    // 5.生成10分钟有效期的预签名 URL
    int expiresIn = 600;
    String uploadUrl = ossStorageService.generatePresignedPutUrl(
        objectKey,
        request.contentType(),
        expiresIn
    );
    Map<String, String> headers = Map.of("Content-Type", request.contentType());
    // 6.返回:上传URL、文件URL、请求头、过期时间
    return new StoragePresignResponse(objectKey, uploadUrl, headers, expiresIn);
}

4.1 权限校验关键点

  • 必须校验 postId 对应用户是否为当前登录用户
  • 无校验会导致越权覆盖、恶意上传、数据污染。
  • 这是系统安全的核心防线

4.2 返回体结构

复制代码
{
  "objectKey": "posts/123/images/20260524/xxxx.jpg",
  "uploadUrl": "https://xxx.oss-cn-beijing.aliyuncs.com/...?X-Oss-Signature=xxx",
  "headers": { "Content-Type": "image/jpeg" },
  "expireSeconds": 600
}

五、前端直传执行流程

  1. 调用 /api/v1/storage/presign 获取上传 URL。
  2. 使用 PUT 方法,携带指定 Content-Type 上传文件。
  3. 上传成功后,将 fileUrl 提交给后端业务接口保存。

前端请求示例

复制代码
// 获取预签名
const { uploadUrl, headers } = await api.post("/storage/presign", {
  scene: "knowpost_image",
  postId: "123",
  contentType: "image/jpeg",
  ext: "jpg"
});
// 直传 OSS
await axios.put(uploadUrl, file, { headers });

结语

本文完整实现 SpringBoot + OSS 预签名 URL 直传方案,覆盖请求设计、服务封装、权限校验、接口开放全流程。 该方案大幅降低服务负载,提升上传速度与系统吞吐量,适合文章、图片、视频等大文件上传场景。

实际使用建议:

  1. 预签名有效期控制在 5--10 分钟
  2. 按业务场景做目录隔离。
  3. 接入 CDN 加速文件访问。
  4. 增加文件大小、类型、数量限流防护。
相关推荐
AINative软件工程6 分钟前
MCP Server 权限边界工程实践:OAuth、最小权限与工具沙箱,别让 Agent 拿到整台机器
架构·llm·ai编程
微三云 - 廖会灵 (私域系统开发)24 分钟前
企业级系统开发避坑指南:源码交付与高并发架构,我们为什么最终选了微三云
架构
程序员小羊!10 小时前
集团多事业部架构下数仓分层建模规范
架构
ARM|X86+FPGA工业主板厂家11 小时前
RK3588+FPGA+EtherCAT异构架构解析|工业场景如何同时保住AI算力与微秒级运动实时性
人工智能·fpga开发·架构
小码哥哥12 小时前
四大企业AI知识库技术架构深度对比:从设计哲学到实现差异
人工智能·架构
fīɡЙtīиɡ ℡13 小时前
内存泄漏产生的原因
java·spring·servlet
xixingzhe214 小时前
spring ai简单使用skills
数据库·人工智能·spring
乐观的Terry18 小时前
8、发布系统-完整流水线的核心
java·spring boot·spring·spring cloud
2601_9605679618 小时前
电商套图批处理架构的性能分析——逐图生成与流水线模式的工程对比
架构
老刘说AI19 小时前
SGLang 深度优化: Radix 缓存与复杂任务的极致吞吐
人工智能·神经网络·机器学习·缓存·架构·sglang