OSS对象存储

背景

实际开发中,图片等资源一般不会直接存储在数据库,而是单独存储后在数据库中存储资源url.

Spring Boot上传图片有两种方式:本地存储和远程对象存储,远程对象存储可以使用现有的服务(eg.OSS)或自己搭建服务器(eg.MinIO),这里主要讲OSS存储。

1.本地存储:直接将图片上传到resources目录下

 @RestController
 public class uploadController{
     @PostMapping("/upload")
     public String upload(MultipartFile file){
         //对file做校验eg.图片格式 大小
         if(file.isEmpty())return "图片为空";
         //图片重命名,防止冲突
         String originalFilename = file.getOriginalFilename();//原来的图片名称
         String ext = "."+originalFilename.split("\\.")[1];
         String uuid = UUID.randomUUID().toString.replace("-","");
         String fileName = uuid+ext;
         //上传图片到resources目录
         ApplicationHome applicationHome = new ApplicationHome(this.getClass);
         String pre = applicationHome.getDir().getParentFile().getParentFile().getAbsolutePath()+"\\src\\main\\resources\\static\\images";
         String path = pre+fileName;
         try{
             file.transferTo(new File(path));
             return path;
        }catch(IOException e){
             e.printStackTrace();
        }
         return "图片上传失败";
    }
 }

2.OSS对象存储

OSS对象存储是阿里云提供的服务,如何申请/免费试用阿里云OSS不在这里详述,网上教程很多。CloudFlare R2的Bucket和阿里云OSS存储桶是一样的,有其中一个就行。

阿里云OSS存储文档

阿里云提供了几种对象存储模式:

  • 服务器直传
  • 服务器签名前端直传
  • 服务器签名直传并设置上传回调

2.1 服务器直传

如图,用户先把文件上传给应用服务器,应用服务器再把文件上传到OSS.

image-20230903221625892

简单案例:

1.pom文件添加依赖 <dependency>
  <groupId>com.aliyun.oss</groupId>
     <artifactId>aliyun-sdk-oss</artifactId>
     <version>3.15.0</version>
 </dependency>
 <dependency>
  <groupId>commons-io</groupId>
     <artifactId>commons-io</artifactId>
     <version>2.4</version>
 </dependency>
 <dependency>
  <groupId>commons-beanutils</groupId>
     <artifactId>commons-beanutils</artifactId>
     <version>1.9.3</version>
 </dependency>

2.为了方便各模块调用,可以直接写成工具类

image-20230903161028811
3.在controller中调用 @RestController
 public class uploadController{
     @PostMapping("/upload")
     public String upload(MultipartFile file){
         try{
             return UploadUtil.uploadImage(file);
        }catch(IOException e){
             e.printStackTrace();
             return "图片上传失败";
        }
    }
 }

缺点:

  • 上传慢:用户数据需要先上传到应用服务器,再上传到OSS
  • 扩展性差:若后续用户增多,应用服务器会成为瓶颈
  • 费用高:需要准备堕胎应用服务器。由于OSS上传流量是免费的,若数据直传到OSS,不通过应用服务器,可以节省大量费用。

2.2 服务端签名后前端直传

为什么需要服务器签名?

服务器签名是对用户上传数据的一种管制,只有通过服务器认证的有对应权限的用户才可以进行OSS上传操作,不能让用户随意上传。

image-20230904110735600

具体签名步骤在后面2.4给出示例。

2.3 上传回调

某些业务场景下,当用户上传成功时,需要接收对应的结果信息,例如文件存储地址、文件名称等内容,这些信息可以通过上传回调完成。应用服务器向OSS返回什么结果,OSS就像客户端返回什么结果。

oss

代码示例直接看2.4

2.4 服务器签名直传并设置上传回调

简单示例

1.分析:

上传图片操作中客户端需要发起两次请求,1.第一次请求应用服务器,获取上传图片所需参数;2.第二次直接向OSS发送请求,上传图片;3.而OSS上传图片成功时,需要向应用服务器发送回调请求。

过程1:需要应用服务器提供一个接口,返回上传图片所需参数;

过程2:客户端需要知道OSS请求地址,这个请求地址从过程1的返回结果中取得;同时OSS需要知道回调请求的请求地址和请求参数,以便访问应用服务器;

过程3:应用服务器提供回调接口,返回前端所需请求结果。

根据以上分析,可以写出如下代码:

