前言
Java Web 三大组件已经学完了,今天学习Java代码实现文件的上传和下载,也是经常出现漏洞的一个关键点。
上传
创建上传表单
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
文件上传
实现文件上传需要导入两个jar包。
- commons-fileupload-1.2.2.jar
- commons-io-2.11.0.jar
再配置web.xml,注册一个servlet
UploadServlet
com.atguigu.java.UploadServlet
UploadServlet
/upload
UploadServlet 代码如下:
package com.atguigu.java;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileItemFactory;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.UUID;
/**
* @author cseroad
*/
public class UploadServlet extends HttpServlet {
/**
* 处理上传页面
*/
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
/**
* * 导入commons-fileupload、commons-io 两个jar包
* 解析上传的数据
*/
// 判断上传的数据是否是多段数据
if(ServletFileUpload.isMultipartContent(req)){
// 创建FileItemFactory 工厂实现类
FileItemFactory fileItemFactory = new DiskFileItemFactory();
// 创建用于解析上传数据的工具类servletFileUpload
ServletFileUpload servletFileUpload = new ServletFileUpload(fileItemFactory);
// 解析上传的数据。得到每一个表单的fileitem
servletFileUpload.setHeaderEncoding("UTF-8");
// 处理乱码
try {
List list = servletFileUpload.parseRequest(req);
// 解析form表单中的每个字段的数据,并将它们分别包装成独立的FileItem对象,区分是普通还是上传
for (FileItem item : list){
if (item.isFormField()){
// 普通表单
System.out.println("表单项的name属性:"+item.getFieldName());
System.out.println("表单项的value属性值:"+item.getString("UTF-8"));//处理乱码
} else {
// 文件表单
System.out.println("表单项的name属性:"+item.getFieldName());
String uploadfilename = item.getName();
System.out.println("上传的文件名:"+uploadfilename);
// 判断文件名是否存在
if (uploadfilename.trim().equals("") || uploadfilename == null){
continue;
}
//获取上传的文件名 1.txt
String fileName = uploadfilename.substring(uploadfilename.lastIndexOf("/") + 1);
//获得文件的后缀名
String fileExtName = uploadfilename.substring(uploadfilename.lastIndexOf(".") + 1);
//可以使用UUID(唯一识别的通用码),保证文件名唯一
String uuidFileName= UUID.randomUUID().toString();
System.out.println("保存的文件名:"+uuidFileName+"."+fileExtName);
// 1. 第一种保存方法
item.write(new File("/Users/cseroad/server/apache-tomcat-7.0.107/webapps/"+uuidFileName+"."+fileExtName));
}
// 2. 第二种存方法字节流
InputStream inputStream = item.getInputStream();
FileOutputStream fileOutputStream = new FileOutputStream("/Users/cseroad/server/apache-tomcat-7.0.107/webapps/"+uuidFileName+"."+fileExtName);
byte[] buffer = new byte[1024];
int len;
while ((len=inputStream.read(buffer) )> 0){
fileOutputStream.write(buffer,0,len);
}
fileOutputStream.close();
inputStream.close();
}
} catch (Exception e){
e.printStackTrace();
}
}
}
}
一般保存的路径是设置在当前工程下
String fileSaveRootPath = this.getServletContext().getRealPath("/file");
item.write(new File(fileSaveRootPath+"//"+uuidFileName+"."+fileExtName));
以上代码用了两种方式实现上传的文件进行保存到本地。
一种直接调用FileItem对象的write()
方法保存
item.write(new File("/Users/cseroad/server/apache-tomcat-7.0.107/webapps/"+uuidFileName+"."+fileExtName));
一种字节流的方式保存
InputStream inputStream = item.getInputStream();
FileOutputStream fileOutputStream = new FileOutputStream("/Users/cseroad/server/apache-tomcat-7.0.107/webapps/"+uuidFileName+"."+fileExtName);
byte[] buffer = new byte[1024 * 1024];
int len;
while ((len=inputStream.read(buffer) )> 0){
fileOutputStream.write(buffer,0,len);
}
fileOutputStream.close();
inputStream.close();
下载
创建表单点击下载
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
文件下载
再配置web.xml,同样注册一个servlet。
Download
com.atguigu.java.Download
Download
/download
Download 代码如下:
package com.atguigu.java;
import org.apache.commons.io.IOUtils;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLEncoder;
import java.util.UUID;
/**
* @author cseroad
*/
public class Download extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {// 读取下载的文件内容
String downloadFileName = req.getParameter("filename");
// 获取web应用的ServletContext对象
ServletContext servletContext = getServletConfig().getServletContext();
InputStream resourceAsStream = servletContext.getResourceAsStream("/file/"+downloadFileName);
// 告诉客户端返回的数据类型
String mimeType = servletContext.getMimeType("/file/" + downloadFileName);
resp.setContentType(mimeType);
resp.setCharacterEncoding("UTF-8");
String uuidFileName= UUID.randomUUID().toString();
String new_filename = uuidFileName+".jpg";
// 告诉客户端收到的数据用于下载使用
//resp.setHeader("Content-Disposition","attachment;filename="+downloadFileName);
// 处理中文编码
resp.setHeader("Content-Disposition","attachment;filename="+URLEncoder.encode(new_filename,"UTF-8"));
// 获取响应的输出流
ServletOutputStream outputStream = resp.getOutputStream();
// 复制流中数据
IOUtils.copy(resourceAsStream,outputStream);
}
}
这种方式非常简洁,用IOUtils完成流的copy操作。
第二种方式是创建字节流,完成写入和写出操作。
代码如下:
package com.atguigu.java;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLEncoder;
import java.util.UUID;
/**
* @author cseroad
*/
public class Download extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 读取下载的文件内容
String downloadFileName = req.getParameter("filename");
// 获取web应用的ServletContext对象
ServletContext servletContext = getServletConfig().getServletContext();
// 告诉客户端返回的数据类型
String mimeType = servletContext.getMimeType("/file/" + downloadFileName);
resp.setContentType(mimeType);
resp.setCharacterEncoding("UTF-8");
// 告诉客户端收到的数据用于下载使用
String uuidFileName= UUID.randomUUID().toString();
String new_filename = uuidFileName+".jpg";
// 处理中文编码
resp.setHeader("Content-Disposition","attachment;filename="+URLEncoder.encode(new_filename,"UTF-8"));
// 获取响应的输出流
ServletOutputStream outputStream = resp.getOutputStream();
// 第二种方法:保存流中数据
String path = req.getServletContext().getRealPath("/file/");
File file = new File(path,downloadFileName);
if (!file.exists()) {
req.setAttribute("message:", "您要下载的资源已被删除!!");
return;
}
FileInputStream fileInputStream = new FileInputStream(file);
byte[] buffer = new byte[1024];
int len;
while ((len=fileInputStream.read(buffer) )> 0){
outputStream.write(buffer,0,len);
outputStream.flush();
}
outputStream.close();
fileInputStream.close();
}
}
扩展
作为安全从业者,在以上代码处思考是否存在漏洞又该如何修复?
文件上传存在漏洞,属于任意文件上传,并未校验文件后缀名。
添加对文件后缀的强制校验,且采用白名单的方式
if (!(fileExtName.equals("png") || fileExtName.equals("gif") || fileExtName.equals("jpg"))){
System.out.println("图片格式有误!只能是png、gif、jpg");
return;
}
文件下载也存在漏洞,未对下载参数做任何过滤。
if (downloadFileName.contains("./") || downloadFileName.contains("../") || downloadFileName.contains("%")){
System.out.println("文件名包含非法字符!");
return;
}
通过判断参数是否存在./
、../
、%
非法字符来过滤处理。
总结
学习了java实现文件上传和下载用到的一些类和方法,并进一步通过代码修复了自己写的漏洞。