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

Nacos 动态配置管理
为了使系统更加灵活和可维护,我们可以利用 Nacos 进行动态配置管理。下面是一个简单的 OSS 配置类示例,它可以从 Nacos 的 YAML 文件中注入配置,并初始化 OSS 客户端。
java
@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)
java
@RestController
@RequestMapping("/oss")
public class ThirdFileController {
@Autowired
private 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/uuid
String 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~1GB
policyConds.addConditionItem(MatchMode.StartWith, PolicyConditions.COND_KEY, objectName); // 限制上传路径前缀
policyConds.addConditionItem(PolicyConditions.COND_CONTENT_TYPE, "image/*"); // 仅允许图片类型
// 生成 Post Policy
String 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
html
<template>
<div>
<el-upload
class="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);
// 直传 OSS
fetch(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,定义了严格的上传规则,例如:
java
{
"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)"!
你可以在上传请求中加入:
javascript
Callback: https://your-api.com/oss-callback
Callback-Body: filename=${object}&size=${size}&mimeType=${mimeType}
文件一上传成功,OSS就会自动调用你的接口,通知结果,无需前端再"汇报"。
🎯 场景:用户上传头像 → OSS回调你的服务 → 你异步下载并校验图片宽高 → 存入数据库 → 返回结果