### 引言
> 服务端(Web端)签名直传并设置回调。当采用[服务端签名后直传](https://help.aliyun.com/document_detail/31926.html?spm=5176.doc31927.2.2.TRKLwY)方案后,问题来了,用户上传数据后,很多场景下,应用服务器都要知道用户上传了哪些文件,文件名字,甚至如果是图片的话,图片的大小等。为此阿里云OSS开发了上传回调功能。
>
> 在实际业务中,例如商城系统的客户端上传图片(例如,用户在客户端上传用户头像),那么在应用服务器(一般就是服务端)就要统计用户上传图片的规格、大小、文件名等信息。这就是要实现“SpringBoot+阿里云OSS上传回调”操作的背景。
>
> 如果您还不十分了解SpringBoot,请先掌握[《Spring Boot实战》](https://book.douban.com/subject/26857423/);并阅读阿里云文档中关于OSS的部分。
>
> 同时,请先购买阿里云[OSS](https://www.aliyun.com/product/oss)。阿里云对象存储OSS(Object Storage Service)是一款海量、安全、低成本、高可靠的云存储服务,提供99.9999999999%(12个9)的数据持久性,99.995%的数据可用性。多种存储类型供选择,全面优化存储成本。
### 准备工作
1.请先创建Bucket,建议选择北京地区,选择公共读。
![image-20220318221706060](https://mybolg-typora.oss-cn-beijing.aliyuncs.com/image-20220318221706060.png)
![image-20220318221814004](https://mybolg-typora.oss-cn-beijing.aliyuncs.com/image-20220318221814004.png)
2.修改CROS,设置跨域。
![image-20220318224505304](https://mybolg-typora.oss-cn-beijing.aliyuncs.com/image-20220318224505304.png)
3.获得AccessId和AccessKey。下载CSV文件到本地。
![image-20220318222108763](https://mybolg-typora.oss-cn-beijing.aliyuncs.com/image-20220318222108763.png)
如果对此不清楚,请阅读阿里云文档[创建存储空间](https://help.aliyun.com/document_detail/31885.html)。确保准备工作完成。
### 原理解析
#### 用户的请求逻辑
1. 用户向应用服务器取到上传policy和回调设置。
2. 应用服务器返回上传policy和回调。
3. 用户直接向OSS发送文件上传请求。
4. 等文件数据上传完,OSS给用户Response前,OSS会根据用户的回调设置,请求用户的服务器。
5. 如果应用服务器返回成功,那么就返回用户成功,如果应用服务器返回失败,那么OSS也返回给用户失败。这样确保了用户上传成功的照片,应用服务器都已经收到通知了。
6. 应用服务器给OSS返回。
7. OSS将应用服务器返回的内容返回给用户
#### 用例图
![](https://mybolg-typora.oss-cn-beijing.aliyuncs.com/p139016.png)
#### 流程图
![](https://docs-aliyun.cn-hangzhou.oss.aliyun-inc.com/internal/oss/0.0.4/assets/image/practice-post-callback-7.png)
### 项目代码实现
##### 配置文件application.yaml
```yml
server:
port: 80
# OSS相关配置信息
aliyun:
oss:
endpoint: oss-cn-beijing.aliyuncs.com # oss对外服务的访问域名
accessKeyId: LTAI5tS1dbXZ5Bd4UrXXXXXX # 访问身份验证中用到用户标识
accessKeySecret: VEcT4rYx1HDyZofhjtrd9XXXXXXX # 用户用于加密签名字符串和oss用来验证签名字符串的密钥
bucketName: zhoujk-mall # oss的存储空间
policy:
expire: 300 # 签名有效期(S)
maxSize: 10 # 上传文件大小(M)
# 需要外网地址
callback: http://59.110.190.21:8080/aliyun/oss/callback
dir:
prefix: mall/images/ # 上传文件夹路径前缀
```
##### 实现类
```java
@Service
public class OssServiceImpl implements OssService {
private static final Logger LOGGER = LoggerFactory.getLogger(OssServiceImpl.class);
@Value("${aliyun.oss.policy.expire}")
private int ALIYUN_OSS_EXPIRE;
@Value("${aliyun.oss.maxSize}")
private int ALIYUN_OSS_MAX_SIZE;
@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.callback}")
private String ALIYUN_OSS_CALLBACK;
@Autowired
private OSSClient ossClient;
/**
* 签名生成
*/
@Override
public OssPolicyResult policy() {
OssPolicyResult result = new OssPolicyResult();
// 存储目录
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
String dir = ALIYUN_OSS_DIR_PREFIX+sdf.format(new Date());
// 签名有效期
long expireEndTime = System.currentTimeMillis() + ALIYUN_OSS_EXPIRE * 1000;
Date expiration = new Date(expireEndTime);
// 文件大小
long maxSize = ALIYUN_OSS_MAX_SIZE * 1024 * 1024;
// 回调
OssCallbackParam callback = new OssCallbackParam();
callback.setCallbackUrl(ALIYUN_OSS_CALLBACK);
callback.setCallbackBody("filename=${object}&size=${size}&mimeType=${mimeType}&height=${imageInfo.height}&width=${imageInfo.width}");
callback.setCallbackBodyType("application/x-www-form-urlencoded");
// 提交节点
String action = "http://" + ALIYUN_OSS_BUCKET_NAME + "." + ALIYUN_OSS_ENDPOINT;
try {
PolicyConditions policyConds = new PolicyConditions();
policyConds.addConditionItem(PolicyConditions.COND_CONTENT_LENGTH_RANGE, 0, maxSize);
policyConds.addConditionItem(MatchMode.StartWith, PolicyConditions.COND_KEY, dir);
String postPolicy = ossClient.generatePostPolicy(expiration, policyConds);
byte[] binaryData = postPolicy.getBytes("utf-8");
String policy = BinaryUtil.toBase64String(binaryData);
String signature = ossClient.calculatePostSignature(postPolicy);
String callbackData = BinaryUtil.toBase64String(JSONUtil.parse(callback).toString().getBytes("utf-8"));
// 返回结果
result.setAccessKeyId(ossClient.getCredentialsProvider().getCredentials().getAccessKeyId());
result.setPolicy(policy);
result.setSignature(signature);
result.setDir(dir);
result.setCallback(callbackData);
result.setHost(action);
} catch (Exception e) {
LOGGER.error("签名生成失败", e);
}
return result;
}
/**
* 上传成功回调
*/
@Override
public OssCallbackResult callback(HttpServletRequest request) {
OssCallbackResult result= new OssCallbackResult();
String filename = request.getParameter("filename");
filename = "http://".concat(ALIYUN_OSS_BUCKET_NAME).concat(".").concat(ALIYUN_OSS_ENDPOINT).concat("/").concat(filename);
result.setFilename(filename);
result.setSize(request.getParameter("size"));
result.setMimeType(request.getParameter("mimeType"));
result.setWidth(request.getParameter("width"));
result.setHeight(request.getParameter("height"));
return result;
}
}
```
##### Controller类
```java
@Controller
@Api(tags = "OssController", description = "Oss管理")
@RequestMapping("/aliyun/oss")
public class OssController {
public static final Logger logger = LoggerFactory.getLogger(OssController.class);
@Autowired
private OssServiceImpl ossService;
@ApiOperation(value = "oss上传签名生成")
@RequestMapping(value = "/policy", method = RequestMethod.GET)
@ResponseBody
public CommonResult< OssPolicyResult > policy() {
logger.info("policy --------------> start");
OssPolicyResult result = ossService.policy();
logger.info("policy --------------> end");
return CommonResult.success(result);
}
@ApiOperation(value = "oss上传成功回调")
@RequestMapping(value = "/callback", method = RequestMethod.POST)
@ResponseBody
public CommonResult<OssCallbackResult> callback(HttpServletRequest request) {
logger.info("callback --------------> start");
OssCallbackResult ossCallbackResult = ossService.callback(request);
logger.info("callback --------------> end");
return CommonResult.success(ossCallbackResult);
}
}
```
##### 运行结果
![image-20220318231707693](https://mybolg-typora.oss-cn-beijing.aliyuncs.com/image-20220318231707693.png)
### 接口测试
##### 本地接口测试
![image-20220318225232088](https://mybolg-typora.oss-cn-beijing.aliyuncs.com/image-20220318225232088.png)
##### OSS服务接口测试
![image-20220318225537218](https://mybolg-typora.oss-cn-beijing.aliyuncs.com/image-20220318225537218.png)
测试结果说明
1. `<Error>`
2. `<Code>CallbackFailed</Code>`
3. `<Message>Error status : -1.OSS can not connect to your callbackUrl, please check it.</Message>`
4. `<RequestId>587C8735355BE8694A8E9100</RequestId>`
5. `<HostId>bucket.oss-cn-hangzhou.aliyuncs.com</HostId>`
6. `</Error>`
原因:回调服务器处理时间超过 *5秒*,OSS认为超时。建议回调服务器的处理逻辑修改为异步,保证在5秒内处理完毕并返回结果OSS。
### 最后
如果不采用SpringBoot整合,[也可此采用阿里云官方文档的方案](https://help.aliyun.com/document_detail/91868.html),分别下载应用服务器和客户端的源码,根据自己配置进行调整。
1. 配置应用服务器
2. 配置客户端
3. 修改CROS
4. 体验上传回调
### 总结
SpringBoot+阿里云OSS上传回调这一操作更多是配合SpringBoot[微服务项目](http://www.macrozheng.com/#/architect/mall_arch_10)适用,如尚医通预约挂号系统,mall商城系统。
### 参考
1.[服务端签名直传并设置上传回调](https://help.aliyun.com/document_detail/91868.html) 阿里云官方文档,非常规范。
2.[沧海一滴的文章](https://www.cnblogs.com/softidea/p/7217201.html)
3.[上传回调中的常见错误及分析处理](https://www.alibabacloud.com/help/zh/doc-detail/50092.htm)
4.[上传回调适用场景](https://wohugb.gitbooks.io/aliyun/content/oss/userguide/uploadobject/uploadcallback.html)
3.[团子的文章和代码](https://gitee.com/zhou431615/source-code-package)
SpringBoot+阿里云OSS上传回调