阿里云OSS 前端直传实现
·
开通阿里云OSS直联的操作
https://help.aliyun.com/zh/oss/use-cases/set-up-direct-data-transfer-for-mobile-apps?spm=5176.21213303.J_v8LsmxMG6alneH-O7TCPa.5.27dc2f3drB1hZN&scm=20140722.S_help@@%E6%96%87%E6%A1%A3@@31920._.ID_help@@%E6%96%87%E6%A1%A3@@31920-RL_osssts-LOC_2024SPAllResult-OR_ser-PAR1_213e375817447879507666560ebdb4-V_4-RE_new3-P0_1-P1_0
JAVA包依赖
<!-- 阿里云STS -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>sts20150401</artifactId>
<version>1.1.7</version>
</dependency>
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.18.1</version>
</dependency>
STS签名生成代码:
package com.vatontech.controller;
import com.aliyun.oss.common.utils.BinaryUtil;
import cn.dev33.satoken.annotation.SaIgnore;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import cn.hutool.json.JSONUtil;
import com.vatontech.vo.AliyunStsTokenVo;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.constant.GlobalConstants;
import org.dromara.common.core.domain.R;
import org.dromara.common.oss.factory.OssFactory;
import org.dromara.common.oss.properties.OssProperties;
import org.dromara.common.redis.utils.RedisUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.Duration;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/aliyun")
public class AliyunController {
@Value("${aliyun.sts.endpoint:sts.cn-shenzhen.aliyuncs.com}")
private String stsEndpoint;
@Value("${aliyun.sts.roleArn:acs:ram::<xx>:role/oss-web-upload}")
private String stsRoleArn;
@Value("${aliyun.sts.roleSessionName:oss-web-upload}")
private String roleSessionName;
@Value("${aliyun.sts.region:cn-shenzhen}")
private String region;
/**
* 获取阿里云 sts 临时TOKEN
* @return STS临时凭证
*/
@SaIgnore
@RequestMapping("/token")
public R<AliyunStsTokenVo> getToken() {
String aliyunStsCfg = RedisUtils.getCacheObject(GlobalConstants.GLOBAL_REDIS_KEY+"aliyun:sts");
if(Objects.nonNull(aliyunStsCfg)){
log.info("hit aliyunStsCfg cache.{}", aliyunStsCfg);
AliyunStsTokenVo token = JSONUtil.toBean(aliyunStsCfg,AliyunStsTokenVo.class);
token.setDir(DateUtil.format(new Date(), "yyyy/MM/dd"));
return R.ok(token);
}
try {
// 获取当前激活的OSS配置
OssProperties ossConfig = OssFactory.instance().getProperties();
com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config()
// 必填,请确保代码运行环境设置了环境变量 ALIBABA_CLOUD_ACCESS_KEY_ID。
.setAccessKeyId(ossConfig.getAccessKey())
// 必填,请确保代码运行环境设置了环境变量 ALIBABA_CLOUD_ACCESS_KEY_SECRET。
.setAccessKeySecret(ossConfig.getSecretKey());
// Endpoint 请参考 https://api.aliyun.com/product/Sts
config.endpoint = "sts.cn-shenzhen.aliyuncs.com";
com.aliyun.sts20150401.Client client = new com.aliyun.sts20150401.Client(config);
com.aliyun.sts20150401.models.AssumeRoleRequest assumeRoleRequest = new com.aliyun.sts20150401.models.AssumeRoleRequest()
.setRoleArn("acs:ram::<XX>:role/oss-web-upload")
.setRoleSessionName("oss-web-upload");
com.aliyun.teautil.models.RuntimeOptions runtime = new com.aliyun.teautil.models.RuntimeOptions();
com.aliyun.sts20150401.models.AssumeRoleResponse resp = client.assumeRoleWithOptions(assumeRoleRequest, runtime);
log.info("sts.response:{}",JSONUtil.toJsonPrettyStr(resp));
// 构建返回对象
AliyunStsTokenVo token = new AliyunStsTokenVo();
token.setAccessKeyId(resp.getBody().getCredentials().getAccessKeyId());
token.setAccessKeySecret(resp.getBody().getCredentials().getAccessKeySecret());
token.setSecurityToken(resp.getBody().getCredentials().getSecurityToken());
token.setExpiration(resp.getBody().getCredentials().getExpiration());
token.setBucket(ossConfig.getBucketName());
token.setRegion("oss-%s".formatted(region));
token.setDir(DateUtil.format(new Date(), "yyyy/MM/dd"));
// 设置过期时间(秒)
DateTime expiration = DateUtil.parse(resp.getBody().getCredentials().getExpiration());//2025-04-16T09:59:21Z
expiration = DateUtil.offsetSecond(expiration, -60);//提前2分钟过期
//设置缓存
long expireMillis =expiration.toJdkDate().getTime()-System.currentTimeMillis();
RedisUtils.setCacheObject(GlobalConstants.GLOBAL_REDIS_KEY+"aliyun:sts", JSONUtil.toJsonStr(token),Duration.ofMillis(expireMillis));
log.info("获取临时凭证成功:{}",JSONUtil.toJsonPrettyStr(token));
return R.ok("获取临时凭证成功", token);
} catch (Exception e) {
log.error("获取阿里云STS临时TOKEN失败", e);
return R.fail("获取临时凭证失败: " + e.getMessage());
}
}
}
前端上传代码
# 前端依赖 pnpm install ali-oss
import OSS from 'ali-oss'
import request from '@/utils/request';
//eyJjYWxsYmFja0JvZHlUeXBlIjoiYXBwbGljYXRpb24vanNvbiIsImNhbGxiYWNrSG9zdCI6Im9zcy1jbi1zaGVuemhlbi5hbGl5dW5jcy5jb20iLCJjYWxsYmFja1VybCI6Imh0dHA6Ly84LjEzMC4zNi4xNzI6MzAwMC9hcGkvY2FsbGJhY2siLCJjYWxsYmFja0JvZHkiOiJ7XCJidWNrZXRcIjpcIiR7YnVja2V0fVwiLFwibmFtZVwiOlwiJHtvYmplY3R9XCIsXCJzaXplXCI6JHtzaXplfSxcIm1pbWVUeXBlXCI6XCIke21pbWVUeXBlfVwiLFwid2lkdGhcIjoke2ltYWdlSW5mby53aWR0aH0sXCJoZWlnaHRcIjoke2ltYWdlSW5mby5oZWlnaHR9fSJ9
let callback = {
// 设置回调请求的服务器地址,例如http://oss-demo.aliyuncs.com:23450。
url: "http://IP:3000/api/callback",
//(可选)设置回调请求消息头中Host的值,即您的服务器配置Host的值。
host: 'IP',
// 设置发起回调请求的Content-Type。
contentType: "application/json",
// 设置发起回调时请求body的值。
body: "{\"bucket\":${bucket},\"name\":${object},\"size\":${size},\"mimeType\":${mimeType},\"width\":${imageInfo.width},\"height\":${imageInfo.height},\"object\":${object},\"reqId\":${reqId},\"dataId\":${x:dataId},\"classicId\":${x:classicId}}",
// 客户端发起回调请求时,OSS是否向通过callbackUrl指定的回源地址发送服务器名称指示SNI(Server Name Indication)
callbackSNI: true,
// 设置自定义参数
customValue: {
dataId: "x1000",
classicId: "y-2000"
}
};
async function getStsToken() {
try {
const response = await request({
url: '/aliyun/token',
method: 'get'
});
console.log('原始STS Token响应:', response);
return response;
} catch (error) {
console.error('获取STS Token请求失败:', error);
throw new Error('获取STS Token失败: ' + ((error as any)?.message || '网络请求异常'));
}
}
// 从后端获取ststoken 并初始化ossclient,通过promise返回ossclient和stsToken
export async function initOssClient() {
try {
const res = await getStsToken();
// 检查接口响应数据格式
console.log('STS Token响应处理中:', JSON.stringify(res));
if (!res) {
console.error('STS Token响应为空');
throw new Error('获取上传凭证失败:服务器未返回数据');
}
// 提取data数据,适配不同的响应结构
let tokenData;
// 检查各种可能的响应结构
if (res && res.code === 200 && res.data) {
tokenData = res.data;
console.log('使用标准响应结构: code=200');
}
// 获取基本信息
const region = tokenData.region;
const bucket = tokenData.bucket;
if (!bucket) {
console.error('缺少bucket信息:', tokenData);
throw new Error('获取的上传凭证缺少bucket信息');
}
// 确保使用正确的endpoint,不含重复bucket
let endpoint;
let domain;
// 完全跳过后端提供的endpoint,而是直接根据region和bucket构建标准格式的endpoint
domain = `oss-${region}.aliyuncs.com`;
endpoint = `https://${domain}`;
console.log('使用构建的标准OSS格式endpoint:', endpoint);
console.log('使用的bucket:', bucket);
// 创建OSS客户端,使用更安全的配置方式
const client = new OSS({
bucket: bucket,
region: region,
accessKeyId: tokenData.accessKeyId,
accessKeySecret: tokenData.accessKeySecret,
stsToken: tokenData.securityToken,
secure: false,
timeout: 60000,
cname: false, // 禁用自定义域名
useFetch: false, // 避免使用fetch API,使用XMLHttpRequest更可靠
refreshSTSToken: async () => {
try {
// 向您搭建的STS服务获取临时访问凭证。
const refreshRes = await getStsToken();
if (refreshRes && res.code === 200 && res.data) {
tokenData = res.data;
console.log('使用标准响应结构: code=200');
}
console.log('刷新STS Token响应:', JSON.stringify(refreshRes));
// 提取刷新后的token数据
let refreshData=refreshRes.data;
if (!refreshData.securityToken) {
throw new Error('刷新STS Token失败:缺少securityToken');
}
return {
accessKeyId: refreshData.accessKeyId,
accessKeySecret: refreshData.accessKeySecret,
stsToken: refreshData.securityToken,
}
} catch (error) {
console.error('刷新STS Token失败:', error);
throw error;
}
},
// 刷新临时访问凭证的时间间隔,单位为毫秒。
refreshSTSTokenInterval: 3600 * 1000
});
console.log('OSS客户端初始化成功:');
console.log('- bucket:', client.options.bucket);
console.log('- endpoint:', client.options.endpoint);
console.log('- 完整domain:', `${bucket}.${domain}`);
return {
client: client,
dir: tokenData.dir || '',
bucket: bucket,
region: region,
domain: domain
}
} catch (error) {
console.error('初始化OSS客户端失败:', error);
throw error;
}
}
/**
* 构建正确的OSS对象URL
* @param {string} bucket Bucket名称
* @param {string} region 区域
* @param {string} objectName 对象名称
* @returns {string} 完整的OSS URL
*/
export function buildOssUrl(bucket: string, region: string, objectName: string): string {
// 使用标准格式构建OSS URL,确保不会出现重复bucket
return `http://${bucket}.oss-${region}.aliyuncs.com/${objectName}`;
}
/**
* 普通上传
* @param {Object} client OSS客户端
* @param {Object} file 上传的文件
* @param {Object} dir 上传目录
*/
export async function upload(client: any, file: File, dir: string): Promise<string> {
try {
// 确保dir以/结尾
const uploadDir = dir.endsWith('/') ? dir : dir + '/';
// 构造完整的文件路径
const objectName = uploadDir + file.name;
console.log('上传文件到路径:', objectName);
console.log('上传文件到路径-callback:', callback);
// 使用multipart模式上传,比较稳定 callback:callback,
const options = {
callback:callback,
mime: file.type,
partSize: 500 * 1024, // 分片大小500KB
timeout: 120000 // 超时时间2分钟
};
const result = await client.multipartUpload(objectName, file, options);
console.log('上传结果:', result);
// 直接构建标准格式的OSS URL
const config = client.options;
const bucket = config.bucket;
const region = config.region;
// 使用安全的URL构建方法
const ossUrl = buildOssUrl(bucket, region, objectName);
console.log('构建的标准OSS URL:', ossUrl);
return ossUrl;
} catch (error) {
console.error('文件上传失败:', error);
throw error;
}
}
/**
* 分片上传
* @param {Object} client oss客户端
* @param {Object} file 上传的文件
* @param {Object} dir 上传oss的文件夹
* @param {Object} progressCallback 分片进度回调
*/
export async function partUpload(client: any, file: File, dir: string, progressCallback?: (p: number) => void): Promise<string> {
try {
// 确保dir以/结尾
const uploadDir = dir.endsWith('/') ? dir : dir + '/';
// 构造完整的文件路径
const objectName = uploadDir + file.name;
console.log('分片上传文件到路径:', objectName);
let options: any = {
// 获取分片上传进度、断点和返回值。
progress: progressCallback,
callback:callback,
// 设置并发上传的分片数量。
parallel: 4,
// 设置分片大小。默认值为1 MB,最小值为100 KB。
partSize: 500 * 1024,
mime: file.type,
timeout: 120000 // 超时时间2分钟
}
const result = await client.multipartUpload(objectName, file, options);
console.log('分片上传结果:', result);
// 直接构建标准格式的OSS URL
const config = client.options;
const bucket = config.bucket;
const region = config.region;
// 使用安全的URL构建方法
const ossUrl = buildOssUrl(bucket, region, objectName);
console.log('构建的标准OSS URL:', ossUrl);
return ossUrl;
} catch (error) {
console.error('分片上传失败:', error);
throw error;
}
}
/**
* 断点续传
* @param {Object} client oss客户端
* @param {Object} file 上传的文件
* @param {Object} dir oss上传的文件夹
* @param {Object} progressCallback 分片回调进度
* @param {Object} checkPoint 断点
*/
export async function resumeUpload(client: any, file: File, dir: string, progressCallback?: (p: number) => void, checkPoint?: any): Promise<string> {
try {
// 确保dir以/结尾
const uploadDir = dir.endsWith('/') ? dir : dir + '/';
// 构造完整的文件路径
const objectName = uploadDir + file.name;
console.log('断点续传文件到路径:', objectName);
let options: any = {
// 获取分片上传进度、断点和返回值。
progress: progressCallback,
callback: callback,
// 断点
checkpoint: checkPoint,
timeout: 120000 // 超时时间2分钟
}
const result = await client.multipartUpload(objectName, file, options);
console.log('断点续传结果:', result);
// 直接构建标准格式的OSS URL
const config = client.options;
const bucket = config.bucket;
const region = config.region;
// 使用安全的URL构建方法
const ossUrl = buildOssUrl(bucket, region, objectName);
console.log('构建的标准OSS URL:', ossUrl);
return ossUrl;
} catch (error) {
console.error('断点续传失败:', error);
throw error;
}
}
/**
* 结束上传并删除碎片
* @param {Object} client OSS客户端
* @param {Object} uploadId 上传ID
* @param {Object} name 文件名
*/
export async function abortUpload(client: any, uploadId: string, name: string): Promise<void> {
try {
await client.abortMultipartUpload(name, uploadId);
console.log('成功中止上传并清理分片:', name, uploadId);
} catch (error) {
console.error('中止上传失败:', error);
throw error;
}
}
上传回调
package com.example.controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@RequestMapping("/api")
@RestController
public class ApiController {
@RequestMapping("/callback")
public String callBack(@RequestBody String body) {
log.info("revice.body:{}",body);
return "good";
}
}
其它的参照
https://help.aliyun.com/zh/oss/developer-reference/callback?spm=a2c4g.11186623.0.0.12107efdT1Kikx#reference-zkm-311-hgb https://www.alibabacloud.com/help/zh/oss/developer-reference/callback#ddb90e94e22g3
魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐


所有评论(0)