当前位置: 首页 > news >正文

阿里云 OSS 前端直传实战:表单上传 + Policy 模式详解

在现代 Web 应用中,用户上传图片、视频等文件已成为标配功能。如果所有文件都经过后端中转,不仅增加服务器带宽压力,还可能导致性能瓶颈。阿里云 OSS(Object Storage Service)提供了强大的对象存储能力,支持前端直传,真正做到“上传不走后端”。本文将带你深入实践使用 Post Policy 表单上传 实现前端直传,并对比 STS Token 模式,分析其优劣。同时,我们将展示如何通过 Nacos 动态配置管理 OSS 参数。

前端直传 + 后端签发 Policy

我们采用 OSS 表单上传(Post Object) 方式,流程如下:

Nacos 动态配置管理

为了使系统更加灵活和可维护,我们可以利用 Nacos 进行动态配置管理。下面是一个简单的 OSS 配置类示例,它可以从 Nacos 的 YAML 文件中注入配置,并初始化 OSS 客户端。

@Configuration
@Data
@RefreshScope // 当 Nacos 配置变化时,自动刷新此 Bean
public class OssConfig {@Value("${spring.cloud.alicloud.oss.endpoint}")private String endpoint;@Value("${spring.cloud.alicloud.oss.bucket}")private String bucket;@Value("${spring.cloud.alicloud.access-key}")private String accessId;@Value("${spring.cloud.alicloud.secret-key}")private String accessKey;/*** 创建 OSS 客户端*/public OSSClient initClient() {OSS client = new OSSClientBuilder().build(endpoint, accessId, accessKey);return (OSSClient)client;}/*** 查询存储桶是否存在 例如:传入参数examplebucket-1250000000,返回true代表存在此桶*/public boolean doesBucketExist(OSSClient client, String bucketName) {try {return client.doesBucketExist(bucketName);} catch (OSSException | ClientException e) {throw new RRException(e.getMessage());}}
}

后端:签发上传策略(Policy)