2.步骤:
1.pom文件引入必要依赖 <!--阿里云OSS SDK-->
 <dependency>
     <groupId>com.aliyun.oss</groupId>
     <artifactId>aliyun-sdk-oss</artifactId>
     <version>3.15.1</version>
 </dependency>

2.application.yml配置oss相关信息

这里把上传成功回调地址圈起来了,因为这里不能写本地地址,具体原因后面会讲

image-20230904221958391

3.创建OSSClient客户端 

@Configuration
 public class OSSConfig {
 ​
  @Value("${aliyun.oss.endpoint}")
  private String ALIYUN_OSS_ENDPOINT;
 ​
  @Value("${aliyun.oss.accessKeyId}")
  private String ALIYUN_OSS_ACCESSKEYID;
 ​
  @Value("${aliyun.oss.accessKeySecret}")
  private String ALIYUN_OSS_ACCESSKEYSECRET;
 ​
  @Bean
  public OSS ossClient(){
  return new OSSClientBuilder().build(ALIYUN_OSS_ENDPOINT,ALIYUN_OSS_ACCESSKEYID,ALIYUN_OSS_ACCESSKEYSECRET);
  }
 }

4.创建相关实体类

过程1提到的需要应用服务器返回的”上传图片所需参数”:OssPolicyResult 

 @Data
 @EqualsAndHashCode
 public class OssPolicyResult {
 ​
  // 访问身份验证中用到用户标识
  private String accessKeyId;
 ​
  // 用户表单上传的策略,经过base64编码过的字符串
  private String policy;
 ​
  // 对policy签名后的字符串
  private String signature;
 ​
  // 上传文件夹路径前缀
  private String dir;
 ​
  // oss对外服务的访问域名
  private String host;
 ​
  // 上传成功后的回调设置
  private String callback;
 }

过程2提到的OSS必须知道回调函数相关参数:OssCallbackParam 

 @Data
 @EqualsAndHashCode
 public class OssCallbackParam {
 ​
  //请求的回调地址
  private String callbackUrl;
 ​
  //回调时传入request中的参数
  private String callbackBody;
 ​
  //回调时传入参数的格式,比如表单提交形式
  private String callbackBodyType;
 }

过程3提到的回调函数的返回结果:OssCallbackResult

 @Data
 @EqualsAndHashCode
 public class OssCallbackResult {
  // 文件名称
  private String filename;
  // 文件大小
  private String size;
  // 文件的mimeType
  private String mimeType;
  // 图片文件的宽
  private String width;
  // 图片文件的高
  private String height;
 }

5.编写接口和api

 public interface OssService {
  //返回上传图片所需参数
  OssPolicyResult policy();
  //回调函数
  OssCallbackResult callback(HttpServletRequest request);
 } @RestController
 @RequestMapping("/aliyun/oss")
 public class OssController {
  @Autowired
  private OssService ossService;
 ​
  //oss上传签名生成,由前端调用
  @GetMapping("/policy")
  public OssPolicyResult policy(){
  return ossService.policy();
  }
 ​
  //oss上传成功回调,由OSS回调
  @PostMapping("/callback")
  public OssCallbackResult callback(HttpServletRequest request){
  return ossService.callback(request);
  }
 }

