阿里云OSS基于POST Policy方式上传文件
一、POST Policy方式简介
Post policy 是阿里推出的一种安全的文件上传方式,但是官方文档中没有做一些详细的介绍,只是简单的提到了几个官方写好的SDK和一些核心的代码,但是正式环境下的应用可能提的不是太多。
为什么说他是安全的呢?因为我们都知道使用第三方业务会有accessKeySecret 与accessKeySecret 私密信息,来确保操作的安全性。而Post policy的就是给匿名用户提供一个有时效性的上传接口。
先看看他的具体流程(下图出自阿里OSS官方文档 https://help.aliyun.com/document_detail/31927.html?spm=a2c4g.11186623.2.10.28314367hQVm9f#concept-qp2-g4y-5db)
流程分析:
1.在你的服务端提供一个接口,用来提供Policy参数(因为这些参数是阿里生成的,所以你要在服务端通过SDK调用阿里的方法,然后生成Policy对象响应给请求端,这样请求段就可以拿这些参数去请求阿里并上传文件)
2.请求端可以是web可以是java等等等,因为他的原理其实就是Http 协议 form multipart/form-data 方式上传文件
3.请求端通过你提供的接口获取policy 参数,然后模拟http以multipart方式请求参数中host地址 直接上传文件
4.阿里云调用你提供的回调接口,告诉你上传是否成功
二、具体实践
注意:在这里调用的只是阿里的sdk
1.获取policy参数的接口
请求参数:请求参数需要自己定义根据自己的业务需求和权限控制来安排
响应参数:
参数简介
响应参数Java实体类
@Data
@NoArgsConstructor
@AllArgsConstructor
public class OssPolicy implements Serializable {
/** accesskeyId */
private String accessId;
/** policy */
private String policy;
/** 签名 */
private String signature;
/** 目录 */
private String dir;
/** 服务器地址 */
private String host;
/** 失效时间。单位:秒 */
private String expire;
/** 回调地址 */
private String callback;
/** 在此处只是简单地callback参数加密时用到 也可以写在外部*/
@JsonIgnore
private String callbackUrl;
public void setCallbackUrl(String callbackUrl) {
this.callbackUrl = callbackUrl;
OssPolicy.Callback param = new OssPolicy.Callback(callbackUrl);
this.callback = BinaryUtil.toBase64String(JSONObject.fromObject(param).toString().getBytes());
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class Callback {
private static final String CALLBACK_BODY = "filename=${object}&size=${size}&mimeType=${mimeType}&height=${imageInfo.height}&width=${imageInfo.width}";
private static final String CALLBACK_BODY_TYPE = "application/x-www-form-urlencoded";
private String callbackUrl;
private String callbackBody = CALLBACK_BODY;
private String callbackBodyType = CALLBACK_BODY_TYPE;
public Callback(String callbackUrl) {
this.callbackUrl = callbackUrl;
}
}
}
接口响应示例
Spring mvc 版本接口样例
2.提供给阿里的回调接口
回调接口的基本设置
让阿里调用回调时可以把一些需要的字段请求过来,可以是json格式也可以是form格式
设置后响应的信息 response 是阿里以form格式传过来的,我没有做处理就返回过来了。具体可以看代码
当然也可以自定义参数
具体java代码
//region 回调处理
@PostMapping("/uploads/callback/{requestId}")
public ResponseInfo<Map> uploadCallback(@PathVariable String requestId, @RequestBody(required = false) String body, HttpServletRequest request, HttpServletResponse response) {
Map responseBody = new HashMap();
try {
body = (null == body) ? "" : body;
boolean ret = verifyOSSCallbackRequest(request, body);
// System.out.println("OSS Callback Body:" + ossCallbackBody);
if (ret) {
responseBody.put("status","ok");
responseBody.put("response",body);
return new ResponseInfo<Map>(true,responseBody);
} else {
responseBody.put("status","failure");
return new ResponseInfo<Map>(false,responseBody);
}
} catch (Exception e) {
responseBody.put("status","error");
e.printStackTrace();
return new ResponseInfo<Map>(false);
}
}
/**
* 验证上传回调的Request
*
* @param request
* @param ossCallbackBody
* @return
* @throws NumberFormatException
* @throws IOException
*/
private 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 = getPublicKeyFromOss(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 = validateAuth(authStr, authorization, retString);
return ret;
}
/**
* 获取OSS public key
* @param url
* @return
*/
@SuppressWarnings({"finally"})
private String getPublicKeyFromOss(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;
}
}
/**
* 验证RSA
* @param content
* @param sign
* @param publicKey
* @return
*/
private static boolean validateAuth(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;
}
/**
* 服务器响应结果
*
* @param request
* @param response
* @param results
* @param status
* @throws IOException
*/
private String getResponse(HttpServletRequest request, HttpServletResponse response, String results, int status){
String callback = request.getParameter("callback");
response.addHeader("Content-Length", String.valueOf(results.length()));
response.setStatus(status);
return StringUtils.isEmpty(callback) ? results : callback + "( " + results + " )";
}
3.使用Http form multipart/form-data 方式上传到阿里OSS
在上传前先调用获取Policy的接口获取以下参数(具体可以看上边,有详细的例子)
上传文件要用form表单的multipart/form-data方式
请求地址:调用获取Policy接口是返回的host参数就是上传的地址
序号 | 字段名 | 类型 | 长度 | 描述 |
1 | OSSAccessKeyId | String |
| 获取到的accessId |
2 | policy | String |
| 获取到的policy |
3 | signature | String |
| 获取到的签名 |
4 | key | String |
| 获取到的dir再加文件名拼接 |
5 | callback | String |
| 获取到的callback |
6 | success_action_status | String |
| 设置上传成功返回的状态码,默认是204 |
7 | file | File | 需要上传的文件 |
下面是java代码中的参数构成关键部分
multipartEntityBuilder.addTextBody("key",uploadPolicy.getDir()+file.getName(),contentType);
multipartEntityBuilder.addTextBody("policy",uploadPolicy.getPolicy());
multipartEntityBuilder.addTextBody("OSSAccessKeyId",uploadPolicy.getAccessId());
multipartEntityBuilder.addTextBody("success_action_status","200");
multipartEntityBuilder.addTextBody("callback",uploadPolicy.getCallback());
multipartEntityBuilder.addTextBody("signature",uploadPolicy.getSignature());
multipartEntityBuilder.addBinaryBody("file",file);
postman中测试的请求
java代码里上传测试
(因为multipart/form-data底层拼接比较麻烦,所以我们可以使用优秀的第三方Http工具包)
Maven依赖
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.3</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpmime</artifactId>
<version>4.5.3</version>
</dependency>
@Test
public void javaUploadByHttpClient() throws IOException {
GetUploadPolicyRequest reqs = new GetUploadPolicyRequest();
reqs.setClientId("2a2d2a8a42b64d418ad272e496c59253");
reqs.setClientType(GetUploadPolicyRequest.CLIENTTYPE_OTHER);
OssPolicy uploadPolicy = ossService.getUploadPolicy(reqs);
upload2(new File("C:\\Users\\Administrator\\Pictures\\日志事件上传测试数据.txt"),uploadPolicy);
}
public void upload2(File file, OssPolicy uploadPolicy) throws ClientProtocolException, IOException{
CloseableHttpClient httpClient = HttpClientBuilder.create().build();
CloseableHttpResponse httpResponse = null;
RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(200000).setSocketTimeout(200000000).build();
HttpPost httpPost = new HttpPost(uploadPolicy.getHost());
httpPost.setConfig(requestConfig);
MultipartEntityBuilder multipartEntityBuilder = MultipartEntityBuilder.create();
multipartEntityBuilder.setCharset(Charset.forName("UTF-8"));
multipartEntityBuilder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE);
//处理文件名乱码
ContentType contentType = ContentType.create(HTTP.PLAIN_TEXT_TYPE, HTTP.UTF_8);
//multipartEntityBuilder.addBinaryBody("file", file,ContentType.create("image/png"),"abc.pdf");
//当设置了setSocketTimeout参数后,以下代码上传PDF不能成功,将setSocketTimeout参数去掉后此可以上传成功。上传图片则没有个限制
//multipartEntityBuilder.addBinaryBody("file",file,ContentType.create("application/octet-stream"),"abd.pdf");
// multipartEntityBuilder.addTextBody("name",/*file.getName()*/"aaaaa.png");
multipartEntityBuilder.addTextBody("key",uploadPolicy.getDir()+file.getName(),contentType);
multipartEntityBuilder.addTextBody("policy",uploadPolicy.getPolicy());
multipartEntityBuilder.addTextBody("OSSAccessKeyId",uploadPolicy.getAccessId());
multipartEntityBuilder.addTextBody("success_action_status","200");
multipartEntityBuilder.addTextBody("callback",uploadPolicy.getCallback());
multipartEntityBuilder.addTextBody("signature",uploadPolicy.getSignature());
multipartEntityBuilder.addBinaryBody("file",file);
//multipartEntityBuilder.addPart("comment", new StringBody("This is comment", ContentType.TEXT_PLAIN));
HttpEntity httpEntity = multipartEntityBuilder.build();
httpPost.setEntity(httpEntity);
httpResponse = httpClient.execute(httpPost);
HttpEntity responseEntity = httpResponse.getEntity();
int statusCode= httpResponse.getStatusLine().getStatusCode();
if(statusCode == 200){
BufferedReader reader = new BufferedReader(new InputStreamReader(responseEntity.getContent()));
StringBuffer buffer = new StringBuffer();
String str = "";
while(!StringUtils.isEmpty(str = reader.readLine())) {
buffer.append(str);
}
System.out.println(buffer.toString());
}
httpClient.close();
if(httpResponse!=null){
httpResponse.close();
}
}
其实只要POSTMan里面可以跑通代码,在代码中实现也就不是什么事了。根据本文测试,java、ios、android 都已完成上传
完美解决方案需要使用js方式上传而且官方也有个很好的SDK提供,当然只适合于web端
使用讲解
目录结构
下面的代码是我根据我们的业务需求改的
里面主要修改的地方有三处
1.请求的地址:将serverUrl修改为你再服务端写的获取policy对象的接口
serverUrl = 'http://localhost:9099/openapi/oss/uploads/getUploadPolicy'
2.请求参数与方式:
将请求方式修改为POST或者GET
请求参数如果是json个是就修改对应的Content-type 为 application/json;charset=UTF-8
(记得设置跨域)
//如果是get 就修改为GET
xmlhttp.open( "POST", serverUrl, true );
xmlhttp.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
requestBody = {
"messageBody":{
"clientId":"aa83860fc5804ed1afe1bc083edee9df"
,"clientType":"other"
},
} //使用json格式将"Content-Type"改为 "application/json;charset=UTF-8"
xmlhttp.send(JSON.stringify(requestBody));
3.响应参数的处理
body中是字符类型的响应参数
obj 会转化为对象,然后根据响应的对象属性名称获取对象信息,按照正常的json格式取值,其中可以加自己的业务判断
因为我的接口响应格式是这样的
{"code":xxx,"message":"成功"messagebody:{"policy":"xxx"...............}}
所以下面obj我直接取出了messageBody
body = send_request()
var obj = eval ("(" + body + ")");
//上面会将我们响应的json转换成键值对的方式 (因为我设置的响应参数多了一层,所以这里policy参数被分装在messageBody参数中了)
obj = obj['messageBody']
host = obj['host']
policyBase64 = obj['policy']
accessid = obj['accessId']
signature = obj['signature']
expire = parseInt(obj['expire'])
callbackbody = obj['callback']
key = obj['dir']
return true;
accessid = ''
accesskey = ''
host = ''
policyBase64 = ''
signature = ''
callbackbody = ''
filename = ''
key = ''
expire = 0
g_object_name = ''
g_object_name_type = ''
now = timestamp = Date.parse(new Date()) / 1000;
function send_request()
{
var xmlhttp = null;
if (window.XMLHttpRequest)
{
xmlhttp=new XMLHttpRequest();
}
else if (window.ActiveXObject)
{
xmlhttp=new ActiveXObject("Microsoft.XMLHTTP");
}
if (xmlhttp!=null)
{
// serverUrl是 用户获取 '签名和Policy' 等信息的应用服务器的URL,请将下面的IP和Port配置为您自己的真实信息。
//====================================================获取policy的地址
serverUrl = 'http://localhost:9099/openapi/oss/uploads/getUploadPolicy'
xmlhttp.open( "POST", serverUrl, true );
//发什么格式的参数就用对应的Content-Type 因为使我们自定义的接口,所以请求参数的方式和格式都要我们自己定义
xmlhttp.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
requestBody = {
"messageBody":{
"clientId":"aa83860fc5804ed1afe1bc083edee9df"
,"clientType":"other"
}
} //使用json格式将"Content-Type"改为 "application/json;charset=UTF-8"
xmlhttp.send(JSON.stringify(requestBody));
return xmlhttp.responseText
}
else
{
alert("Your browser does not support XMLHTTP.");
}
};
function check_object_radio() {
var tt = document.getElementsByName('myradio');
for (var i = 0; i < tt.length ; i++ )
{
if(tt[i].checked)
{
g_object_name_type = tt[i].value;
break;
}
}
}
function get_signature()
{
// 可以判断当前expire是否超过了当前时间, 如果超过了当前时间, 就重新取一下,3s 作为缓冲。
now = timestamp = Date.parse(new Date()) / 1000;
if (expire < now + 3)
{
body = send_request()
var obj = eval ("(" + body + ")");
//上面会将我们响应的json转换成键值对的方式 (因为我设置的响应参数多了一层,所以这里policy参数被分装在messageBody参数中了)
obj = obj['messageBody']
host = obj['host']
policyBase64 = obj['policy']
accessid = obj['accessId']
signature = obj['signature']
expire = parseInt(obj['expire'])
callbackbody = obj['callback']
key = obj['dir']
return true;
}
return false;
};
function random_string(len) {
len = len || 32;
var chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678';
var maxPos = chars.length;
var pwd = '';
for (i = 0; i < len; i++) {
pwd += chars.charAt(Math.floor(Math.random() * maxPos));
}
return pwd;
}
function get_suffix(filename) {
pos = filename.lastIndexOf('.')
suffix = ''
if (pos != -1) {
suffix = filename.substring(pos)
}
return suffix;
}
function calculate_object_name(filename)
{
if (g_object_name_type == 'local_name')
{
g_object_name += "${filename}"
}
else if (g_object_name_type == 'random_name')
{
suffix = get_suffix(filename)
g_object_name = key + random_string(10) + suffix
}
return ''
}
function get_uploaded_object_name(filename)
{
if (g_object_name_type == 'local_name')
{
tmp_name = g_object_name
tmp_name = tmp_name.replace("${filename}", filename);
return tmp_name
}
else if(g_object_name_type == 'random_name')
{
return g_object_name
}
}
function set_upload_param(up, filename, ret)
{
if (ret == false)
{
ret = get_signature()
}
g_object_name = key;
if (filename != '') { suffix = get_suffix(filename)
calculate_object_name(filename)
}
new_multipart_params = {
'key' : g_object_name,
'policy': policyBase64,
'OSSAccessKeyId': accessid,
'success_action_status' : '200', //让服务端返回200,不然,默认会返回204
'callback' : callbackbody,
'signature': signature,
};
up.setOption({
'url': host,
'multipart_params': new_multipart_params
});
up.start();
}
var uploader = new plupload.Uploader({
runtimes : 'html5,flash,silverlight,html4',
browse_button : 'selectfiles',
//multi_selection: false,
container: document.getElementById('container'),
flash_swf_url : 'lib/plupload-2.1.2/js/Moxie.swf',
silverlight_xap_url : 'lib/plupload-2.1.2/js/Moxie.xap',
url : 'http://oss.aliyuncs.com',
filters: {
mime_types : [ //只允许上传图片和zip文件
{ title : "Image files", extensions : "jpg,gif,png,bmp" },
{ title : "Zip files", extensions : "zip,rar" }
],
max_file_size : '10mb', //最大只能上传10mb的文件
prevent_duplicates : true //不允许选取重复文件
},
init: {
PostInit: function() {
document.getElementById('ossfile').innerHTML = '';
document.getElementById('postfiles').onclick = function() {
set_upload_param(uploader, '', false);
return false;
};
},
FilesAdded: function(up, files) {
plupload.each(files, function(file) {
document.getElementById('ossfile').innerHTML += '<div id="' + file.id + '">' + file.name + ' (' + plupload.formatSize(file.size) + ')<b></b>'
+'<div class="progress"><div class="progress-bar" style="width: 0%"></div></div>'
+'</div>';
});
},
BeforeUpload: function(up, file) {
check_object_radio();
set_upload_param(up, file.name, true);
},
UploadProgress: function(up, file) {
var d = document.getElementById(file.id);
d.getElementsByTagName('b')[0].innerHTML = '<span>' + file.percent + "%</span>";
var prog = d.getElementsByTagName('div')[0];
var progBar = prog.getElementsByTagName('div')[0]
progBar.style.width= 2*file.percent+'px';
progBar.setAttribute('aria-valuenow', file.percent);
},
FileUploaded: function(up, file, info) {
if (info.status == 200)
{
document.getElementById(file.id).getElementsByTagName('b')[0].innerHTML = 'upload to oss success, object name:' + get_uploaded_object_name(file.name) + ' 回调服务器返回的内容是:' + info.response;
}
else if (info.status == 203)
{
document.getElementById(file.id).getElementsByTagName('b')[0].innerHTML = '上传到OSS成功,但是oss访问用户设置的上传回调服务器失败,失败原因是:' + info.response;
}
else
{
document.getElementById(file.id).getElementsByTagName('b')[0].innerHTML = info.response;
}
},
Error: function(up, err) {
if (err.code == -600) {
document.getElementById('console').appendChild(document.createTextNode("\n选择的文件太大了,可以根据应用情况,在upload.js 设置一下上传的最大大小"));
}
else if (err.code == -601) {
document.getElementById('console').appendChild(document.createTextNode("\n选择的文件后缀不对,可以根据应用情况,在upload.js进行设置可允许的上传文件类型"));
}
else if (err.code == -602) {
document.getElementById('console').appendChild(document.createTextNode("\n这个文件已经上传过一遍了"));
}
else
{
document.getElementById('console').appendChild(document.createTextNode("\nError xml:" + err.response));
}
}
}
});
uploader.init();
效果图
在这里顺便给大家提一下文件下载以安全模式下载的方式
可以参考官方api:官方安全访问介绍
第一种(不推荐)、这种控制可能权限方面比较宽一点,也就是说不保证拿到权限后不搞其他操作
第二种:推荐