开始使用阿里云OSS
https://www.alibabacloud.com/help/zh/doc-detail/31883.htm
一.开发一个APP上传服务,需要有存储的支持,那么我们的解决方案将以下几种:
1. 1 直接将图片保存到服务的硬盘
- 1. 优点:开发便捷,成本低
- 2. 缺点:扩容困难
1.2 使用分布式文件系统进行存储
- 1. 优点:容易实现扩容
- 2. 缺点:开发复杂度稍大(尤其是开发复杂的功能)
1.3 使用nfs做存储
- 1. 优点:开发较为便捷
- 2. 缺点:需要有一定的运维知识进行部署和维护
1.4 使用第三方的存储服务
- 1. 优点:开发简单,拥有强大功能,免维护
- 2. 缺点:付费
在这我们采用第一、四解决方案,第三方服务选用阿里云的OSS服务。
二.配置
2.1 导入依赖
<dependency> <groupId>com.aliyun.oss</groupId> <artifactId>aliyun-sdk-oss</artifactId> <version>2.8.3</version> </dependency>
2.2 编写aliyun.properties配置文件
aliyun.endpoint= aliyun.accessKeyId= aliyun.accessKeySecret= aliyun.bucketName= aliyun.urlPrefix=
2.3 编写AliyunConfig
@Configuration @PropertySource(value = {"classpath:aliyun.properties"}) @ConfigurationProperties(prefix = "aliyun") @Data public class AliyunConfig { private String endpoint; private String accessKeyId; private String accessKeySecret; private String bucketName; private String urlPrefix; @Bean public OSS oSSClient() { return new OSSClient(endpoint, accessKeyId, accessKeySecret); } }
三.文件上传(图片,APP)
3.1 图片
// 允许上传的格式 private static final String[] IMAGE_TYPE = new String[]{".bmp", ".jpg", ".jpeg", ".gif", ".png"};
3.2 APP-OSS
/** * APK上传到阿里云oss * * @param request * @return */ // @RequestMapping(value = "/upload", method = RequestMethod.POST) @ResponseBody public Map<String, Object> upload(HttpServletRequest request) { try { MultipartHttpServletRequest fileRequest = (MultipartHttpServletRequest) request; MultipartFile file = fileRequest.getFile("file"); InputStream inputStream = file.getInputStream(); String name = file.getOriginalFilename(); systemOperLogWrite.systemLogWrite("上传文件", SystemOperLog.LOG_TYPE_INSERT, systemOperLogWrite.objectToString(name)); //本地判断是否存在 String localMd5 = AliyunOSSClientUtil.getLocalMd5(file); AppVersionVo vo = new AppVersionVo(); vo.setMd5(localMd5); List<AppVersionEntity> appVersionEntities = appVersionDubboService.findByVo(vo); if (CollectionUtils.isNotEmpty(appVersionEntities)) { return ResultUtil.getFailResJson("该版本已存在", AcsConstent.BACK_FAILED_CODE); } String keyNeme = ""; if (StringUtils.isBlank(name)) { return ResultUtil.getFailResJson("文件名不能为空"); } if (name.endsWith(AcsConstent.APP.endWithName)) { keyNeme = AcsConstent.APP.startFileName + StringUtils.getUUID() + AcsConstent.APP.endWithName; } else { return ResultUtil.getFailResJson(AcsConstent.APP.uploaFileNameError, AcsConstent.BACK_FAILED_CODE); } long size = 1; if (file.getSize() > AcsConstent.INT_NUM_KB) { size = file.getSize() / AcsConstent.INT_NUM_KB / AcsConstent.INT_NUM_KB; } // String bucketName = "aiot-face-image"; SystemConfigEntity systemConfigField = systemConfigDubboService.getSystemConfigField(Constants.Aliyun_BucketName); String bucketName = systemConfigField.getValue(); // Endpoint以杭州为例,其它Region请按实际情况填写 // String endpoint = "http://oss-cn-shenzhen.aliyuncs.com"; SystemConfigEntity systemConfigField1 = systemConfigDubboService.getSystemConfigField(Constants.Aliyun_Endpoint); String endpoint = systemConfigField1.getValue(); SystemConfigEntity accessKey = systemConfigDubboService.getSystemConfigField(Constants.Aliyun_AccessKeyId); String accessKeyId = accessKey.getValue(); SystemConfigEntity accessSecret = systemConfigDubboService.getSystemConfigField(Constants.Aliyun_AccessKeySecret); String accessKeySecret = AESUtil.aesDecrypt(accessSecret.getValue(), AcsConstent.APP.AccessKeySecret_DecryptKey); // 创建OSSClient实例 OSSClient ossClient = new OSSClient(endpoint, accessKeyId, accessKeySecret); // 文件大小 ObjectMetadata metadata = new ObjectMetadata(); // 上传的文件的长度 metadata.setContentLength(inputStream.available()); // 指定该Object被下载时的网页的缓存行为 metadata.setCacheControl("no-cache"); // 指定该Object下设置Header metadata.setHeader("Pragma", "no-cache"); // 指定该Object被下载时的内容编码格式 metadata.setContentEncoding("utf-8"); // 文件的MIME,定义文件的类型及网页编码,决定浏览器将以什么形式、什么编码读取文件。如果用户没有指定则根据Key或文件名的扩展名生成 // 如果没有扩展名则填默认值application/octet-stream metadata.setContentType(AliyunOSSClientUtil.getContentType(name)); // 指定该Object被下载时的名称(指示MINME用户代理如何显示附加的文件,打开或下载,及文件名称) PutObjectResult putResult = ossClient.putObject(bucketName, keyNeme, inputStream, metadata); String eTag = putResult.getETag(); // 关闭OSSClient ossClient.shutdown(); Date expiration = new Date(System.currentTimeMillis() + AcsConstent.APP.EXPIRE_DAY); String url = ossClient.generatePresignedUrl(bucketName, keyNeme, expiration).toString(); JSONObject jsonObject = new JSONObject(); jsonObject.put("url", url); jsonObject.put("MD5", eTag); jsonObject.put("appSize", size); return StringUtils.isNotBlank(url) ? ResultUtil.getSuccessResJson("data", jsonObject.toString()) : ResultUtil.getFailResJson("上传失败", "1"); } catch (Exception e) { logger.info("上传文件失败###################{}", e); } return null; }
3.2 APP-服务器
@RequestMapping(value = "/upload", method = RequestMethod.POST) @ResponseBody public Map<String, Object> uploadApk(HttpServletRequest request) { FileOutputStream fileOutputStream = null; InputStream inputStream = null; try { MultipartHttpServletRequest fileRequest = (MultipartHttpServletRequest) request; MultipartFile file = fileRequest.getFile("file"); inputStream = file.getInputStream(); String fileName = file.getOriginalFilename(); //本地判断是否存在 String localMd5 = AliyunOSSClientUtil.fileToMd5(file); JSONObject checkJson = checkPamams(localMd5, fileName); if (checkJson != null) { return checkJson; } long size = 1; if (file.getSize() > AcsConstent.INT_NUM_KB) { size = file.getSize() / AcsConstent.INT_NUM_KB / AcsConstent.INT_NUM_KB; } String downloadFileUrl = redisOperatorManager.getValue("upload.file.url", CommonConstants.SYSTEM_CONFIG_REDIS_INDEX, "http://%s:%s/acs-admin/system/fileManager/downloadFileByKey?key=%s"); downloadFileUrl = String.format(downloadFileUrl, host, port, localMd5); String fileUploadPath = fileUploadBasePath + File.separator + "app"; File fileUploadPathFile = new File(fileUploadPath); if (!fileUploadPathFile.exists()) { fileUploadPathFile.mkdirs(); } //新文件名为md5值 String keyName = localMd5 + AcsConstent.APP.endWithName; String fileUploadFileName = fileUploadPath + File.separator + keyName; File fileUploadFile = new File(fileUploadFileName); if (!fileUploadFile.exists()) { fileUploadFile.createNewFile(); } fileOutputStream = new FileOutputStream(fileUploadFile); IOUtils.copy(inputStream, fileOutputStream); JSONObject jsonObject = new JSONObject(); jsonObject.put("url", downloadFileUrl); jsonObject.put("MD5", localMd5); jsonObject.put("appSize", size); jsonObject.put("originalFilename", fileName); FileUploadVo fileUploadVo = new FileUploadVo(); fileUploadVo.setFilePath(fileUploadPath); fileUploadVo.setFileName(keyName); fileUploadVo.setMd5(localMd5); //通知 notifyFileUploadZk(fileUploadVo); systemOperLogWrite.systemLogWrite("上传文件", SystemOperLog.LOG_TYPE_INSERT, jsonObject.toJSONString()); return ResultUtil.getSuccessResJson("data", jsonObject.toString()); } catch (RuntimeException e){ logger.info("运行时抛出异常###################{}", e); } catch (Exception e) { logger.info("上传文件失败###################{}", e); } finally { IOUtils.closeQuietly(fileOutputStream); IOUtils.closeQuietly(inputStream); } return ResultUtil.getFailResJson("上传失败", "1"); }
OSS的简单使用
OSS简介
Object Storage Service,简称 OSS,是阿里云提供的海量、安全、低成本、高可靠的云存储服务。
它具有与平台无关的RESTful API接口,能够提供99.999999999%的服务持久性。
使用场景:
- 图片分享
- 热点视频
优势:
- 成本低(40G才6元,比ECS便宜太多)
- 不会影响ECS带宽
- 和服务器解耦
下面介绍一些基本功能:
- 初始化
- 创建存储空间
- 上传文件
- 跨域访问设置
- 设置读写权限
OSS使用(NET SDK使用)
1.初始化
创建一个OssClient,就可以很方便的调用OSS的方法。
const string accessKeyId = "xxxxxxxxx"; const string accessKeySecret = "xxxxxxxxxx"; const string endpoint = "oss-cn-beijing.aliyuncs.com"; //OSS对应的区域地址 private static OssClient ossClient = new OssClient(endpoint, accessKeyId, accessKeySecret);
2.创建存储空间
很简单,只需要调用OssClient.CreateBucket
ossClient.CreateBucket("myBucket"); //新建一个Bucket
3.设置读写权限
调用OssClient.SetBucketAcl
ossClient.SetBucketAcl("myBucket", CannedAccessControlList.PublicRead); //设置为公共读
CannedAccessControlList有三个属性:Private(私有),PublicRead(公共读),PublicReadWrite(公共读写)
4.跨域访问设置
调用OssClient.SetBucketCors
var req = new SetBucketCorsRequest("myBucket"); var rule = new CORSRule(); //指定允许跨域请求的来源 rule.AddAllowedOrigin("*"); //指定允许的跨域请求方法(GET/PUT/DELETE/POST/HEAD) rule.AddAllowedMethod("POST"); //控制在OPTIONS预取指令中Access-Control-Request-Headers头中指定的header是否允许。 rule.AddAllowedHeader("*"); req.AddCORSRule(rule); ossClient.SetBucketCors(req);
5.上传文件
调用OssClient.PutObject
var result = ossClient.PutObject("myBucket", "111.mp4", @"d:\237badef-0f6d-4a8e-a634-a44c9704b6e6.mp4"); Console.WriteLine(result.ETag);
6.列出存储空间中的所有文件
调用ossClient.ListObjects
var listObjectsRequest = new ListObjectsRequest("myBucket"); var result = ossClient.ListObjects(listObjectsRequest); Console.WriteLine("List objects succeeded"); foreach (var summary in result.ObjectSummaries) { Console.WriteLine("File name:{0}", summary.Key); }
以上步骤1到4,可以在OSS管理后端完成
WEB端直传
刚开始使用OSS的时候,是采用前端将文件流上传到Web服务器,然后通过Web服务器再上传到OSS
这种做法有三个缺点:
- 第一:上传慢。先上传到应用服务器,再上传到OSS,网络传送多了一倍。如果数据直传到OSS,不走应用服务器,速度将大大提升,而且OSS是采用BGP带宽,能保证各地各运营商的速度。
- 第二:扩展性不好。如果后续用户多了,应用服务器会成为瓶颈。
- 第三:费用高。由于OSS上传流量是免费的。如果数据直传到OSS,不走应用服务器,那么将能省下几台应用服务器
如何操作?
1.通过JS控件直传
- 采用plupload 直接提交表单数据(即PostObject)到OSS;
- 在JS端,输入OSS的认证信息(不安全)
var policyText = { "expiration": "2020-01-01T12:00:00.000Z", //设置该Policy的失效时间,超过这个失效时间之后,就没有办法通过这个policy上传文件了 "conditions": [ ["content-length-range", 0, 1048576000] // 设置上传文件的大小限制 ] }; accessid= '6MKOqxGiGU4AUk44'; accesskey= 'ufu7nS8kS59awNihtjSonMETLI0KLy'; host = 'http://post-test.oss-cn-hangzhou.aliyuncs.com'; var policyBase64 = Base64.encode(JSON.stringify(policyText)) message = policyBase64 var bytes = Crypto.HMAC(Crypto.SHA1, message, accesskey, { asBytes: true }) ; var signature = Crypto.util.bytesToBase64(bytes); var uploader = new plupload.Uploader({ runtimes : 'html5,flash,silverlight,html4', browse_button : 'selectfiles', 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 : host, multipart_params: { 'Filename': '${filename}', 'key' : '${filename}', 'policy': policyBase64, 'OSSAccessKeyId': accessid, 'success_action_status' : '200', //让服务端返回200,不然,默认会返回204 'signature': signature, }, init: { PostInit: function() { document.getElementById('ossfile').innerHTML = ''; document.getElementById('postfiles').onclick = function() { uploader.start(); 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>'; }); }, 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) { //alert(info.status) if (info.status >= 200 || info.status < 200) { document.getElementById(file.id).getElementsByTagName('b')[0].innerHTML = 'success'; } else { document.getElementById(file.id).getElementsByTagName('b')[0].innerHTML = info.response; } }, Error: function(up, err) { document.getElementById('console').appendChild(document.createTextNode("\nError xml:" + err.response)); } } }); uploader.init();
具体可以下载OSS官网的 [例子]:https://help.aliyun.com/document_detail/31925.html?spm=a2c4g.11186623.6.629.AdgPho
2.通过服务端构建认证信息(安全,参看下一小节)
服务端构建认证信息
上一小节中,通过JS来上传文件,虽然可以上传,但是AppSecret会泄露,不安全。所以需要在服务端将认证信息构建出来,再给到前端
public virtual ActionResult GetPostPolicy() { string host = "http://" + bucket + "."+ endpoint; //第一步,构造policy //var dir = "zhangsan/";//设置用户上传指定的前缀,必须以斜线结尾 var expiration = DateTime.Now.AddMinutes(100); var policyConds = new PolicyConditions(); //policyConds.AddConditionItem(MatchMode.StartWith, PolicyConditions.CondKey, dir);//上传目录 policyConds.AddConditionItem(PolicyConditions.CondContentLengthRange, 1, 1048576000);//允许上传的文件大小限制 var postPolicy = ossClient.GeneratePostPolicy(expiration, policyConds);//给policyConds添加过期时间并json序列化(格式iso8601:"yyyy-MM-dd'T'HH:mm:ss.fff'Z'") //第二步 将policy 的json字符串进行base64编码 var base64Policy = Convert.ToBase64String(Encoding.UTF8.GetBytes(postPolicy)); //第三步,生成签名 var signature = ComputeSignature(accessKeySecret, base64Policy);//生成签名 //以下返回给前端 TimeSpan ts = expiration - new DateTime(1970, 1, 1, 0, 0, 0, 0); var expire = Convert.ToInt64(ts.TotalSeconds); Dictionary<string, object> response = new Dictionary<string, object>(); response["accessid"] = accessKeyId; response["host"] = host; response["policy"] = base64Policy; response["signature"] = signature; response["expire"] = expire; return ResponseSuccess(response); //返回json,可以自己修改(该方法是自己封装的) } private static string ComputeSignature(string key, string data) { using (var algorithm = KeyedHashAlgorithm.Create("HmacSHA1".ToUpperInvariant())) { algorithm.Key = Encoding.UTF8.GetBytes(key.ToCharArray()); return Convert.ToBase64String( algorithm.ComputeHash(Encoding.UTF8.GetBytes(data.ToCharArray()))); } }
再结合刚才的前端,修改如下
<script type="text/javascript"> var accessid= ''; var host = 'http://muBucket.oss-cn-beijing.aliyuncs.com'; var policyBase64 = ''; var signature = ''; var uploadFileName=''; //文件名称 //获取随机字符串 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) { var pos = filename.lastIndexOf('.') suffix = '' if (pos != -1) { suffix = filename.substring(pos) } return suffix; } //设置plupload属性 function set_upload_param(up, filename, ret) { var suffix=''; if (filename != '') { suffix = get_suffix(filename) }else{ return; } uploadFileName=random_string(20)+suffix; new_multipart_params = { 'key' : uploadFileName, 'policy': policyBase64, 'OSSAccessKeyId': accessid, 'success_action_status' : '200', //让服务端返回200,不然,默认会返回204 'signature': signature, }; up.setOption({ 'url': host, 'multipart_params': new_multipart_params }); up.start(); } //初始化plupload控件 var uploader = new plupload.Uploader({ runtimes : 'html5,flash,silverlight,html4', browse_button : 'selectfiles', 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', multi_selection:false, filters: { mime_types : [ { title : "Video files", extensions : "mp4,rmvb" } ], max_file_size : '600000kb', //最大只能上传600M的文件 prevent_duplicates : true //不允许选取重复文件 }, url : host, init: { PostInit: function() { document.getElementById('ossfile').innerHTML = ''; $.ajax({ type:'GET', url:'GetPostPolicyURL', //获取认证信息的URL success:function(res){ if(res.rspcode==="0000"){ accessid = res.accessid; host = res.host; policyBase64 =res.policy; signature =res.signature; }else{ /* 弹出框提示错误 */ toastr.error(res.rspmsg); } } }) }, FilesAdded: function(up, files) { plupload.each(files, function(file) { set_upload_param(up, file.name, true); 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) { }, 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 || info.status < 200) { vm.formModel.VideoPath=uploadFileName; //将文件名记录下来,用于保存 document.getElementById(file.id).getElementsByTagName('b')[0].innerHTML = 'success'; } else { document.getElementById(file.id).getElementsByTagName('b')[0].innerHTML = info.response; } }, Error: function(up, err) { document.getElementById('console').appendChild(document.createTextNode("\nError xml:" + err.response)); } } }); uploader.init(); </script>
参考文章:
[OSS官网文档]:https://help.aliyun.com/document_detail/31817.html?spm=a2c4g.11186623.6.539.PK7qiJ