🔥你好我是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 核心优势
- 服务无压力:后端不处理文件流,仅做授权与记录。
- 上传速度快:客户端直连 OSS,带宽不受后端限制。
- 安全可控:URL 有时效、有路径、有权限,防止越权与盗传。
- 成本更低:减少服务器出口带宽消耗。
1.3 整体流程
- 前端请求获取预签名上传 URL。
- 后端校验权限、生成路径、返回带签名 URL 与访问 URL。
- 前端直接 PUT 上传到 OSS。
- 上传完成后通知后端,保存文件 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
}
五、前端直传执行流程
- 调用
/api/v1/storage/presign获取上传 URL。 - 使用 PUT 方法,携带指定 Content-Type 上传文件。
- 上传成功后,将 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 直传方案,覆盖请求设计、服务封装、权限校验、接口开放全流程。 该方案大幅降低服务负载,提升上传速度与系统吞吐量,适合文章、图片、视频等大文件上传场景。
实际使用建议:
- 预签名有效期控制在 5--10 分钟。
- 按业务场景做目录隔离。
- 接入 CDN 加速文件访问。
- 增加文件大小、类型、数量限流防护。
