aliyun oss 服务端签名后直传
前言
以前的文件上传都是前端将文件流上传到服务端后,服务端进行处理,返回给前端地址,这种方式适合传统单体的系统架构,不将文件服务作为一个单独的服务进行部署,在现在流行的分布式大环境下,传统的文件存储方式已经不适用,文件存储服务以第三方服务的形式,直接调用即可。例如阿里云 OSS,七牛云,腾讯云的COS 等等。这些第三方的文件服务就为我们提供了存储解决方案,既然我们已经不将文件存储在服务端,那我们也没有必要先让前端将文件流传递到服务端,再由服务端将文件上传到第三方文件服务。
下面是博主基于springboot搭建的一套完整的oss直传的DEMO
码云地址 :https://gitee.com/jack_whh/oss-policy-springboot.git
码云地址
前端直传 官方文档地址
- 优点:
- 直传文件流不用过服务端,上传速度不受服务端带宽影响。
- 由于文件不过服务端,所以也不占用服务端的系统资源。 - 缺点:
- 客户端通过JavaScript把AccesssKeyID和AccessKeySecret写在代码里面有泄露的风险。
为解决直传的缺点,所以提出本文介绍重点:服务端签名后直传。
一. 流程介绍
-
流程如下图所示:
-
当用户要上传一个文件到OSS,而且希望将上传的结果返回给应用服务器时,需要设置一个回调函数,将请求告知应用服务器。用户上传完文件后,不会直接得到返回结果,而是先通知应用服务器,再把结果转达给用户。
代码实现
- 安装:
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.10.2</version>
</dependency>
-
前期设置
- 创建bucket 快捷入口
- 修改CORS
-
获取policy的主要代码
public Map<String, String> getPolicy() {
// host的格式为 bucketname.endpoint
final String host = "http://" + ossProperties.getBucket() + "." + ossProperties.getEndpoint();
// 直传有效截止时间
long expireEndTime = System.currentTimeMillis() + (ossProperties.getExpireTime() * 1000);
Date expiration = new Date(expireEndTime);
PolicyConditions policyConditions = new PolicyConditions();
// 设置可上传文件的大小
policyConditions.addConditionItem(PolicyConditions.COND_CONTENT_LENGTH_RANGE, ossProperties.getMin(), ossProperties.getMax());
// 设置上传文件的前缀、可忽略
policyConditions.addConditionItem(MatchMode.StartWith, PolicyConditions.COND_KEY, dir);
// 生成policy
String postPolicy = ossClient.generatePostPolicy(expiration, policyConditions);
byte[] binaryData = postPolicy.getBytes(StandardCharsets.UTF_8);
String encodedPolicy = BinaryUtil.toBase64String(binaryData);
String postSignature = ossClient.calculatePostSignature(postPolicy);
// 封装policy等信息
Map<String, String> aliOssPolicy = new HashMap<>();
aliOssPolicy.put("ossAccessKeyId", ossProperties.getAccessId());
aliOssPolicy.put("policy", encodedPolicy);
aliOssPolicy.put("signature", postSignature);
aliOssPolicy.put("dir", dir);
aliOssPolicy.put("host", host);
aliOssPolicy.put("expire", String.valueOf(expireEndTime / 1000));
aliOssPolicy.put("callback", getCallBackBody());
return aliOssPolicy;
}
private String getCallBackBody() {
Map<String, String> map = new HashMap<>();
map.put("callbackUrl", ossProperties.getCallbackUrl());
map.put("callbackBody", "filename=${object}&size=${size}&mimeType=${mimeType}&height=${imageInfo.height}&width=${imageInfo.width}");
map.put("callbackBodyType", "application/x-www-form-urlencoded");
String s = JSON.toJSONString(map);
String base64CallbackBody = null;
try {
base64CallbackBody = BinaryUtil.toBase64String(s.getBytes("utf-8"));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return base64CallbackBody;
}
- 主要参数说明:
{
"accessid":"6MKO******4AUk44",
"host":"http://post-test.oss-cn-hangzhou.aliyuncs.com",
"policy":"eyJleHBpcmF0aW9uIjoiMjAxNS0xMS0wNVQyMDo1Mjoy******Jjdb25kaXRpb25zIjpbWyJjdb250ZW50LWxlbmd0aC1yYW5nZSIsMCwxMDQ4NTc2MDAwXSxbInN0YXJ0cy13aXRoIiwiJGtleSIsInVzZXItZGlyXC8iXV19",
"signature":"VsxOcOudx******z93CLaXPz+4s=",
"expire":1446727949,
"callback":"eyJjYWxsYmFja1VybCI6Imh0dHA6Ly9vc3MtZGVtby5hbGl5dW5jcy5jdb206MjM0NTAiLCJjYWxsYmFja0hvc3QiOiJvc3MtZGVtby5hbGl5dW5jcy5jdb20iLCJjYWxsYmFja0JvZHkiOiJmaWxlbmFtZT0ke29iamVjdH0mc2l6ZT0ke3NpemV9Jm1pbWVUeXBlPSR7bWltZVR5cGV9JmhlaWdodD0ke2ltYWdlSW5mby5oZWlnaHR9JndpZHRoPSR7aW1hZ2VJdbmZvLndpZHRofSIsImNhbGxiYWNrQm9keVR5cGUiOiJhcHBsaWNhdGlvbi94LXd3dy1mb3JtLXVybGVuY29kZWQifQ==",
"dir":"user-dirs/"
}
accessid 密钥key
host oss的上传地址
signature 签名
expire 密钥过期时间
callback 回调的base64编码
dir 要上传的指定文件夹
- 上述示例的callback内容采用的是Base64编码。经过Base64解码后的内容如下:
{"callbackUrl":"http://oss-demo.aliyuncs.com:23450",
"callbackBody":"filename=${object}&size=${size}&mimeType=${mimeType}&height=${imageInfo.height}&width=${imageInfo.width}",
"callbackBodyType":"application/x-www-form-urlencoded"}
-
内容解析如下:
- CallbackUrl:OSS往这个服务器发送的URL请求
上传成功后,oss向服务端发送的回调地址,需是公网地址
。 - callbackHost:OSS发送这个请求时,请求头部所带的Host头。
- callbackBody:OSS请求时,发送给应用服务器的内容,可以包括文件的名称、大小、类型。如果是图片,可以是图片的高度、宽度。
- callbackBodyType:请求发送的Content-Type。
- CallbackUrl:OSS往这个服务器发送的URL请求
-
前端代码,这里使用vue+element-ui 写的一个小页面
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<!-- import CSS -->
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
<title>上传图片</title>
</head>
<body>
<div id="app">
<div class="div-center-class">
<el-upload
class="upload-demo"
action="#"
drag
:http-request="httpRequestHandle"
>
<i class="el-icon-upload"></i>
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
<div class="el-upload__tip" slot="tip">只能上传jpg/png文件,且不超过500kb</div>
</el-upload>
<div v-if="imgUrl">
<img :src="imgUrl"></img>
</div>
</div>
</div>
</body>
<!-- import Vue before Element -->
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<!-- import JavaScript -->
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
<!--import axios -->
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script>
new Vue({
el: '#app',
data: function () {
return {
imgUrl: ''
}
},
methods: {
getpolicy(file) {
_that = this
axios.get('ali/oss/getpolicy')
.then(function (response) {
let {ossAccessKeyId, policy, signature, host, callback} = response.data.data;
let formData = new FormData();
formData.append("key", `${new Date().getTime()}_${file.name}`);
formData.append("success_action_status", 200); // 让服务端返回200,不设置则默认返回204。
formData.append("OssAccessKeyId", ossAccessKeyId);
formData.append("policy", policy);
formData.append("signature", signature);
formData.append("callback", callback);
formData.append("file", file); // 必须放在最后
// 发送 POST 请求
_that.axiosPost("post", host, formData).then(function (res) {
_that.imgUrl = res
})
})
},
httpRequestHandle(options) {
let {file} = options;
this.getpolicy(file);
},
//封装
//axios封装post请求
axiosPost(method, url, data) {
let result = axios({
method: method,
url: url,
data: data
}).then(resp => {
return resp.data.data;
}).catch(error => {
return "exception=" + error;
});
return result;
}
}
})
</script>
<style>
.div-center-class {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
</style>
</html>
- 前端上传OSS回调
请求路由需和上面的callbackurl一致。
@PostMapping("/ali/oss/callback")
public R ossCallback(HttpServletRequest request) throws IOException {
// request 只能获取一次
String body = ossComponent.GetPostBody(request.getInputStream(), Integer.parseInt(request.getHeader("content-length")));
String[] strings = body.split("&");
String filename = strings[0].split("=")[1];
boolean b = ossComponent.VerifyOSSCallbackRequest(request,body);
if (b) {
String url = ossComponent.getUrl(filename);
return R.success(url);
}else {
return R.error("上传失败");
}
}
- 回调后,我们需要对请求进行校验,校验方法如下:
/**
* 验证上传回调的Request
*
* @param request
* @return
* @throws NumberFormatException
* @throws IOException
*/
public boolean VerifyOSSCallbackRequest(HttpServletRequest request,String ossCallbackBody)
throws NumberFormatException, IOException {
boolean ret = false;
String autorizationInput = new String(request.getHeader("Authorization"));
String pubKeyInput = request.getHeader("x-oss-pub-key-url");
byte[] authorization = BinaryUtil.fromBase64String(autorizationInput);
byte[] pubKey = BinaryUtil.fromBase64String(pubKeyInput);
String pubKeyAddr = new String(pubKey);
if (!pubKeyAddr.startsWith("http://gosspublic.alicdn.com/")
&& !pubKeyAddr.startsWith("https://gosspublic.alicdn.com/")) {
System.out.println("pub key addr must be oss addrss");
return false;
}
String retString = executeGet(pubKeyAddr);
retString = retString.replace("-----BEGIN PUBLIC KEY-----", "");
retString = retString.replace("-----END PUBLIC KEY-----", "");
String queryString = request.getQueryString();
String uri = request.getRequestURI();
String decodeUri = java.net.URLDecoder.decode(uri, "UTF-8");
String authStr = decodeUri;
if (queryString != null && !queryString.equals("")) {
authStr += "?" + queryString;
}
authStr += "\n" + ossCallbackBody;
ret = doCheck(authStr, authorization, retString);
return ret;
}
/**
* 获取Post消息体
*
* @param is
* @param contentLen
* @return
*/
public String GetPostBody(InputStream is, int contentLen) {
if (contentLen > 0) {
int readLen = 0;
int readLengthThisTime = 0;
byte[] message = new byte[contentLen];
try {
while (readLen != contentLen) {
readLengthThisTime = is.read(message, readLen, contentLen - readLen);
if (readLengthThisTime == -1) {// Should not happen.
break;
}
readLen += readLengthThisTime;
}
return new String(message);
} catch (IOException e) {
}
}
return "";
}
/**
* 验证RSA
*
* @param content
* @param sign
* @param publicKey
* @return
*/
private boolean doCheck(String content, byte[] sign, String publicKey) {
try {
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
byte[] encodedKey = BinaryUtil.fromBase64String(publicKey);
PublicKey pubKey = keyFactory.generatePublic(new X509EncodedKeySpec(encodedKey));
java.security.Signature signature = java.security.Signature.getInstance("MD5withRSA");
signature.initVerify(pubKey);
signature.update(content.getBytes());
boolean bverify = signature.verify(sign);
return bverify;
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
/**
* 获取public key
*
* @param url
* @return
*/
@SuppressWarnings({"finally"})
private String executeGet(String url) {
BufferedReader in = null;
String content = null;
try {
// 定义HttpClient
@SuppressWarnings("resource")
DefaultHttpClient client = new DefaultHttpClient();
// 实例化HTTP方法
HttpGet request = new HttpGet();
request.setURI(new URI(url));
HttpResponse response = client.execute(request);
in = new BufferedReader(new InputStreamReader(response.getEntity().getContent()));
StringBuffer sb = new StringBuffer("");
String line = "";
String NL = System.getProperty("line.separator");
while ((line = in.readLine()) != null) {
sb.append(line + NL);
}
in.close();
content = sb.toString();
} catch (Exception e) {
} finally {
if (in != null) {
try {
in.close();// 最后要关闭BufferedReader
} catch (Exception e) {
e.printStackTrace();
}
}
return content;
}
}
- 校验成功后,获取图片的URL返回给前端
- 获取图片链接的地址
/**
* 获得url链接
*
* @param key
* @return
*/
public String getUrl(String key) {
if (StringUtils.isBlank(key)) {
return "";
}
Date expiration = new Date(System.currentTimeMillis() + 3600L * 1000 * 24 * 365 * 10);
URL url = ossClient.generatePresignedUrl(ossProperties.getBucket(), key, expiration);
if (url != null) {
return url.toString();
}
return null;
}
key 为上传文件的文件名。
这里可以设置图片链接的有限期。
====================================================================
以上就是配置服务端签名后直传文件的步骤。
下面是博主基于springboot搭建的一套完成的oss直传的DEMO
码云地址 :https://gitee.com/jack_whh/oss-policy-springboot.git
码云地址