阿里云OSS对象存储服务 基本使用

295 阅读2分钟

需求

由于个人的云服务器带宽小,又刚好有上传多媒体文件(图片、音频、视频)的要求,所以决定尝试使用对象存储服务来存储文件。

准备工作

购买了阿里云的40G标准存储包半年

在OSS控制面板下创建新的Bucket

点击阿里云右上角用户面板下的访问控制,创建新的子用户,专门用于对象存储服务的管理,更安全

并为用户添加权限 AliyunOSSFullAccess | 管理对象存储服务(OSS)权限

在创建用户后记录用户的AccessKeyIdAccessKeySecret, 或者直接下载文件,不然之后这两个标识会隐藏不显示。

@EPS)YVOSV(R{0KW0UG`DH9.png

image-20221114233245217.png

配置跨域策略,暂时完全放开,方便测试

PIRKLZEEMR@3KD~FE[92]Z9.png

A1BNRQK8GF)D36MRB6%316M.png

访问策略

由于前端直传会暴露AccessKeyIdAccessKeySecret

而将文件传至云服务器再上传到OSS,因为云服务器带宽小,速度过慢

所以此处采用前端携带文件名访问后端接口,获得临时访问的经过签名的地址,前端再使用该地址上传文件,在上传完文件后,再向服务器确认,服务器此时在数据库插入数据行,并记录文件名

至于下载文件,也是前端携带文件名访问后端接口获得文件访问的临时下载路径

后端实现

UploadUrlRequest

@Data
/**
 * 用于接收文件名和文件类型
 */
public class UploadUrlRequest {
    String fileName;
    String contentType;
}

UploadUrlResponse

@Data
public class UploadUrlResponse {
    // 处理后的文件名
    private String fileName;
    // 临时签名地址
    private String tempUrl;
    // 处理后的需要填入的contentDisposition
    private String disposition;
}

OssService

public interface OssService {

    /**
     * 根据文件名和内容类型生成临时签名url
     * @param uploadUrlRequest
     * @return 临时签名地址,处理后的文件名,请求头
     */
    UploadUrlResponse getUploadTempUrl(
            UploadUrlRequest uploadUrlRequest);

    /**
     * 获得下载文件的临时地址
     * @param fileName 文件名
     * @return 返回临时地址
     */
    UploadUrlResponse getDownloadTempUrl(String fileName);
    
    /**
     * 判断文件是否存在
     * @param fileName 文件名
     * @return
     */
    Boolean isFileExist(String fileName);
    
    /**
     * 删除文件
     * @param fileName 文件名
     */
    void deleteFile(String fileName);
}

PutObject文档

OssServiceImpl

@Service
public class OssServiceImpl implements OssService {

    @Value("${oss.endPoint}")
    private String endPoint;
    @Value("${oss.accessKeyId}")
    private String accessKeyId;
    @Value("${oss.accessKeySecret}")
    private String accessKeySecret;
    @Value("${oss.bucketName}")
    private String bucketName;
    @Value("${oss.filePrefix}")
    /**
     * 上传的文件夹,即前缀
     */
    private String filePrefix;

    /**
     * 设置150秒过期
     */
    private final Long expires = 150L;

    @Bean
    private OSS getOssClient() {
        return new OSSClientBuilder().build(endPoint, accessKeyId, accessKeySecret);
    }

    @Override
    public UploadUrlResponse getUploadTempUrl(
            UploadUrlRequest uploadUrlRequest) {
        // 1.检查参数是否为空
        String rawFileName = uploadUrlRequest.getFileName();
        String contentType = uploadUrlRequest.getContentType();
        if (StringUtils.isAnyBlank(rawFileName, contentType))
            throw new BusinessException(ErrorCode.PARAMS_NULL_ERROR, "有必传参数为空");
        // 2.检查文件名是否含非法字符
        if (StringUtils.contains(rawFileName, '/'))
            throw new BusinessException(ErrorCode.PARAMS_ERROR, "文件名含/,非法");
        // 3.处理文件名
        // 为文件名添加时间戳前缀,防止重复
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String finFileName = simpleDateFormat.format(new Date()) + "-" + rawFileName;
        // 4.生成签名
        // 拼接文件夹名
        String objectKey = filePrefix + "/" + finFileName;
        GeneratePresignedUrlRequest signedRequest = new GeneratePresignedUrlRequest(bucketName, objectKey, HttpMethod.PUT);
        // 设置过期时间和内容类型
        Date expiration = new Date(new Date().getTime() + expires * 1000);
        // ! 注意,此处设置的contentType和返回临时地址后前端上传的文件类型必须一致,否则会报错
        signedRequest.setExpiration(expiration);
        signedRequest.setContentType(contentType);
        // 此处是为了设置上传文件PutObject接口的请求头 Content-Disposition
        // 见文档:如需确保下载名称中包含中文字符的Object到本地指定路径后,文件名称不出现乱码的现象,
        // 您需要将名称中包含的中文字符进行URL编码。
        String downloadFileName;
        try {
            // 对文件名进行编码处理
            int i = rawFileName.lastIndexOf(".");
            String after = rawFileName.substring(i);
            String front = rawFileName.substring(0, i);
            downloadFileName = URLEncoder.encode(front, "UTF-8") + after;
        } catch (UnsupportedEncodingException e) {
            throw new BusinessException(ErrorCode.INNER_ERROR, "文件名编码错误");
        }
        // 处理后需要填入的header value
        String disposition = "attachment;filename="
                + downloadFileName + ";filename*=UTF-8''" + downloadFileName;
        // 获得url返回
        URL signedUrl = getOssClient().generatePresignedUrl(signedRequest);
        // 设置各项返回值
        UploadUrlResponse uploadUrlResponse = new UploadUrlResponse();
        uploadUrlResponse.setTempUrl(signedUrl.toString());
        uploadUrlResponse.setFileName(finFileName);
        uploadUrlResponse.setDisposition(disposition);
        return uploadUrlResponse;
    }

    @Override
    public UploadUrlResponse getDownloadTempUrl(String fileName) {
        // 填写过期时间,处理文件名
        Date expiration = new Date(new Date().getTime() + expires * 1000);
        String finFileName = filePrefix + "/" + fileName;
        GeneratePresignedUrlRequest request = new GeneratePresignedUrlRequest(bucketName, finFileName, HttpMethod.GET);
        request.setExpiration(expiration);
        // 添加上请求头,设置为 打开链接直接下载文件
        Map<String, String> headers = new HashMap<String, String>();
        headers.put("content-disposition", "attachment");
        request.setHeaders(headers);
        // 生成用于下载文件的签名url
        URL url = getOssClient().generatePresignedUrl(request);
        UploadUrlResponse uploadUrlResponse = new UploadUrlResponse();
        uploadUrlResponse.setTempUrl(url.toString());
        return uploadUrlResponse;
    }

    @Override
    public Boolean isFileExist(String fileName) {
        String finFileName = filePrefix + "/" + fileName;
        return getOssClient().doesObjectExist(bucketName, finFileName);
    }

    @Override
    public void deleteFile(String fileName) {
        String finFileName = filePrefix + "/" + fileName;
        getOssClient().deleteObject(bucketName, finFileName);
    }

}

application.yml配置相关参数

oss:
  endPoint: xxx
  accessKeyId: xxx
  accessKeySecret : xxx
  bucketName: xxx
  filePrefix: xxx

OSSController

@RestController
@RequestMapping("/oss")
public class OSSController {
    @Resource
    private OssService ossService;

    /**
     * 输入文件名和编码,返回临时上传地址
     * 注意contentType必须一致
     *
     * @param uploadUrlRequest
     * @return
     */
    @PostMapping("/getUploadUrl")
    public BaseResponse<UploadUrlResponse> getUploadUrl(@RequestBody(required = false) UploadUrlRequest uploadUrlRequest) {
        // 检查参数
        if (uploadUrlRequest == null)
            throw new BusinessException(ErrorCode.PARAMS_NULL_ERROR);
        UploadUrlResponse uploadUrlResponse = ossService.getUploadTempUrl(uploadUrlRequest);
        return ResultUtils.success(uploadUrlResponse);
    }

    /**
     * 根据文件名
     * 获取临时get请求url
     */
    @PostMapping("/getDownloadUrl")
    public BaseResponse<UploadUrlResponse> getDownloadUrl(@RequestBody(required = false) UploadUrlRequest uploadUrlRequest) {
        // 检查参数
        String fileName = uploadUrlRequest.getFileName();
        if (StringUtils.isAnyBlank(fileName))
            throw new BusinessException(ErrorCode.PARAMS_NULL_ERROR);
        UploadUrlResponse downloadTempUrl = ossService.getDownloadTempUrl(fileName);
        return ResultUtils.success(downloadTempUrl);
    }
}

前端实现

Test.vue

<template>
  <div>
    <input type="file" @change="fileChange" ref="fileInput" />
  </div>
</template>

<script>
import axios from 'axios'
export default {
  name: 'Test',
  methods: {
    fileChange(e) {
      // 获得选择的文件
      const file = e.target.files?.[0]
      if (!file) return
      console.log(file)
      this.uploadFile(file)
    },
    async uploadFile(file) {
      // 获得文件信息
      const fileName = file.name
      const contentType = file.type
      // 请求临时地址
      const tempUrlParams = {
        fileName,
        contentType,
      }
      const tempUrlRes = await axios.post('/api/getUploadUrl', tempUrlParams)
      if (tempUrlRes.data.code != 0) {
        console.error(tempUrlRes)
        console.error('获得临时地址错误')
        return
      }
      console.log(tempUrlRes)
      const resData1 = tempUrlRes.data.data
      // 获得处理后的文件名,临时地址,请求头
      const finFileName = resData1.fileName
      const tempUrl = resData1.tempUrl
      const disposition = resData1.disposition
      const headers = {}
      headers['Content-Type'] = contentType
      // 设置下载时的文件名
      headers['Content-Disposition'] = disposition
      let uploadFileRes
      // 调用阿里云上传文件接口
      try {
        uploadFileRes = await axios.put(tempUrl, file, {
          headers,
          withCredentials: false,
        })
      } catch (err) {
        console.error(err)
        console.error('阿里云上传文件错误')
      }
      console.log(uploadFileRes)
      // 向服务器正式发送插入请求
      /**
       * 后端接口
       */
      // 清除文件
      this.$refs.fileInput.value = ''
    },
  },
  created() {},
}
</script>

测试

随便上传一个文件

可见第一次前端请求服务器返回了临时地址,经过处理的文件名和经过编码的请求头value 第二次前端使用临时地址上传文件成功

_4SFO(ZFEY5KTFIRTRH7M]B.png

再测试下载文件功能 直接在postman中测试,填入刚才经过服务器处理的文件名

%P(L~87XV0E7GPXRO9NPI.png

将链接在浏览器中打开,该文件以原来的中文文件名下载成功

FY9U0@A50E~0J21)MPY(`KR.png

至此,阿里云对象存储服务的基础功能使用结束