龙空技术网

如何实现无刷新安全图片上传功能

财会小贝 1712

前言:

眼前咱们对“异步上传图片”大约比较重视,看官们都想要剖析一些“异步上传图片”的相关文章。那么小编在网摘上收集了一些有关“异步上传图片””的相关知识,希望大家能喜欢,我们一起来学习一下吧!

有时候我们想通过表单的方式收集用户的数据,例如,想收集用户在使用系统的过程中遇到的故障,我们可能需要提供一个表单让用户填写故障模块并附上截图。这时候,我们可能面对的是匿名的用户,即用户无需任何权限就能够提交表单,这就不得不考虑安全性的问题。例如,要检查用户提交的内容中是否包含恶意代码,防范SQL注入、XSS攻击等行为,另外,还要对用户的访问行为进行控制,例如是否频繁提交和刷新。(当然,授权用户也要考虑安全问题,只是我们可以有更多手段来降低这种风险)

而对于要使用到文件上传功能的表单,我们要考虑的问题就更为复杂了,例如要进行文件类型校验、文件的大小及上传频率进行限制等,而且要在服务器中进行合理的数据隔离,再甚者,需要调用杀毒模块进行病毒查杀。

下面,我以自己项目中的例子来谈一下实现方式,如果有不完善的地方欢迎留言指正。

为了给用户更好的体验,这个项目采取了ajax异步上传的方式,避免大量的数据一次性提交到服务器而造成较大的处理压力。

一、前端功能展示

1、正常的上传过程及前端代码实现

这里只展示了表单当中的故障截图、问题描述两个域。

前端界面

当用户点击上述带加号图片的时候,直接弹出图片文件选择框:

直接弹出文件选择框

上图右下角我们看到弹出框默认只是显示图片文件(不过,用户可以下拉选择显示“所有文件”)。

这个实现比较简单,就是用到accept属性:

<div style="display:none;">

<input id="upload-file" name="files" accept="image/*" type="file">

</div>

注意这里我们对这个file元素进行了隐藏,因为它默认呈现是下面这个样子的,并不好看。

bindUpload:function(){

var me = $(this);

me.click(function(e){

$("#upload-file").click();

$("#upload-file").unbind("change");

$("#upload-file").bind("change",function(){

uploadFile(me);

});

});

}

上面我们定义了一个绑定图片click()事件的方法,当点击图片的时候触发file元素的click()事件,从而可以灵活根据需要给用户更加人性化的上传提示。接着还为file元素绑定了change事件,当其内容发生改变时(也就是用户已经在弹出框中选择了文件并确定之后),触发uploadFile方法来执行ajax的提交。

uploadFile()方法:

function uploadFile(me){

var file = document.getElementById("upload-file").files[0];

var data = new FormData();

data.append("file",file);

$.ajax({

data : data,

type : "POST",

url : "upload",

cache : false,

contentType : false,

processData : false,

dataType:"json",

success : function(response) {

if (response.success) {

for (var i = 0; i < response.data.length; i++) {

var obj = response.data[i];

var imageUrl = obj.serverPath;

me.attr("src",imageUrl);

}

}else{

toastr.error(response.message, '提示', {positionClass: 'toast-top-center'});

}

}

});

}

这里用到FormData()来封装要提交的数据(当然你也可以单独建立一个enctype="multipart/form-data"的form表单来实现)。如果提交成功,则将原来默认的那个图片文件替换为服务器返回的文件。当然,这里你还可以增加对文件扩展名的校验逻辑,例如文件结尾只能是.jpg、.png、.bmp。

上传成功后的界面是这样子的:

正常上传返回结果

上图中我们看到图片右上角还有一个删除按钮,即用户传错了可以删除后重新上传。而用户直接单击已经上传的图片,则可以重新选择其他图片。

2、异常的上传

除了正常的上传外,我们还要对非法的上传进行限制,这里进行了三种限制:文件类型、文件大小、上传次数。

文件类型校验

文件大小校验

文件次数我这里就不展示了,看后面的讲述。

二、服务器端实现

结合上述的功能,在服务器端我们要完成以下几件事情:

首先是要保存上传的数据。我使用的是springmvc,所以实现起来比较简单,下面只列出主要的代码:

MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request; //转换request类型

Map<String, MultipartFile> fileMap = multipartRequest.getFileMap();

Iterator<String> fileIterator = multipartRequest.getFileNames();

while (fileIterator.hasNext()) {

String fileKey = fileIterator.next();

MultipartFile multipartFile = fileMap.get(fileKey); //得到临时文件

if (multipartFile.getSize() != 0L) {

File destFile = new File(uploadRootDir, fileName); //目标文件

multipartFile.transferTo(destFile);// 将临时文件拷贝到目标文件

}

}

我们来思考一下,如果只是使用上述代码直接保存文件,会有哪些问题??我们带着这些问题来谈一下如何优化。

