文章目录
前言
最近公司的技术负责人让我整合下 OSS 到项目中,所以花了一点时间研究了下 OSS
,虽然说在 OSS 的官方文档中有如何整合 OSS 的详细说明,但是不得不说文档实在是太详细了,如果仅仅是通过看官方文档去整合,可能会看到太多暂时用不上的内容,所以我简化下文档中的内容,也是谨防日后忘记,故此作为分享。
一、OSS 简介
阿里云对象存储 OSS(Object Storage Service)是一款海量、安全、低成本、高可靠的云存储服务,提供最高可达 99.995 % 的服务可用性。多种存储类型供选择,全面优化存储成本。
可以在阿里云的产品列表中找到
如果只是想玩一玩,做技术扩展用的,可以买一个商品类型为 OSS 资源包
的,价格很便宜
对于整合这些第三方的技术,最重要的就是学会去看这些第三方提供的文档
约莫看一下,大概就知道这些技术是什么?怎么用的
对象存储 OSS 产品文档:https://help.aliyun.com/zh/oss/
从产品文档中可以看到 OSS 的工作原理:
数据以对象(Object)的形式存储在 OSS 的存储空间(Bucket )中。如果要使用 OSS 存储数据,需要先创建 Bucket
,并指定Bucket的地域、访问权限、存储类型等属性
。创建 Bucket 后,您可以将数据以 Object 的形式上传到 Bucket,并指定 Object 的文件名(Key)作为其唯一标识。
至少需要了解如下几个概念:
- 存储空间(Bucket):存储空间是用户用于存储对象(Object)的容器,所有的对象都必须隶属于某个存储空间。存储空间具有各种配置属性,包括地域、访问权限、存储类型等。用户可以根据实际需求,创建不同类型的存储空间来存储不同的数据。
- 对象(Object):对象是 OSS 存储数据的基本单元,也被称为 OSS 的文件。和传统的文件系统不同,对象没有文件目录层级结构的关系。对象由元信息(Object Meta)、用户数据(Data)和文件名(Key)组成,并且由存储空间内部唯一的 Key 来标识。
- Region(地域):Region表示OSS的数据中心所在物理位置。用户可以根据费用、请求来源等选择合适的地域创建 Bucket。一般来说,距离用户更近的Region访问速度更快。Region 是在创建 Bucket 的时候指定的。Bucket 创建成功后,Region 将无法修改。
- Endpoint(访问域名):Endpoint 表示 OSS 对外服务的访问域名。
- AccessKey(访问密钥):AccessKey 简称 AK,指的是访问身份验证中用到的 AccessKey ID 和 AccessKey Secret。OSS 通过使用 AccessKey ID 和 AccessKey Secret 对称加密的方法来验证某个请求的发送者身份。AccessKey ID 用于标识用户;AccessKey Secret 是用户用于加密签名字符串和 OSS 用来验证签名字符串的密钥,必须保密。
OSS 产品文档 中有详细的说明,我就不多做赘述了。
二、OSS 的使用
1. Bucket 的创建与文件上传
购买 OSS 之后,登录阿里云账号,可在 产品与服务
中找到所购买的 对象存储 OSS
进入 对象存储 OSS
点击左侧菜单的 Bucket列表
,就可以新建 Bucket
了
注意:Bucket 新建之后不可修改
创建成功后可在 Bucket 列表
中看到所创建的 Bucket
点击该 Bucket
就可以查看其下的 文件列表
与上传文件了
上传文件完成后会在 文件列表
中展示,可点击详情查看文件下载的 URL
可以看到这个 url 的组成:https://bucket.endpoint/filePath
2. 创建 RAM 与用户授权
在网页上虽然可以做这些文件上传等操作,但是需要登录本人的阿里云账号是不安全的,其次是在开发中也不可能在这上面进行操作,都是通过 API
进行文件上传下载等操作。
从上面的 OSS 介绍可知,OSS 的文件是存储在 Bucket
中,如果想要通过程序长期访问 Bucket
下的指定资源,就需要创建 RAM 用户(可以不登录阿里云主账号就能使用指定权限的功能),从而获取 AccessKeyId
和 AccessKeySecret
作为访问 OSS
的凭证
。
关于 RAM 的详细作用可参见 访问控制-RAM用户概览
(1)创建 RAM 用户
- ① 登录阿里云,找到
AccessKey 管理
,点击进入 RAM 控制台,在左侧导航栏,选择身份管理
>用户
- ② 点击
创建用户
,输入登录名称
和显示名称
,访问方式勾选OpenAPI 调用访问
进行验证之后
就能获得 AccessKey ID
和 AccessKey Secret
,这个是使用 API
连接 OSS
需要用到的,一定要及时保存 AccessKey 的信息,页面关闭后将无法再次获取信息
。
(2)RAM 用户分配权限
- ① 进入
RAM 控制台
,在左侧导航栏,选择身份管理
>用户
,可以看到所有创建好的RAM
用户 - ② 在用户页面,单击
目标RAM用户
操作列的添加权限
- ③ 在添加权限面板,为 RAM 用户添加权限
可以看到 选择权限
这里有很多条,针对控制访问 OSS
,只需要勾选 AliyunOSSFullAccess
这个权限,点击保存即可。
关于 RAM 用户授权
详细说明可参见:为 RAM 用户授权
3. 图形化管理工具-ossbrowser
ossbrowser 是阿里云官方提供的 OSS
图形化管理工具,提供类似 Windows
资源管理器的功能。使用 ossbrowser
,您可以快速完成存储空间(Bucket)和文件(Object)的相关操作。
下载:
官方下载地址:https://help.aliyun.com/zh/oss/developer-reference/install-and-log-on-to-ossbrowser
这里以 windows 64
为例,则下载 Windows 64
下的 oss-browser-win32-x64.zip
压缩包
安装与使用:
下载完成之后,进行解压,在解压后的 oss-browser-win32-x64
文件夹下找到 oss-browser.exe
,双击即可打开
输入之前创建 RAM 用户
时所获取的 AccessKey
账号信息,并且该账户要有访问控制 OSS
的权限(AliyunOSSFullAccess),登入
在该软件上也可以进行文件上传下载等操作
我使用这个图形化管理工具的目的主要是为了测试 AccessKey
信息是否能正常连接访问 OSS
~~
三、Java 整合 OSS
关于如何使用 Java
整合 OSS
其实在 OSS 产品文档 中也有比较详细的说明,我们只需要参照 开发参考
中内容基本上都能实现。
所以我只提供常用的一些常用的功能实现,如果以下内容无法实现你目前的需要,可参考 OSS 产品文档 进行代码编写会比较稳妥。
1. 基本实现
首先是需要安装 OSS 的 Java SDK,在 Maven 工程中只需要在 pom.xml
中加入响应的依赖即可
(1)引入依赖
以 3.15.1
版本为例,在 <dependencies>
中加入如下内容:
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.15.1</version>
</dependency>
如果使用的是 Java 9
及以上的版本,则需要添加 jaxb
相关依赖。添加 jaxb
相关依赖示例代码如下:
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
<version>1.1.1</version>
</dependency>
<!-- no more than 2.3.3-->
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
<version>2.3.3</version>
</dependency>
(2)添加配置文件
想要通过程序的方式去连接 OSS
,那就必须告诉程序要连接哪个 OSS 端点
、哪个 Bucket
、并且告诉 OSS 你是谁,交出你的访问凭证
所以就需要配置 endpoint
、bucketName
以及 AccessKey Id
和 AccessKey Secret
信息,
endpoint
和 bucketName
可以在 Bucket
的详情页面获得
这部分内容一般会放在配置文件中,例如:
yml
文件配置
aliyun:
oss:
access-key-id: YOUR_ACCESS_KEY_ID
access-key-secret: YOUR_ACCESS_KEY_SECRET
endpoint: oss-cn-xxxxxx.aliyuncs.com
bucket-name: mike-system-file
配置类 OssProperty.java
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Data
@Component
@ConfigurationProperties("aliyun.oss")
public class OssProperty {
/**
* AccessKey ID
*/
private String accessKeyId;
/**
* AccessKey Secret
*/
private String accessKeySecret;
/**
* endpoint
*/
private String endpoint;
/**
* bucketName
*/
private String bucketName;
}
(3)代码编写
OssService.java
import com.aliyun.oss.model.Bucket;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
import java.util.Map;
public interface OssService {
public static final String HTTPS = "https://";
public static final String DOT = ".";
public static final String FORWARD_SLASH = "/";
/**
* 列举存储空间
*/
List<Bucket> showBuckets();
/**
* 创建存储空间
*/
void createBucket(String bucketName);
/**
* 删除储存空间
*/
void removeBucket(String bucketName);
/**
* 上传文件
* @param dir 存储空间某文件夹下,例如:app
* @param file 上传的文件
* @return 可访问的路径
*/
String upload(String dir, MultipartFile file);
/**
* 下载文件
* @param filePath 文件存储全路径,例如:dir/filename(不带 bucket 名称)
*/
void download(String filePath);
/**
* 删除文件
* @param filePath 文件存储全路径,例如:dir/filename(不带 bucket 名称)
*/
boolean remove(String filePath);
}
OssServiceImpl.java
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.OSSException;
import com.aliyun.oss.common.utils.BinaryUtil;
import com.aliyun.oss.internal.OSSHeaders;
import com.aliyun.oss.model.*;
import com.aliyuncs.exceptions.ClientException;
import com.fsy.common.core.exception.CustomException;
import com.fsy.common.core.utils.DateUtils;
import com.fsy.common.core.utils.ServletUtils;
import com.fsy.tool.config.OssProperty;
import com.fsy.tool.listener.OssProgressListener;
import com.fsy.tool.service.OssService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@Slf4j
@Service
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class OssServiceImpl implements OssService {
private final OssProperty ossProperty;
private final HttpServletResponse response;
/**
* 获取 OSSClient 实例
*/
private OSS getOssClient() {
String endpoint = ossProperty.getEndpoint();
return new OSSClientBuilder().build(endpoint, ossProperty.getAccessKeyId(), ossProperty.getAccessKeySecret());
}
/**
* 列举存储空间
*/
@Override
public List<Bucket> showBuckets() {
OSS ossClient = getOssClient();
try {
// 列举当前账号所有地域下的存储空间
return ossClient.listBuckets();
} catch (OSSException e) {
printlnException(e);
} finally {
if (ossClient != null) {
ossClient.shutdown();
}
}
return null;
}
/**
* 创建存储空间
*/
@Override
public void createBucket(String bucketName) {
OSS ossClient = getOssClient();
try {
// 创建CreateBucketRequest对象。
CreateBucketRequest createBucketRequest = new CreateBucketRequest(bucketName);
// 如果创建存储空间的同时需要指定存储类型、存储空间的读写权限、数据容灾类型, 请参考如下代码
// 此处以设置存储空间的存储类型为标准存储为例介绍
//createBucketRequest.setStorageClass(StorageClass.Standard);
// 数据容灾类型默认为本地冗余存储,即 DataRedundancyType.LRS。如果需要设置数据容灾类型为同城冗余存储,请设置为DataRedundancyType.ZRS
//createBucketRequest.setDataRedundancyType(DataRedundancyType.ZRS);
// 设置存储空间读写权限为公共读,默认为私有
//createBucketRequest.setCannedACL(CannedAccessControlList.PublicRead);
// 在支持资源组的地域创建Bucket时,您可以为Bucket配置资源组。
//createBucketRequest.setResourceGroupId(rsId);
// 创建存储空间
ossClient.createBucket(createBucketRequest);
} catch (OSSException e) {
printlnException(e);
} finally {
if (ossClient != null) {
ossClient.shutdown();
}
}
}
/**
* 删除储存空间
*/
@Override
public void removeBucket(String bucketName) {
OSS ossClient = getOssClient();
try {
// 删除存储空间
ossClient.deleteBucket(bucketName);
} catch (OSSException e) {
printlnException(e);
} finally {
if (ossClient != null) {
ossClient.shutdown();
}
}
}
/**
* 上传文件
* @param dir 存储空间某文件夹下,例如:app
* @param file 上传的文件
* @return 可访问的路径
*/
@Override
public String upload(String dir, MultipartFile file) {
// 获取文件名称
String sourceName = file.getOriginalFilename();
// 获取地域节点
String endpoint = ossProperty.getEndpoint();
// 获取存储空间名称
String bucketName = ossProperty.getBucketName();
// 当前日期
String ymd = DateUtils.parseDateToStr(DateUtils.YYMMDD, new Date());
// 文件存放地址(不带 bucket)
String filePath;
if (StringUtils.isNotBlank(dir)) {
// 例如:app/ymd/wms.apk
filePath = dir + FORWARD_SLASH + ymd + FORWARD_SLASH + sourceName;
} else {
filePath = ymd + FORWARD_SLASH + sourceName;
}
// 访问路径:https://bucket.endpoint/filePath
String urlPath = HTTPS + bucketName + DOT + endpoint + FORWARD_SLASH + filePath;
// 相对路径
String relativePath = FORWARD_SLASH + filePath;
OSS ossClient = getOssClient();
try {
// 判断 bucket 是否存在
if (!ossClient.doesBucketExist(bucketName)) {
throw new CustomException("存储空间不存在");
}
// 创建PutObjectRequest对象。
PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, filePath, file.getInputStream());
ObjectMetadata metadata = new ObjectMetadata();
/*
* 指定存储类型:
* 对于任意存储类型的Bucket,如果上传Object时指定此参数,则此次上传的Object将存储为指定的类型
* 取值:
* Standard:标准存储
* IA:低频访问
* Archive:归档存储
* ColdArchive:冷归档存储
* DeepColdArchive:深度冷归档存储
*/
// 设置存储类型:标准存储(默认标准存储)
metadata.setHeader(OSSHeaders.OSS_STORAGE_CLASS, StorageClass.Standard.toString());
/*
* 指定上传文件的访问权限:
* 取值:
* default(默认):Object遵循所在存储空间的访问权限
* private:私有
* public-read:公共读
* public-read-write:公共读写
*/
// 设置访问权限:默认(遵循所在存储空间的访问权限)
metadata.setObjectAcl(CannedAccessControlList.Default);
/*
* 指定上传文件操作时是否覆盖同名 Object:
* 不指定 x-oss-forbid-overwrite 时,默认覆盖同名 Object
* 指定 x-oss-forbid-overwrite 为 false 时,表示允许覆盖同名 Object
* 指定 x-oss-forbid-overwrite 为 true 时,表示禁止覆盖同名 Object,如果同名 Object 已存在,程序将报错
*/
// 设置禁止覆盖同名文件
metadata.setHeader("x-oss-forbid-overwrite", "false");
// 设置元数据
putObjectRequest.setMetadata(metadata);
// 上传文件
ossClient.putObject(putObjectRequest);
} catch (IOException e) {
log.error("failed to upload file.detail message:{}", e.getMessage());
} catch (OSSException e) {
printlnException(e);
} finally {
if (ossClient != null) {
ossClient.shutdown();
}
}
return urlPath;
}
/**
* 下载文件
* @param filePath 文件存储全路径,例如:dir/filename(不带 bucket 名称)
*/
@Override
public void download(String filePath) {
String bucketName = ossProperty.getBucketName();
OSS ossClient = getOssClient();
// 截取文件名称
String fileName = filePath.substring(filePath.lastIndexOf(FORWARD_SLASH));
try {
// 判断文件是否存在
if (!ossClient.doesObjectExist(bucketName, filePath)) {
throw new CustomException("文件不存在");
}
// ossObject 包含文件所在的存储空间名称、文件名称、文件元信息以及一个输入流
OSSObject ossObject = ossClient.getObject(new GetObjectRequest(bucketName, filePath));
InputStream inputStream = ossObject.getObjectContent();
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int num;
while ((num = inputStream.read(buffer)) != -1) {
byteArrayOutputStream.write(buffer, 0, num);
}
byteArrayOutputStream.flush();
byte[] bytes = byteArrayOutputStream.toByteArray();
byteArrayOutputStream.close();
// 读取,返回
ServletUtils.writeAttachment(response, fileName, bytes);
// ossObject 对象使用完毕后必须关闭,否则会造成连接泄漏,导致请求无连接可用,程序无法正常工作
ossObject.close();
} catch (OSSException oe) {
printlnException(oe);
} catch (Throwable ce) {
System.out.println("Caught an ClientException, which means the client encountered "
+ "a serious internal problem while trying to communicate with OSS, "
+ "such as not being able to access the network.");
System.out.println("Error Message:" + ce.getMessage());
} finally {
if (ossClient != null) {
ossClient.shutdown();
}
}
}
@Override
public boolean remove(String filePath) {
String bucketName = ossProperty.getBucketName();
OSS ossClient = getOssClient();
try {
// 判断文件是否存在
if (!ossClient.doesObjectExist(bucketName, filePath)) {
log.warn("need delete file:{} not exists", filePath);
return false;
}
// 删除文件或目录。如果要删除目录,目录必须为空
ossClient.deleteObject(bucketName, filePath);
return true;
} catch (OSSException oe) {
printlnException(oe);
} finally {
if (ossClient != null) {
ossClient.shutdown();
}
}
return false;
}
/**
* 返回附件
*/
public void writeAttachment(HttpServletResponse response, String filename, byte[] content) throws IOException {
// 设置 header 和 contentType
response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, "UTF-8"));
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
// 输出附件
IoUtil.write(response.getOutputStream(), false, content);
}
/**
* 打印异常日志
*/
public void printlnException(Exception e) {
if (e instanceof OSSException) {
OSSException oe = (OSSException) e;
System.out.println("Caught an OSSException, which means your request made it to OSS, "
+ "but was rejected with an error response for some reason.");
System.out.println("Error Message:" + oe.getErrorMessage());
System.out.println("Error Code:" + oe.getErrorCode());
System.out.println("Request ID:" + oe.getRequestId());
System.out.println("Host ID:" + oe.getHostId());
}
if (e instanceof ClientException) {
ClientException ce = (ClientException) e;
System.out.println("Caught an ClientException, which means the client encountered "
+ "a serious internal problem while trying to communicate with OSS, "
+ "such as not being able to access the network.");
System.out.println("Error Message:" + ce.getMessage());
}
}
}
(4)测试
上传文件
以上便是 Java 对 OSS
的简单整合全部内容。
OSS 产品文档 中还有很多个案例,写得也是比较详细,我就不多做赘述了,只要先实现以上的功能,其它的都可以参照产品文档慢慢研究,比如说分片上传、进图条等等。
2. 客户端直传
除了通过服务器代理上传文件的方式外,OSS 还提供了客户端直传的方式。
在典型的服务端和客户端架构下,常见的文件上传方式是服务端代理上传:客户端将文件上传到业务服务器,然后业务服务器将文件上传到OSS
。在这个过程中,一份数据需要在网络上传输两次,会造成网络资源的浪费、增大服务端的资源开销。为了解决这一问题,可以在客户端直连 OSS
来完成文件上传,无需经过业务服务器中转。
服务端代理上传和客户端直传相比,有以下三个缺点:
- 上传慢:用户数据先上传到应用服务器,之后再上传到 OSS,网络传输时间比直传到 OSS 多一倍,如果用户数据不通过应用服务器中转,而是直传到 OSS,速度将大大提升,而且 OSS 采用 BGP 宽带,能保证各地各运营商之间的传输速度
- 扩展性差:如果后续用户多了,应用服务器会成为瓶颈
- 费用高:需要准备多台应用服务器,用于 OSS 上传流量是免费的,如果数据直传到 OSS,不通过应用服务器,那么将能省下几台应用服务器
从服务端代理上传的案例可知,要想要上传图片,那就必须提供 endpoint
、bucket
和 AccessKey
的信息,但是在前端直接将这些信息写在 js
里面是非常不安全的,容易造成信息泄漏,遭受攻击
所以通常的做法就是前端先向后端发送上传文件的 Post Policy 请求,应用服务器返回签名给前端,前端再携带签名直接将文件上传至 OSS
后端可以参照 OSS 产品文档 来编写接口,提供签名
例如(在 基本实现 的案例上添加代码):
Controller
@GetMapping(value = "/signature")
@ApiOperation(value = "获取签名")
public ResponseBean signature(@ApiParam(required = true, value = "上传路径") @RequestParam(required = true) String dir) {
return ResponseBean.success(ossService.signature(dir));
}
Service
/**
* 服务端签名直传
* @param dir 设置上传到 OSS 的路径(目录)
* @return 签名信息
*/
Map<String, String> signature(String dir);
ServiceImpl
@Override
public Map<String, String> signature(String dir) {
String accessId = ossProperty.getAccessKeyId();
String endpoint = ossProperty.getEndpoint();
String bucket = ossProperty.getBucketName();
// Host 地址,格式为:https://bucket.endpoint
String host = HTTPS + bucket + DOT + endpoint;
// 设置上传回调 URL
String callbackUrl = "https://www.xxxx.xxx";
// 创建ossClient实例
OSS ossClient = getOssClient();
try {
long expireTime = 30;
long expireEndTime = System.currentTimeMillis() + expireTime * 1000;
Date expiration = new Date(expireEndTime);
PolicyConditions policyConditions = new PolicyConditions();
policyConditions.addConditionItem(PolicyConditions.COND_CONTENT_LENGTH_RANGE, 0, 1048576000);
policyConditions.addConditionItem(MatchMode.StartWith, PolicyConditions.COND_KEY, dir);
String postPolicy = ossClient.generatePostPolicy(expiration, policyConditions);
byte[] binaryData = postPolicy.getBytes(StandardCharsets.UTF_8);
String encodedPolicy = BinaryUtil.toBase64String(binaryData);
String postSignature = ossClient.calculatePostSignature(postPolicy);
Map<String, String> respMap = new LinkedHashMap<>();
respMap.put("access_id", accessId);
respMap.put("policy", encodedPolicy);
respMap.put("signature", postSignature);
respMap.put("dir", dir);
respMap.put("host", host);
respMap.put("expire", String.valueOf(expireEndTime / 1000));
/*
* 设置回调接口的一些相关参数
*
* JSONObject jasonCallback = new JSONObject();
* jasonCallback.put("callbackUrl", callbackUrl);
* jasonCallback.put("callbackBody",
* "filename=${object}&size=${size}&mimeType=${mimeType}&height=${imageInfo.height}&width=${imageInfo.width}");
* jasonCallback.put("callbackBodyType", "application/x-www-form-urlencoded");
* String base64CallbackBody = BinaryUtil.toBase64String(jasonCallback.toString().getBytes());
* respMap.put("callback", base64CallbackBody);
*/
return respMap;
} catch (Exception e) {
// Assert.fail(e.getMessage());
System.out.println(e.getMessage());
}
return null;
}
Body中的各字段说明如下:
字段 | 描述 |
---|---|
accessid | 用户请求的AccessKey ID |
host | 用户发送上传请求的域名 |
policy | 用户表单上传的策略(Policy),Policy为经过Base64编码过的字符串。详情请参见Post Policy |
signature | 对Policy签名后的字符串 |
expire | 由服务器端指定的Policy过期时间,格式为Unix时间戳(自UTC时间1970年01月01号开始的秒数) |
dir | 限制上传的文件前缀 |
测试:
后端就这样写就行了,
前端 vue + element-ui,OSS 上传文件代码如下:
...
未完待续...
参考博客
JAVA整合阿里云OSS/VUE上传阿里云OSS:https://blog.51cto.com/u_15899048/5903392