阿里云OSS基于POST Policy方式上传文件

                       阿里云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.提供给阿里的回调接口

    官方详细文档:https://help.aliyun.com/document_detail/31989.html?spm=a2c4g.11186623.2.20.70494c07FVeyAl#section-btz-phx-wdb

回调接口的基本设置

 让阿里调用回调时可以把一些需要的字段请求过来,可以是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

7fileFile 需要上传的文件

 

下面是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端

 

官方客户端SDK获取地址 :http://docs-aliyun.cn-hangzhou.oss.aliyun-inc.com/assets/attach/86983/APP_zh/1537971352825/aliyun-oss-appserver-js-master.zip?spm=a2c4g.11186623.2.19.560e7eaeWdiv4K&file=aliyun-oss-appserver-js-master.zip

使用讲解

目录结构

 下面的代码是我根据我们的业务需求改的

里面主要修改的地方有三处

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:官方安全访问介绍

第一种(不推荐)、这种控制可能权限方面比较宽一点,也就是说不保证拿到权限后不搞其他操作

第二种:推荐 

 

 

  • 4
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值