6.编写接口实现类⭐

 @Service
 public class OssServiceImpl implements OssService {
 ​
 ​
  @Value("${aliyun.oss.policy.expire}")
  private int ALIYUN_OSS_POLICY_EXPIRE;
 ​
  @Value("${aliyun.oss.maxSize}")
  private int ALIYUN_OSS_MAX_SIZE;
 ​
  @Value("${aliyun.oss.callback}")
  private String ALIYUN_OSS_CALLBACK;
 ​
  @Value("${aliyun.oss.bucketName}")
  private String ALIYUN_OSS_BUCKET_NAME;
 ​
  @Value("${aliyun.oss.endpoint}")
  private String ALIYUN_OSS_ENDPOINT;
 ​
  @Value("${aliyun.oss.dir.prefix}")
  private String ALIYUN_OSS_DIR_PREFIX;
 ​
  @Value("${aliyun.oss.accessKeyId}")
  private String ALIYUN_OSS_ACCESS_KEYID;
 ​
  @Autowired
  OSS ossClient;
 ​
  /**
  * 由前端直接调用,返回OSS上传相关参数
  * @return
  */
  @Override
  public OssPolicyResult policy() {
  OssPolicyResult ossPolicyResult = new OssPolicyResult();
  //存储目录
  SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
  String dir = ALIYUN_OSS_DIR_PREFIX + sdf.format(new Date());
  //签名有效期
  long expireEndTime = System.currentTimeMillis() + ALIYUN_OSS_POLICY_EXPIRE* 1000L;
  Date expiration = new Date(expireEndTime);
  //文件大小
  long maxSize = ALIYUN_OSS_MAX_SIZE * 1024 *1024L;
  //回调函数的参数
  OssCallbackParam callbackParam = new OssCallbackParam();
  callbackParam.setCallbackUrl(ALIYUN_OSS_CALLBACK);
  callbackParam.setCallbackBody("filename=${object}&size=${size}&mimeType=${mimeType}&height=${imageInfo.height}&width=${imageInfo.width}");
  callbackParam.setCallbackBodyType("application/x-www-form-urlencoded");
  //提交节点
  String action = "http://"+ALIYUN_OSS_BUCKET_NAME+"."+ALIYUN_OSS_ENDPOINT;
 ​
  PolicyConditions policyConditions = new PolicyConditions();
  policyConditions.addConditionItem(PolicyConditions.COND_CONTENT_LENGTH_RANGE, 0, maxSize);
  policyConditions.addConditionItem(MatchMode.StartWith, PolicyConditions.COND_KEY, dir);
 ​
  String postPolicy = ossClient.generatePostPolicy(expiration, policyConditions);
  byte[] binaryData = postPolicy.getBytes(StandardCharsets.UTF_8);
  String policy = BinaryUtil.toBase64String(binaryData);
  String signature = ossClient.calculatePostSignature(postPolicy);
  String callbackData = BinaryUtil.toBase64String(JSONUtil.parse(callbackParam).toString().getBytes(StandardCharsets.UTF_8));
 ​
  //返回结果
  ossPolicyResult.setAccessKeyId(ALIYUN_OSS_ACCESS_KEYID);
  ossPolicyResult.setPolicy(policy);
  ossPolicyResult.setSignature(signature);
  ossPolicyResult.setDir(dir);
  ossPolicyResult.setHost(action);
  ossPolicyResult.setCallback(callbackData);
 ​
  return ossPolicyResult;
  }
 ​
  /**
  * 由OSS存储成功时调用的回调函数
  * @param request
  * @return
  */
  @Override
  public OssCallbackResult callback(HttpServletRequest request) {
  OssCallbackResult ossCallbackResult = new OssCallbackResult();
  String filename = request.getParameter("filename");
  filename = "http://".concat(ALIYUN_OSS_BUCKET_NAME).concat(".").concat(ALIYUN_OSS_ENDPOINT).concat("/").concat(filename);
  ossCallbackResult.setFilename(filename);
  ossCallbackResult.setSize(request.getParameter("size"));
  ossCallbackResult.setMimeType(request.getParameter("mimeType"));
  ossCallbackResult.setWidth(request.getParameter("width"));
  ossCallbackResult.setHeight(request.getParameter("height"));
  return ossCallbackResult;
  }
 ​
 }

Q:OssCallbackParamHttpServletRequest的关系?

A:

OssCallbackParam:

OssCallbackParam 是你配置给OSS的参数对象,用于告诉OSS在文件上传成功后如何回调应用服务器。

  • callbackUrl:OSS回调服务器的URL.
  • callbackBody:回调时传给服务器的参数.
  • callbackBodyType:回调参数的格式(如application/json).

HttpServletRequest:

HttpServletRequest 是Web框架提供的,当OSS回调应用服务器时,这个HTTP请求会被你的Web框架(例如Spring Boot)捕获,并转化为一个HttpServletRequest对象。这个对象包含了HTTP请求的所有信息,例如HTTP头、请求参数等。包含的参数来源于callbackBody的参数。

3.结果

请求应用服务器获取OSS所需参数

image-20230904223817103

请求OSS上传图片

image-20230904223945578

若未添加callback参数,则会返回204 No Content,这是OSS上传成功的默认返回,如果想要有具体信息,就需要加上callback参数。

加上callback后请求会报错:

 Private address is forbidden to callback.

这就是前面提到的callback地址问题,callback地址必须是公网能够访问到的地址,阿里云OSS是不能通过 localhost找到本地计算机上的callback接口的。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注