1、如何判断文件是否为图片?

我们期望用户上传的文件是图片文件,但由于在客户端我们只能通过扩展名来进行校验,这显然是不可靠的,用户改个扩展名就照样可以上传了。

而我们知道MultipartFile中有一个getContentType()的方法,但这个方法也不靠谱,因为它也是根据客户端给出的数据类型来判定的。我们将一个js文件的扩展名改成.png,然后用Firebug来看一下上传的数据:

一个js文件更改扩展名为.png

可以看到Post中有一个Content-Type,这就是MultipartFile获取到的值。

那么,怎样才能知道文件本来的真正类型呢?方法只有去读一下文件的内容。由于所有的文件都是二进制数据(下面显示的是十六进制),所以我们试一下用此方式来打开两张正常的图片文件,看有什么共同点。从下图可以看到,前面的几个字节是完全一样的。

我们再打开被改成png的那个js文件,可以看到它的二进制代码是这样的,显然跟真正的图片文件不一样,因此,通过读取这几个字节,我们基本可以判断文件的所属类型。

网上提供了以下的方法,我们借用以下:

private static String getFileHeader(File file) {

FileInputStream is = null;

String value = null;

try {

is = new FileInputStream(file);

byte[] b = new byte[4];

is.read(b, 0, b.length);

value = bytesToHexString(b);

System.out.println("value"+value);

} catch (Exception e) {

e.printStackTrace();

} finally {

if (null != is) {

try {

is.close();

} catch (IOException e) {

}

}

}

return value;

}

上面的代码读取文件的前4个字节,将其转换为十六进制的字符串。我们只需要对上述结果与图片文件的头部信息进行对比,就知道是否为图片文件了。

常见图片的头部值:

"FFD8FF", "jpg"

"FFD8FFE0", "jpeg"

"89504E47", "png"

"47494638", "gif"

"49492A00", "tif"

"424D", "bmp"

可是,这完全放心了吗?NO!!!既然图片的头部代码是相同的,有心人只需要对文件的前几个字节篡改一下,就可以绕过我们的检查了,有些木马就是这样传播的,例如伪造成一张美女图片,你一打开就中招。所以,上述方法依旧有漏洞。网上已经有人给出了一个思路,利用ImageIO类来读取文件的宽度和高度,并为图片添加水印,如果成功,表明是图片,否则不是。具体代码这里就不展示,大家可以百度一下。当然,我们还可以有其他类似的方法,例如对图片进行缩放、剪切、生成缩略图等。

2、检验上传图片的大小

这个实现起来比较简单,因为MultipartFile本身就具有getSize()这个方法来获得文件的大小。不过,这个是事后的,因为用户已经上传了文件,所以更为保险的方法是直接对用户提交的数据进行拦截,在springmvc是直接配置在xml文件里就可以了。

3、其他问题

由于我们采用的是异步上传的方式,因此用户可能会上传了很多无用的文件。例如拍摄不清晰,删除了又重新上传。由于文件删除的操作会比较敏感,所以一般我们不会让用户直接执行,而是在服务器后台通过定期清理无效文件的方式来处理。例如,我们可以建立一个UploadFile的实体类,用来记录用户已经上传的所有文件(文件名、大小、IP地址、上传时间),然后用户提交整个表单的时候再进行对比,删除无关的文件即可。如果不想用实体类,也可以用SessionID+随机数的方式来命名文件,由此确定文件的归属(但这个还要考虑会话过期的问题)。而对于半途而废的用户(只上传了图片但没有提交表单),我们还需要定期清理已上传的无主文件。在这个项目中,我的实现方式还多了一步,我用一个单独的目录来保存用户上传的文件,例如/tmp目录,然后用户提交表单的时候再将这些文件移动到最终的目录,这样做的好处是,可以对/tmp目录进行更加严格的权限控制。而且,我们可以设置会话的过期时间(如30分钟),要清理垃圾文件的时候只需要将这个目录中30分钟之前的文件删除掉即可,而不需要过多的判断。

还有一个问题,如果用户频繁上传、频繁提交怎么办?我们可以设置一个实体类,如Client,用来记录这些行为,并放到会话当中,用户每次上传都会刷新并进行比对。这个实体类需要包括:上一次上传时间(用来判断用户是否频繁上传)、上传文件总数(限制上传文件总数量)、提交问题总数(限制表单提交总数)、上次提交表单时间(避免频繁提交)、IP地址及SESSIONID(确定用户身份)等。这个Client类可以放到传统的Session,也可以放到Redis等数据库,但实现过程基本相同。

此外,你还可以对上传的数据(主要是非图片的内容)进行加密(如RSA),或者采用https的方式来进一步提升数据传输过程中的安全性。

温馨提示:如果使用springmvc整合Redis,一定要找到相匹配的版本(不一定要找最新的),否则很容易产生异常。

标签: #异步上传图片