@RestController
@RequestMapping("/oss")
public class ThirdFileController {@Autowiredprivate OssConfig ossConfig;/*** 获取 OSS 上传所需的签名信息(Policy 模式)*/@PostMapping("/policy")public R policy(@RequestBody FileUploadForm form) {String bucket = ossConfig.getBucket();String endpoint = ossConfig.getEndpoint();OSSClient ossClient = ossConfig.initClient();// 校验文件类型 & 业务模块Assert.isTrue(EnumUtil.contains(FileTypeEnum.class, form.getFileType()), "文件类型错误");Assert.isTrue(EnumUtil.contains(BizModuleNameEnum.class, form.getBizModuleName()), "业务类型错误");Assert.isTrue(ossConfig.doesBucketExist(ossClient, bucket), "存储桶不存在");// 上传路径: bizModule/fileType/yyyy/MM/dd/uuidString objectName = form.getBizModuleName() + "/" +form.getFileType() + "/" +new SimpleDateFormat("yyyy/MM/dd").format(new Date()) +UUID.randomUUID().toString();String host = "https://" + bucket + "." + endpoint;// 设置过期时间(30秒)long expireTime = 30;Date expiration = new Date(System.currentTimeMillis() + expireTime * 1000);// 构建 Policy 条件PolicyConditions policyConds = new PolicyConditions();policyConds.addConditionItem(PolicyConditions.COND_CONTENT_LENGTH_RANGE, 0, 1048576000); // 限制大小 0~1GBpolicyConds.addConditionItem(MatchMode.StartWith, PolicyConditions.COND_KEY, objectName); // 限制上传路径前缀policyConds.addConditionItem(PolicyConditions.COND_CONTENT_TYPE, "image/*"); // 仅允许图片类型// 生成 Post PolicyString postPolicy = ossClient.generatePostPolicy(expiration, policyConds);String encodedPolicy = BinaryUtil.toBase64String(postPolicy.getBytes(StandardCharsets.UTF_8));String signature = ossClient.calculatePostSignature(postPolicy);// 返回前端所需参数Map<String, String> resData = new HashMap<>();resData.put("accessid", ossConfig.getAccessId());resData.put("policy", encodedPolicy);resData.put("signature", signature);resData.put("dir", objectName);resData.put("host", host);resData.put("expire", String.valueOf(expiration.getTime() / 1000));ossClient.shutdown();return R.ok().put("data", resData);}/*** 获取文件预签名访问 URL(用于前端预览)*/@GetMapping("/getPreSignedUrl")public R getPreSignedUrl(@RequestParam String objectName) {OSSClient ossClient = ossConfig.initClient();Date expire = new Date(System.currentTimeMillis() + 3600 * 1000); // 1小时有效String url = ossClient.generatePresignedUrl(ossConfig.getBucket(), objectName, expire).toString();ossClient.shutdown();return R.ok().put("data", url);}
}

前端: 直传 OSS

<template><div><el-uploadclass="upload-demo"action="#":on-preview="handlePreview":on-remove="handleRemove":file-list="fileList":before-upload="onBeforeUpload":limit="1":on-success="onSuccess"list-type="picture"><el-button size="small" type="primary">点击上传</el-button><div slot="tip" class="el-upload__tip">只能上传 JPG/PNG 文件,且不超过 5MB</div></el-upload></div>
</template><script>
import { policy } from "@/api/oss"; // 封装的请求方法export default {data() {return {fileList: [],currentFile: {},policyData: {}};},methods: {handleRemove(file, fileList) {console.log('文件已移除:', file);},handlePreview(file) {console.log('预览文件:', file.url);},onSuccess() { /* 上传成功回调 */ },onBeforeUpload(file) {const isImage = ['image/jpeg', 'image/png'].includes(file.type);const isLt5M = file.size / 1024 / 1024 < 5;if (!isImage) {this.$message.error('仅支持 JPG/PNG 格式!');return false;}if (!isLt5M) {this.$message.error('文件大小不能超过 5MB!');return false;}this.currentFile = file;this.getPolicy(file); // 获取签名信息并上传return false; // 阻止 el-upload 默认上传},getPolicy(file) {policy({bizModuleName: "product",fileType: "image"}).then(res => {const data = res.data;const formData = new FormData();// 构建上传表单字段formData.append('key', data.dir);formData.append('policy', data.policy);formData.append('OSSAccessKeyId', data.accessid);formData.append('success_action_status', '200');formData.append('signature', data.signature);formData.append('file', file);// 直传 OSSfetch(data.host, {method: 'POST',body: formData}).then(() => {// 上传成功,获取预览链接return fetch(`/thirdpart/oss/getPreSignedUrl?objectName=${data.dir}`);}).then(resp => resp.json()).then(presignedRes => {this.fileList.push({url: presignedRes.data,name: file.name});this.$message.success('上传成功!');}).catch(err => {this.$message.error('上传失败');console.error("上传失败:", err);});}).catch(err => {this.$message.error('获取上传凭证失败');console.error("获取Policy失败:", err);});}}
}
</script>

💡 为什么说“前端直传不安全”是个伪命题?

很多开发者一听“前端上传”,第一反应就是:

“啊?那不是要把AccessKey写在前端?这不是白送黑客吗?”

——错!大错特错!

 误解1:必须把AK/SK放前端?

真相:你根本不需要把长期密钥暴露在前端!

阿里云OSS提供多种安全上传方式,最常用的是:

✅ 方式一:Post Policy(表单上传)
  • 后端用AK/SK生成一个带策略(Policy) 的签名(Signature)。
  • 签名中包含上传限制条件(如路径、大小、过期时间等)。
  • 前端只拿到这个签名,发起POST请求上传文件。
  • 密钥不暴露,安全性高
✅ 方式二:STS临时凭证(Security Token Service)
  • 后端调用STS服务,获取一个临时Token(含AccessKeyID、Secret、Token)。
  • 有效期可设为几分钟到几小时。
  • 前端用这个临时凭证上传,过期自动失效。
  • 即使泄露,影响极小

