赞
踩
最近公司的技术负责人让我整合下 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)作为其唯一标识。
至少需要了解如下几个概念:
OSS 产品文档 中有详细的说明,我就不多做赘述了。
购买 OSS 之后,登录阿里云账号,可在 产品与服务
中找到所购买的 对象存储 OSS
进入 对象存储 OSS
点击左侧菜单的 Bucket列表
,就可以新建 Bucket
了
注意:Bucket 新建之后不可修改
创建成功后可在 Bucket 列表
中看到所创建的 Bucket
点击该 Bucket
就可以查看其下的 文件列表
与上传文件了
上传文件完成后会在 文件列表
中展示,可点击详情查看文件下载的 URL
可以看到这个 url 的组成:https://bucket.endpoint/filePath
在网页上虽然可以做这些文件上传等操作,但是需要登录本人的阿里云账号是不安全的,其次是在开发中也不可能在这上面进行操作,都是通过 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用户
操作列的 添加权限
可以看到 选择权限
这里有很多条,针对控制访问 OSS
,只需要勾选 AliyunOSSFullAccess
这个权限,点击保存即可。
关于 RAM 用户授权
详细说明可参见:为 RAM 用户授权
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
其实在 OSS 产品文档 中也有比较详细的说明,我们只需要参照 开发参考
中内容基本上都能实现。
所以我只提供常用的一些常用的功能实现,如果以下内容无法实现你目前的需要,可参考 OSS 产品文档 进行代码编写会比较稳妥。
首先是需要安装 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 产品文档 中还有很多个案例,写得也是比较详细,我就不多做赘述了,只要先实现以上的功能,其它的都可以参照产品文档慢慢研究,比如说分片上传、进图条等等。
除了通过服务器代理上传文件的方式外,OSS 还提供了客户端直传的方式。
在典型的服务端和客户端架构下,常见的文件上传方式是服务端代理上传:客户端将文件上传到业务服务器,然后业务服务器将文件上传到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
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。