需求
由于个人的云服务器带宽小,又刚好有上传多媒体文件(图片、音频、视频)的要求,所以决定尝试使用对象存储服务来存储文件。
准备工作
购买了阿里云的40G标准存储包半年
在OSS控制面板下创建新的Bucket
点击阿里云右上角用户面板下的访问控制,创建新的子用户,专门用于对象存储服务的管理,更安全
并为用户添加权限 AliyunOSSFullAccess | 管理对象存储服务(OSS)权限
在创建用户后记录用户的AccessKeyId 和 AccessKeySecret, 或者直接下载文件,不然之后这两个标识会隐藏不显示。
配置跨域策略,暂时完全放开,方便测试
访问策略
由于前端直传会暴露AccessKeyId和AccessKeySecret
而将文件传至云服务器再上传到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);
}
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 第二次前端使用临时地址上传文件成功
再测试下载文件功能 直接在postman中测试,填入刚才经过服务器处理的文件名
将链接在浏览器中打开,该文件以原来的中文文件名下载成功
至此,阿里云对象存储服务的基础功能使用结束