背景
实际开发中,图片等资源一般不会直接存储在数据库,而是单独存储后在数据库中存储资源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存储桶是一样的,有其中一个就行。
阿里云提供了几种对象存储模式:
- 服务器直传
- 服务器签名前端直传
- 服务器签名直传并设置上传回调
2.1 服务器直传
如图,用户先把文件上传给应用服务器,应用服务器再把文件上传到OSS.

简单案例:
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.为了方便各模块调用,可以直接写成工具类

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上传操作,不能让用户随意上传。

具体签名步骤在后面2.4给出示例。
2.3 上传回调
某些业务场景下,当用户上传成功时,需要接收对应的结果信息,例如文件存储地址、文件名称等内容,这些信息可以通过上传回调完成。应用服务器向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相关信息
这里把上传成功回调地址圈起来了,因为这里不能写本地地址,具体原因后面会讲

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:OssCallbackParam
和HttpServletRequest
的关系?
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所需参数

请求OSS上传图片

若未添加callback参数,则会返回204 No Content,这是OSS上传成功的默认返回,如果想要有具体信息,就需要加上callback参数。
加上callback后请求会报错:
Private address is forbidden to callback.
这就是前面提到的callback地址问题,callback地址必须是公网能够访问到的地址,阿里云OSS是不能通过 localhost找到本地计算机上的callback接口的。