开通阿里云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

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