【前言】
近段时间在写一个文件上传的小功能,,公司的老项目实际已经有上传功能,,但是经理说在上传大文件的时候失败几率很大,,于是就仔仔细细的看了下老系统的文件上传的写法,在看了其写法之后决定重新整理一下。
因为使用的是阿里的OSS文件存储服务,所以去看了阿里的官方文档 OSS文档 ,然后着手去开发一个新模式的OSS文件上传。
OSS上传文件的三种模式
1.web端直传
Web端常见的上传方法是用户在浏览器或app端上传文件到应用服务器,然后应用服务器再把文件上传到OSS,如下图所示:
特点:上传慢。先上传到应用服务器,再上传到OSS
2.服务端签名后直传
采用javaScript端向服务端发起签名请求,获取参数后直传OSS
特点:客户端向服务端请求签名,然后直接上传,不会对服务端产生压力,而且安全可靠。
3.服务端签名后直传并设置上传回调
客户端向服务端发起签名请求后,直传OSS然后OSS会回调对应的服务端回调接口,OSS回调完成后,应用服务器再返回结果给客户端。
特点:用户上传一个文件到OSS后,OSS会将上传结果返回给应用服务器,然后应用服务器会给OSS响应,然后OSS会将相关响应通知给客户端的用户。
综上:为阿里云的OSS官方给出的解决方案,简单分析了这几种方案。
经理所说的上传大文件失败的几率很大,并研究了代码后发现是用的第一种的上传方案,虽然使用了大文件的分块上传,但是并不建议使用,,
【正传】
这里是将后两种方案进行了拼合,具体写法见下文代码
因为博主为java开发文员,,所以就只放java代码吧,,至于前端代码,用上传控件将参数传对都可以,主要是服务端代码。
- 导入阿里云的jar
<dependency> <groupId>com.aliyun.oss</groupId> <artifactId>aliyun-sdk-oss</artifactId> <version>2.8.3</version> </dependency>
在编写代码的时候注意导入的jar是否为此jar,本文引入的为此jar,因为阿里云的jar更新迭代的几个版本有依赖关系。
-
因为我用的是SpringBoot项目,,所以就先建了个参数的bean实体,后续参数的获取请以项目的实际获取方式进行获取。
/** * Author: hzl * Date: 2018/11/27 15:40 * Description: oss参数配置配置类 */ package com.ejsino.chat.config.aliyunoss; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; /** * @author hzl * @create 2018/11/27 * @Description: oss参数配置配置类 * @since 1.0.0 */ @Component @ConfigurationProperties(prefix = "oss") public class OssConfigBean { private String bucketName; private String accessKeyId; private String accessKeySecret; private String endPoint; private String domainAddress; private String productClauseFilePath; private String tempFilePath; /** * oss参数获取打印 * @return */ @Override public String toString() { return "OssConfigBean{" + "bucketName='" + bucketName + '\'' + ", accessKeyId='" + accessKeyId + '\'' + ", accessKeySecret='" + accessKeySecret + '\'' + ", endPoint='" + endPoint + '\'' + ", domainAddress='" + domainAddress + '\'' + ", productClauseFilePath='" + productClauseFilePath + '\'' + ", tempFilePath='" + tempFilePath + '\'' + '}'; } public String getBucketName() { return bucketName; } public void setBucketName(String bucketName) { this.bucketName = bucketName; } public String getAccessKeyId() { return accessKeyId; } public void setAccessKeyId(String accessKeyId) { this.accessKeyId = accessKeyId; } public String getAccessKeySecret() { return accessKeySecret; } public void setAccessKeySecret(String accessKeySecret) { this.accessKeySecret = accessKeySecret; } public String getEndPoint() { return endPoint; } public void setEndPoint(String endPoint) { this.endPoint = endPoint; } public String getDomainAddress() { return domainAddress; } public void setDomainAddress(String domainAddress) { this.domainAddress = domainAddress; } public String getProductClauseFilePath() { return productClauseFilePath; } public void setProductClauseFilePath(String productClauseFilePath) { this.productClauseFilePath = productClauseFilePath; } public String getTempFilePath() { return tempFilePath; } public void setTempFilePath(String tempFilePath) { this.tempFilePath = tempFilePath; } }
- OSS上传参数获取方式
在这里给那些初次看文档进行开发的码农说一下,,阿里给出的demo上边是将多组参数封装到了一个请求里边,,我这里是将他们分开来进行获取了,因为这些参数分为两种,一个是OSS初始化上传空间需要的参数,,一部分是灵活变动的签名参数,,还有,,因为签名具有实效性,所以每次在进行上传前都需要获取一下参数,,这里要特别注意一下,因为OSS存储文件的形式是重名文件默认覆盖原则,,所以在进行文件上传的时候一定要注意文件名的命名,,这里的写法是将文件名在服务端生成。
首先,我们先获取初始化需要的参数/** * @Description OSS上传需要的OSS服务参数获取 * @Author hzl * @Date 2018/11/27 * @Param [] * @Return java.util.HashMap<java.lang.Stringjava.lang.Object> */ public HashMap<String, Object> generateOssParam() throws Exception { try { String domain = ossConfigBean.getDomainAddress(); String dir = ossConfigBean.getProductClauseFilePath(); String bucketName = ossConfigBean.getBucketName(); String endPoint = ossConfigBean.getEndPoint(); String accessKeyId = ossConfigBean.getAccessKeyId(); String host = "http://" + bucketName + "." + endPoint.replace("http://", ""); HashMap<String, Object> respMap = new LinkedHashMap<String, Object>(); respMap.put("accessid", accessKeyId); respMap.put("dir", dir); respMap.put("host", host); respMap.put("domain", domain); return respMap; } catch (Exception e) { log.error("获取OSS上传参数失败:" + e); throw new Exception("获取OSS上传参数失败!"); } }
其次,就是签名生成的代码
/** * @Description 生成签名 * @Author hzl * @Date 2018/11/27 * @Param [callBack :要进行回调的参数,传入为空即默认为不进行回调] * @Return java.util.HashMap<java.lang.String java.lang.Object> */ public HashMap<String, Object> generateSign(String callBack) throws Exception { //获取上传oss需要的基本参数 String endPoint = ossConfigBean.getEndPoint(); String accessKeyId = ossConfigBean.getAccessKeyId(); String accessKeySecret = ossConfigBean.getAccessKeySecret(); String dir = ossConfigBean.getProductClauseFilePath(); OSSClient client = null; try { //开启OSS客户端 client = new OSSClient(endPoint, accessKeyId, accessKeySecret); long expireTime = 30; long expireEndTime = System.currentTimeMillis() + expireTime * 1000; //生成的到期时间转换位s,并转换为String String expire = String.valueOf(expireEndTime / 1000); PolicyConditions policyConditions = new PolicyConditions(); policyConditions.addConditionItem(PolicyConditions.COND_CONTENT_LENGTH_RANGE, 0, 1048576000); policyConditions.addConditionItem(MatchMode.StartWith, PolicyConditions.COND_KEY, dir); //根据到期时间生成policy Date expiration = new Date(expireEndTime); String postPolicy = client.generatePostPolicy(expiration, policyConditions); //生成signature String postSignature = client.calculatePostSignature(postPolicy); //对policy进行UTF-8编码后转base64 byte[] binaryData = postPolicy.getBytes("utf-8"); String encodedPolicy = BinaryUtil.toBase64String(binaryData); //生成上传文件的文件名 String fileName = MyConst.WEB_OSS_FILE + UUID.randomUUID().toString(); //封装生成好的数据进行参数返回 HashMap<String, Object> respMap = new LinkedHashMap<String, Object>(); respMap.put("policy", encodedPolicy); respMap.put("signature", postSignature); respMap.put("expire", expire); respMap.put("fileName", fileName); //callBack不为空时为OSS回调web服务上传 if (callBack != null && callBack != "") { respMap.put("callback", callBack); } return respMap; } catch (Exception e) { log.error("生成OSS上传签名失败:" + e); throw new Exception("生成OSS上传签名失败!"); } finally { if (client != null) { client.shutdown(); } } }
因为上边的这种写法是将需要回调的和不需要回调的放到一个接口进行处理了,所以,如果需要回调,那么回调参数的组装请用下边的这个方法
/** * @Description OSS回调系统服务参数生成 * @Author hzl * @Date 2018/11/27 * @Param [callBackHost:要OSS进行回调的服务域名(不带http), * callBackInterFace:要进行回调的接口(oss上传结束进行请求的接口), * callBackBody:进行回调时携带的参数(以:key=value 的形式进行携带)] * @Return java.lang.String */ public String generateCallBack(String callBackHost, String callBackInterFace, String callBackBody) throws UnsupportedEncodingException { Map<String, Object> callbackMap = new HashMap<String, Object>(); callbackMap.put("callbackUrl", "http://" + callBackHost + callBackInterFace); callbackMap.put("callbackHost", callBackHost); callbackMap.put("callbackBody", callBackBody); callbackMap.put("callbackBodyType", "application/x-www-form-urlencoded"); byte[] callBack = JSONObject.fromObject(callbackMap).toString().getBytes("utf-8"); String callBackString = BinaryUtil.toBase64String(callBack); return callBackString; }
设置了回调请求的上传那么还需要一个OSS的回调接口
/** * @Description OSS上传回调 * @Author hzl * @Date 2018/11/28 * @Param [ossCallbackBody, authorization, publicKeyUrlBase64, request, response] * @Return com.ejsino.chat.config.domain.JSONResult */ @RequestMapping("/ossCallBack") public JSONResult callBack(@RequestBody String ossCallbackBody, @RequestHeader("Authorization") String authorization, @RequestHeader("x-oss-pub-key-url") String publicKeyUrlBase64, HttpServletRequest request, HttpServletResponse response) { boolean isCallBack = ossConfig.verifyOSSCallbackRequest(authorization, publicKeyUrlBase64, ossCallbackBody, request.getQueryString(), request.getRequestURI()); if (isCallBack) { response.setStatus(HttpServletResponse.SC_OK); return JSONResult.success("success"); } else { response.setStatus(HttpServletResponse.SC_BAD_REQUEST); return JSONResult.error("回调验证失败!"); } }
/** * @Description OSS回调请求验证 * @Author hzl * @Date 2018/11/27 * @Param [authorizationInput, pubKeyInput, ossCallbackBody, queryString, uri] * @Return boolean */ public boolean verifyOSSCallbackRequest(String authorizationInput, String pubKeyInput, String ossCallbackBody, String queryString, String uri){ boolean ret = false; try { //将base64编码的数据进行还原 byte[] authorization = BinaryUtil.fromBase64String(authorizationInput); byte[] pubKey = BinaryUtil.fromBase64String(pubKeyInput); String pubKeyAddr = new String(pubKey); if (!pubKeyAddr.startsWith("http://gosspublic.alicdn.com/") && !pubKeyAddr.startsWith("https://gosspublic.alicdn.com/")) { log.error("pub key addr must be oss address"); return false; } //获取请求中的公钥信息 String retString = executeGet(pubKeyAddr); retString = retString.replace("-----BEGIN PUBLIC KEY-----", ""); retString = retString.replace("-----END PUBLIC KEY-----", ""); String decodeUri = URLDecoder.decode(uri, "utf-8"); if (queryString != null && !"".equals(queryString)) { decodeUri += "?" + queryString; } decodeUri += "\n" + ossCallbackBody; ret = doCheck(decodeUri, authorization, retString); } catch (Exception e) { ret = false; log.error("验证OSS请求出现异常:" + e); } return ret; } /** * @Description 获取请求中的参数 * @Author hzl * @Date 2018/11/27 * @Param [pubKeyUrl] * @Return java.lang.String */ @SuppressWarnings({"finally"}) private String executeGet(String pubKeyUrl) throws Exception { BufferedReader in = null; String content = null; try { // 定义HttpClient @SuppressWarnings("resource") DefaultHttpClient defaultHttpClient = new DefaultHttpClient(); // 实例化HTTP方法 HttpGet request = new HttpGet(); request.setURI(new URI(pubKeyUrl)); HttpResponse response = defaultHttpClient.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(); return content; } catch (Exception e) { log.error("解析公钥参数失败:" + e); throw new Exception("解析公钥参数失败!"); } finally { if (in != null) { try { in.close(); } catch (IOException e) { log.error("关闭BufferedReader出现异常:" + e); } } } } /** * @Description 对请求参数进行规则校验 * @Author hzl * @Date 2018/11/27 * @Param [content, sign, publicKey] * @Return boolean */ 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) { log.error("校验出现异常:" + e); } return false; }
至此,OSS上传就已经全部结束了。
OSS的文件直传因为要设置安全域名,,所以一定要先看官方文档,然后再看我的demo,在测试的时候也要特别的注意,因为有回调设置,所以需要公网或者内网穿透。