📌 所以,说“前端上传=密钥泄露”=完全不懂OSS机制!


误解2:前端能随意上传任意文件?

真相:有 Policy 在,前端说了不算!

Policy 是一段JSON,定义了严格的上传规则,例如:

{"expiration": "2025-12-31T12:00:00.000Z","conditions": [["eq", "$key", "uploads/2025/user123/avatar.jpg"],["content-length-range", 0, 5242880],["eq", "$x-oss-forbid-overwrite", "true"]]
}

这意味着:

  • 只能上传到指定路径 ✅
  • 文件大小不能超过5MB ✅
  • 不允许覆盖已有文件 ✅

👉 哪怕你拿到签名,也只能在这个“沙盒”里操作,超出范围直接被OSS拒绝!


❌ 误解3:一个签名能无限次使用?

真相:通过Policy可以实现“类一次性”效果!

比如你想让用户上传头像 avatar.jpg,你可以设置:

  • key 固定为 users/${userId}/avatar.jpg
  • x-oss-forbid-overwrite: true → 禁止覆盖
  • 过期时间5分钟

结果就是:

  • 用户只能上传一次(第二次会因“禁止覆盖”失败)
  • 即使别人拿到签名,也只能上传这个文件一次
  • 5分钟后签名自动失效

🎯 这不就是“事实上的单次有效”

误解4:上传后端不知道?没法校验?

真相:OSS支持“上传回调(Callback)”!

你可以在上传请求中加入:

Callback: https://your-api.com/oss-callback
Callback-Body: filename=${object}&size=${size}&mimeType=${mimeType}

文件一上传成功,OSS就会自动调用你的接口,通知结果,无需前端再“汇报”。

🎯 场景:用户上传头像 → OSS回调你的服务 → 你异步下载并校验图片宽高 → 存入数据库 → 返回结果

http://www.dtcms.com/a/347978.html

相关文章:

  • 基于魔搭社区与阿里云百炼的云端 RAG 私有知识问答系统实现
  • GHOST 小巧方便的 WinXP !
  • 华为网路设备学习-30(BGP协议 五)Community、
  • 【重学MySQL】八十八、8.0版本核心新特性全解析
  • 质量管理与项目管理学习-初识1
  • ThinkPHP8学习篇(四):请求和响应
  • 基于FPGA的情绪感知系统设计方案:心理健康监测应用(一)
  • centos搭建gitlab服务器
  • 【R语言】R语言中 rbind() 与 merge() 的区别详解
  • 【企业标准开发框架 01】全局异常处理+自定义异常类
  • JAVA限流方法
  • System.IO.Pipelines 与“零拷贝”:在 .NET 打造高吞吐二进制 RPC
  • 【SpringBoot集成篇】SpringBoot 深度集成 Elasticsearch 搜索引擎指南
  • rust语言 (1.88) egui (0.32.1) 学习笔记(逐行注释)(十五)网格布局
  • rust语言 (1.88) egui (0.32.1) 学习笔记(逐行注释)(十三)菜单、右键菜单
  • 【JavaEE】了解synchronized
  • 大数据毕业设计选题推荐-基于大数据的丙型肝炎患者数据可视化分析系统-Hadoop-Spark-数据可视化-BigData
  • 【数据结构】从基础到实战:全面解析归并排序与计数排序
  • 基于stm32汽车雨刮器控制系统设计
  • Java基础第3天总结(面向对象)
  • Shell Case 条件语句详解
  • EP01:【DA】数据分析的概述
  • 01Shell脚本入门:基础命令与变量解析
  • JVM之【类加载系统】
  • 【Qt开发】常用控件(六)
  • Golang云端编程深度指南:架构本质与高阶实践
  • Flink Slot 不足导致任务Pending修复方案
  • 互联网大厂Java面试实录:从Spring到微服务的全面考察
  • 【软件安全】ARM64、x86、32 位与 64 位架构的区别、定义、应用背景
  • 个人搭建小网站教程(云服务器Ubuntu版本)