概述
文件上传和下载是Web应用中的常见功能。Spring MVC通过MultipartResolver接口提供了完善的文件上传支持,结合ResponseEntity可以实现灵活的文件下载功能。
核心技术栈
- Spring MVC 4.3.6: 提供文件处理的核心支持
- Commons FileUpload: Apache的文件上传组件
- Commons IO: 提供文件操作的工具类
- MultipartFile: Spring的文件上传抽象接口
应用场景
- 用户头像上传
- 文档资料管理
- 图片/视频分享
- 批量数据导入导出
- 在线文件存储服务
环境准备
Maven依赖配置
在pom.xml中添加以下依赖:
<!-- Spring Web MVC -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>4.3.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>4.3.6.RELEASE</version>
</dependency>
<!-- 文件上传组件 -->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.5</version>
</dependency>
<!-- Servlet API -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
<scope>provided</scope>
</dependency>
<!-- 可选:JSON处理 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.8</version>
</dependency>
Web配置文件
在web.xml中配置Spring MVC的DispatcherServlet:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
<!-- 配置字符编码过滤器 -->
<filter>
<filter-name>encodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<param-name>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- 配置Spring MVC DispatcherServlet -->
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:springmvc-config.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
Spring MVC配置
在springmvc-config.xml中配置文件上传解析器:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd">
<!-- 开启组件扫描 -->
<context:component-scan base-package="com.owlbay.controller" />
<!-- 开启注解驱动 -->
<mvc:annotation-driven />
<!-- 配置视图解析器 -->
<bean id="viewResolver"
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/jsp/" />
<property name="suffix" value=".jsp" />
</bean>
<!-- 配置文件上传解析器 -->
<bean id="multipartResolver"
class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<!-- 设置请求编码格式 -->
<property name="defaultEncoding" value="UTF-8" />
<!-- 设置允许上传的最大文件大小(10MB) -->
<property name="maxUploadSize" value="10485760" />
<!-- 设置允许上传的单个文件最大大小(5MB) -->
<property name="maxUploadSizePerFile" value="5242880" />
<!-- 设置上传文件的临时目录 -->
<property name="uploadTempDir" value="/temp" />
<!-- 设置内存中的最大缓存大小 -->
<property name="maxInMemorySize" value="4096" />
</bean>
<!-- 静态资源处理 -->
<mvc:resources mapping="/static/**" location="/static/" />
<mvc:resources mapping="/upload/**" location="/upload/" />
</beans>
文件上传实现
基本原理
文件上传的基本原理:
- 客户端:使用
multipart/form-data编码类型提交表单 - 服务端:通过MultipartResolver解析请求,提取文件数据
- 存储:将文件保存到服务器指定位置
- 响应:返回上传结果给客户端
单文件上传
创建上传页面
创建fileUpload.jsp页面:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/>
<title>文件上传示例</title>
<style>
.upload-container {
width: 600px;
margin: 50px auto;
padding: 20px;
border: 1px solid #ddd;
border-radius: 5px;
}
.form-group {
margin-bottom: 15px;
}
label {
display: inline-block;
width: 100px;
text-align: right;
margin-right: 10px;
}
input[type="text"], input[type="file"] {
width: 300px;
padding: 5px;
}
.btn {
margin-left: 110px;
padding: 8px 20px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.btn:hover {
background-color: #45a049;
}
#progressBar {
width: 100%;
height: 20px;
border: 1px solid #ddd;
display: none;
margin-top: 10px;
}
#progress {
width: 0%;
height: 100%;
background-color: #4CAF50;
text-align: center;
line-height: 20px;
color: white;
}
</style>
<script>
function validateForm() {
var name = document.getElementById("name").value;
var file = document.getElementById("file").value;
if(name.trim() === ""){
alert("请填写上传人姓名");
return false;
}
if(file.length === 0 || file === ""){
alert("请选择要上传的文件");
return false;
}
// 获取文件扩展名
var ext = file.substring(file.lastIndexOf('.') + 1).toLowerCase();
var allowedExts = ['jpg', 'jpeg', 'png', 'gif', 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'txt'];
if(allowedExts.indexOf(ext) === -1){
alert("不支持的文件类型!\n支持的类型:" + allowedExts.join(', '));
return false;
}
return true;
}
// 显示文件信息
function showFileInfo() {
var fileInput = document.getElementById("file");
var files = fileInput.files;
if(files.length > 0) {
var fileInfo = "选中文件:\n";
for(var i = 0; i < files.length; i++) {
fileInfo += "- " + files[i].name + " (" + formatFileSize(files[i].size) + ")\n";
}
document.getElementById("fileInfo").innerText = fileInfo;
}
}
// 格式化文件大小
function formatFileSize(bytes) {
if(bytes === 0) return '0 Bytes';
var k = 1024;
var sizes = ['Bytes', 'KB', 'MB', 'GB'];
var i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
</script>
</head>
<body>
<div class="upload-container">
<h2>文件上传</h2>
<form action="${pageContext.request.contextPath}/fileUpload"
method="post"
enctype="multipart/form-data"
onsubmit="return validateForm()">
<div class="form-group">
<label for="name">上传人:</label>
<input type="text" id="name" name="name" placeholder="请输入您的姓名"/>
</div>
<div class="form-group">
<label for="file">选择文件:</label>
<input type="file" id="file" name="uploadfile"
multiple="multiple" onchange="showFileInfo()"/>
</div>
<div class="form-group">
<pre id="fileInfo"></pre>
</div>
<div class="form-group">
<input type="submit" value="开始上传" class="btn"/>
</div>
<div id="progressBar">
<div id="progress">0%</div>
</div>
</form>
</div>
</body>
</html>
创建结果页面
创建上传成功页面success.jsp:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!DOCTYPE html>
<html>
<head>
<title>上传成功</title>
<style>
.success-container {
width: 500px;
margin: 100px auto;
text-align: center;
padding: 20px;
border: 2px solid #4CAF50;
border-radius: 5px;
background-color: #f0f8f0;
}
.success-icon {
color: #4CAF50;
font-size: 48px;
}
a {
color: #4CAF50;
text-decoration: none;
}
</style>
</head>
<body>
<div class="success-container">
<div class="success-icon">✓</div>
<h2>文件上传成功!</h2>
<p>您的文件已成功上传到服务器。</p>
<p><a href="${pageContext.request.contextPath}/fileUpload.jsp">继续上传</a> |
<a href="${pageContext.request.contextPath}/download.jsp">文件下载</a></p>
</div>
</body>
</html>
创建上传失败页面error.jsp:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!DOCTYPE html>
<html>
<head>
<title>上传失败</title>
<style>
.error-container {
width: 500px;
margin: 100px auto;
text-align: center;
padding: 20px;
border: 2px solid #f44336;
border-radius: 5px;
background-color: #fef0f0;
}
.error-icon {
color: #f44336;
font-size: 48px;
}
a {
color: #f44336;
text-decoration: none;
}
</style>
</head>
<body>
<div class="error-container">
<div class="error-icon">✗</div>
<h2>文件上传失败!</h2>
<p>错误信息:${errorMsg}</p>
<p><a href="javascript:history.back()">返回重试</a></p>
</div>
</body>
</html>
创建文件上传控制器
创建FileUploadController.java:
package com.owlbay.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.apache.commons.io.FileUtils;
import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.*;
/**
* 文件上传控制器
* @author Owlbay
* @date 2022/05/17
*/
@Controller
public class FileUploadController {
// 允许上传的文件类型
private static final List<String> ALLOWED_EXTENSIONS = Arrays.asList(
"jpg", "jpeg", "png", "gif", "bmp",
"pdf", "doc", "docx", "xls", "xlsx",
"ppt", "pptx", "txt", "zip", "rar"
);
// 最大文件大小(5MB)
private static final long MAX_FILE_SIZE = 5 * 1024 * 1024;
/**
* 显示文件上传页面
*/
@RequestMapping(value = "/upload", method = RequestMethod.GET)
public String showUploadForm() {
return "fileUpload";
}
/**
* 处理单文件上传
*/
@RequestMapping(value = "/fileUpload", method = RequestMethod.POST)
public String handleFileUpload(
@RequestParam("name") String name,
@RequestParam("uploadfile") MultipartFile file,
HttpServletRequest request,
Model model) {
// 验证文件是否为空
if (file.isEmpty()) {
model.addAttribute("errorMsg", "请选择要上传的文件");
return "error";
}
try {
// 验证文件大小
if (file.getSize() > MAX_FILE_SIZE) {
model.addAttribute("errorMsg", "文件大小超过限制(最大5MB)");
return "error";
}
// 获取文件扩展名
String originalFilename = file.getOriginalFilename();
String extension = getFileExtension(originalFilename);
// 验证文件类型
if (!isAllowedExtension(extension)) {
model.addAttribute("errorMsg", "不支持的文件类型");
return "error";
}
// 生成唯一文件名
String newFilename = generateUniqueFilename(name, originalFilename);
// 获取上传目录
String uploadDir = getUploadDirectory(request);
File uploadPath = new File(uploadDir);
// 创建目录
if (!uploadPath.exists()) {
uploadPath.mkdirs();
}
// 保存文件
File destFile = new File(uploadPath, newFilename);
file.transferTo(destFile);
// 记录上传信息
logUploadInfo(name, originalFilename, newFilename, file.getSize());
model.addAttribute("filename", newFilename);
model.addAttribute("filesize", formatFileSize(file.getSize()));
return "success";
} catch (IOException e) {
e.printStackTrace();
model.addAttribute("errorMsg", "文件上传失败:" + e.getMessage());
return "error";
}
}
/**
* 处理多文件上传
*/
@RequestMapping(value = "/multiFileUpload", method = RequestMethod.POST)
@ResponseBody
public Map<String, Object> handleMultiFileUpload(
@RequestParam("name") String name,
@RequestParam("uploadfiles") List<MultipartFile> files,
HttpServletRequest request) {
Map<String, Object> result = new HashMap<>();
List<Map<String, String>> uploadedFiles = new ArrayList<>();
int successCount = 0;
int failCount = 0;
for (MultipartFile file : files) {
Map<String, String> fileInfo = new HashMap<>();
try {
if (!file.isEmpty()) {
// 处理单个文件上传
String originalFilename = file.getOriginalFilename();
String extension = getFileExtension(originalFilename);
// 验证文件
if (!isAllowedExtension(extension)) {
fileInfo.put("name", originalFilename);
fileInfo.put("status", "failed");
fileInfo.put("message", "不支持的文件类型");
failCount++;
continue;
}
if (file.getSize() > MAX_FILE_SIZE) {
fileInfo.put("name", originalFilename);
fileInfo.put("status", "failed");
fileInfo.put("message", "文件大小超过限制");
failCount++;
continue;
}
// 生成文件名并保存
String newFilename = generateUniqueFilename(name, originalFilename);
String uploadDir = getUploadDirectory(request);
File uploadPath = new File(uploadDir);
if (!uploadPath.exists()) {
uploadPath.mkdirs();
}
File destFile = new File(uploadPath, newFilename);
file.transferTo(destFile);
fileInfo.put("name", originalFilename);
fileInfo.put("newName", newFilename);
fileInfo.put("size", formatFileSize(file.getSize()));
fileInfo.put("status", "success");
successCount++;
}
} catch (Exception e) {
fileInfo.put("name", file.getOriginalFilename());
fileInfo.put("status", "failed");
fileInfo.put("message", e.getMessage());
failCount++;
}
uploadedFiles.add(fileInfo);
}
result.put("files", uploadedFiles);
result.put("successCount", successCount);
result.put("failCount", failCount);
result.put("total", files.size());
return result;
}
/**
* 获取文件扩展名
*/
private String getFileExtension(String filename) {
if (filename == null || filename.isEmpty()) {
return "";
}
int dotIndex = filename.lastIndexOf('.');
return (dotIndex == -1) ? "" : filename.substring(dotIndex + 1).toLowerCase();
}
/**
* 检查文件扩展名是否允许
*/
private boolean isAllowedExtension(String extension) {
return ALLOWED_EXTENSIONS.contains(extension.toLowerCase());
}
/**
* 生成唯一的文件名
*/
private String generateUniqueFilename(String uploaderName, String originalFilename) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
String dateFolder = sdf.format(new Date());
String uuid = UUID.randomUUID().toString().replace("-", "");
String extension = getFileExtension(originalFilename);
// 格式:日期/上传人_时间戳_UUID.扩展名
return dateFolder + "/" + uploaderName + "_" +
System.currentTimeMillis() + "_" +
uuid + "." + extension;
}
/**
* 获取上传目录路径
*/
private String getUploadDirectory(HttpServletRequest request) {
String realPath = request.getSession().getServletContext().getRealPath("/");
return realPath + "upload" + File.separator;
}
/**
* 格式化文件大小
*/
private String formatFileSize(long size) {
if (size <= 0) return "0 B";
final String[] units = {"B", "KB", "MB", "GB", "TB"};
int digitGroups = (int) (Math.log10(size) / Math.log10(1024));
return String.format("%.2f %s", size / Math.pow(1024, digitGroups), units[digitGroups]);
}
/**
* 记录上传日志
*/
private void logUploadInfo(String uploader, String originalName, String savedName, long size) {
// 这里可以将上传信息保存到数据库或日志文件
System.out.println(String.format(
"文件上传 - 上传人: %s, 原始文件名: %s, 保存文件名: %s, 大小: %s",
uploader, originalName, savedName, formatFileSize(size)
));
}
}
多文件上传
多文件上传页面
创建multiFileUpload.jsp:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!DOCTYPE html>
<html>
<head>
<title>多文件上传</title>
<style>
.file-item {
margin: 5px 0;
padding: 5px;
border: 1px solid #ddd;
border-radius: 3px;
}
.remove-btn {
color: red;
cursor: pointer;
float: right;
}
</style>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</head>
<body>
<div class="upload-container">
<h2>多文件上传</h2>
<form id="multiUploadForm" enctype="multipart/form-data">
<div class="form-group">
<label>上传人:</label>
<input type="text" name="name" required/>
</div>
<div class="form-group">
<label>选择文件:</label>
<input type="file" id="fileInput" multiple/>
</div>
<div id="fileList"></div>
<button type="button" onclick="uploadFiles()">开始上传</button>
</form>
<div id="result"></div>
</div>
<script>
var selectedFiles = [];
$('#fileInput').change(function() {
var files = this.files;
var fileList = $('#fileList');
fileList.empty();
selectedFiles = [];
for(var i = 0; i < files.length; i++) {
selectedFiles.push(files[i]);
var item = $('<div class="file-item">' +
files[i].name + ' (' + formatFileSize(files[i].size) + ')' +
'<span class="remove-btn" onclick="removeFile(' + i + ')">×</span>' +
'</div>');
fileList.append(item);
}
});
function removeFile(index) {
selectedFiles.splice(index, 1);
updateFileList();
}
function updateFileList() {
var fileList = $('#fileList');
fileList.empty();
selectedFiles.forEach(function(file, index) {
var item = $('<div class="file-item">' +
file.name + ' (' + formatFileSize(file.size) + ')' +
'<span class="remove-btn" onclick="removeFile(' + index + ')">×</span>' +
'</div>');
fileList.append(item);
});
}
function uploadFiles() {
var formData = new FormData();
formData.append('name', $('input[name="name"]').val());
selectedFiles.forEach(function(file) {
formData.append('uploadfiles', file);
});
$.ajax({
url: '${pageContext.request.contextPath}/multiFileUpload',
type: 'POST',
data: formData,
processData: false,
contentType: false,
success: function(result) {
showResult(result);
},
error: function() {
alert('上传失败!');
}
});
}
function showResult(result) {
var html = '<h3>上传结果</h3>';
html += '<p>总计:' + result.total + ' 个文件</p>';
html += '<p>成功:' + result.successCount + ' 个</p>';
html += '<p>失败:' + result.failCount + ' 个</p>';
html += '<ul>';
result.files.forEach(function(file) {
html += '<li>' + file.name + ' - ' +
(file.status === 'success' ? '成功' : '失败:' + file.message) +
'</li>';
});
html += '</ul>';
$('#result').html(html);
}
function formatFileSize(bytes) {
if(bytes === 0) return '0 Bytes';
var k = 1024;
var sizes = ['Bytes', 'KB', 'MB', 'GB'];
var i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
</script>
</body>
</html>
大文件上传
对于大文件上传,需要实现分片上传功能:
/**
* 大文件分片上传控制器
*/
@RestController
@RequestMapping("/chunk")
public class ChunkUploadController {
// 存储上传进度
private Map<String, ChunkUploadInfo> uploadProgressMap = new ConcurrentHashMap<>();
/**
* 接收文件分片
*/
@PostMapping("/upload")
public Map<String, Object> uploadChunk(
@RequestParam("file") MultipartFile file,
@RequestParam("chunkNumber") int chunkNumber,
@RequestParam("totalChunks") int totalChunks,
@RequestParam("identifier") String identifier,
@RequestParam("filename") String filename) {
Map<String, Object> result = new HashMap<>();
try {
// 创建临时目录存储分片
String tempDir = "temp/" + identifier;
File tempDirFile = new File(tempDir);
if (!tempDirFile.exists()) {
tempDirFile.mkdirs();
}
// 保存分片
String chunkFilename = filename + ".part" + chunkNumber;
File chunkFile = new File(tempDir, chunkFilename);
file.transferTo(chunkFile);
// 更新上传进度
ChunkUploadInfo uploadInfo = uploadProgressMap.computeIfAbsent(
identifier, k -> new ChunkUploadInfo(filename, totalChunks)
);
uploadInfo.addUploadedChunk(chunkNumber);
// 检查是否所有分片都已上传
if (uploadInfo.isComplete()) {
// 合并分片
mergeChunks(identifier, filename, totalChunks);
uploadProgressMap.remove(identifier);
result.put("status", "complete");
result.put("message", "文件上传完成");
} else {
result.put("status", "uploading");
result.put("progress", uploadInfo.getProgress());
}
} catch (Exception e) {
result.put("status", "error");
result.put("message", e.getMessage());
}
return result;
}
/**
* 合并分片
*/
private void mergeChunks(String identifier, String filename, int totalChunks)
throws IOException {
String tempDir = "temp/" + identifier;
String uploadDir = "upload/";
File uploadDirFile = new File(uploadDir);
if (!uploadDirFile.exists()) {
uploadDirFile.mkdirs();
}
// 创建目标文件
File mergedFile = new File(uploadDir, filename);
try (FileOutputStream fos = new FileOutputStream(mergedFile)) {
// 按顺序合并分片
for (int i = 1; i <= totalChunks; i++) {
File chunkFile = new File(tempDir, filename + ".part" + i);
Files.copy(chunkFile.toPath(), fos);
chunkFile.delete(); // 删除分片
}
}
// 删除临时目录
new File(tempDir).delete();
}
/**
* 获取上传进度
*/
@GetMapping("/progress/{identifier}")
public Map<String, Object> getProgress(@PathVariable String identifier) {
Map<String, Object> result = new HashMap<>();
ChunkUploadInfo uploadInfo = uploadProgressMap.get(identifier);
if (uploadInfo != null) {
result.put("progress", uploadInfo.getProgress());
result.put("uploadedChunks", uploadInfo.getUploadedChunks());
result.put("totalChunks", uploadInfo.getTotalChunks());
} else {
result.put("progress", 0);
}
return result;
}
/**
* 分片上传信息类
*/
private static class ChunkUploadInfo {
private String filename;
private int totalChunks;
private Set<Integer> uploadedChunks = new HashSet<>();
public ChunkUploadInfo(String filename, int totalChunks) {
this.filename = filename;
this.totalChunks = totalChunks;
}
public void addUploadedChunk(int chunkNumber) {
uploadedChunks.add(chunkNumber);
}
public boolean isComplete() {
return uploadedChunks.size() == totalChunks;
}
public int getProgress() {
return (int) ((uploadedChunks.size() * 100.0) / totalChunks);
}
// getters...
}
}
上传进度显示
使用Ajax实现文件上传进度显示:
function uploadWithProgress() {
var formData = new FormData();
var fileInput = document.getElementById('file');
var file = fileInput.files[0];
formData.append('file', file);
formData.append('name', document.getElementById('name').value);
$.ajax({
url: '/fileUpload',
type: 'POST',
data: formData,
processData: false,
contentType: false,
xhr: function() {
var xhr = new window.XMLHttpRequest();
// 监听上传进度
xhr.upload.addEventListener('progress', function(evt) {
if (evt.lengthComputable) {
var percentComplete = evt.loaded / evt.total * 100;
$('#progress').css('width', percentComplete + '%');
$('#progress').text(Math.round(percentComplete) + '%');
}
}, false);
return xhr;
},
success: function(response) {
alert('上传成功!');
},
error: function() {
alert('上传失败!');
}
});
}
文件下载实现
基本下载
文件下载的实现方式有多种,最常用的是通过ResponseEntity返回文件流。
创建下载页面
创建download.jsp:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.net.URLEncoder" %>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>文件下载中心</title>
<style>
.download-container {
width: 800px;
margin: 50px auto;
padding: 20px;
}
.file-list {
border: 1px solid #ddd;
border-radius: 5px;
padding: 20px;
}
.file-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
border-bottom: 1px solid #eee;
}
.file-item:last-child {
border-bottom: none;
}
.file-info {
flex: 1;
}
.file-name {
font-weight: bold;
margin-bottom: 5px;
}
.file-meta {
color: #666;
font-size: 14px;
}
.download-btn {
background-color: #4CAF50;
color: white;
padding: 8px 16px;
text-decoration: none;
border-radius: 4px;
}
.download-btn:hover {
background-color: #45a049;
}
</style>
</head>
<body>
<div class="download-container">
<h2>文件下载中心</h2>
<div class="file-list">
<h3>可下载文件列表</h3>
<!-- 示例文件列表 -->
<div class="file-item">
<div class="file-info">
<div class="file-name">示例文档.pdf</div>
<div class="file-meta">大小: 2.5 MB | 上传时间: 2025-09-12</div>
</div>
<a href="${pageContext.request.contextPath}/download?filename=示例文档.pdf"
class="download-btn">下载</a>
</div>
<div class="file-item">
<div class="file-info">
<div class="file-name">项目报告.docx</div>
<div class="file-meta">大小: 1.2 MB | 上传时间: 2025-09-11</div>
</div>
<a href="${pageContext.request.contextPath}/download?filename=<%=URLEncoder.encode("项目报告.docx", "UTF-8")%>"
class="download-btn">下载</a>
</div>
<div class="file-item">
<div class="file-info">
<div class="file-name">数据分析.xlsx</div>
<div class="file-meta">大小: 856 KB | 上传时间: 2025-09-10</div>
</div>
<a href="${pageContext.request.contextPath}/download?filename=<%=URLEncoder.encode("数据分析.xlsx", "UTF-8")%>"
class="download-btn">下载</a>
</div>
</div>
<!-- 批量下载 -->
<div style="margin-top: 20px;">
<h3>批量下载</h3>
<form id="batchDownloadForm">
<label><input type="checkbox" name="files" value="示例文档.pdf"> 示例文档.pdf</label><br>
<label><input type="checkbox" name="files" value="项目报告.docx"> 项目报告.docx</label><br>
<label><input type="checkbox" name="files" value="数据分析.xlsx"> 数据分析.xlsx</label><br>
<button type="button" onclick="batchDownload()" style="margin-top: 10px;">批量下载</button>
</form>
</div>
</div>
<script>
function batchDownload() {
var checkboxes = document.getElementsByName('files');
var selectedFiles = [];
for(var i = 0; i < checkboxes.length; i++) {
if(checkboxes[i].checked) {
selectedFiles.push(checkboxes[i].value);
}
}
if(selectedFiles.length === 0) {
alert('请选择要下载的文件');
return;
}
// 发送批量下载请求
var form = document.createElement('form');
form.method = 'POST';
form.action = '${pageContext.request.contextPath}/batchDownload';
selectedFiles.forEach(function(file) {
var input = document.createElement('input');
input.type = 'hidden';
input.name = 'files';
input.value = file;
form.appendChild(input);
});
document.body.appendChild(form);
form.submit();
document.body.removeChild(form);
}
</script>
</body>
</html>
中文文件名处理
创建文件下载控制器FileDownloadController.java:
package com.owlbay.controller;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.http.*;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import org.apache.commons.io.FileUtils;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLEncoder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
/**
* 文件下载控制器
* @author Owlbay
* @date 2022/05/17
*/
@Controller
public class FileDownloadController {
/**
* 基本文件下载(处理中文文件名)
*/
@RequestMapping("/download")
public ResponseEntity<byte[]> fileDownload(
HttpServletRequest request,
@RequestParam("filename") String filename) throws Exception {
// 安全性检查:防止目录遍历攻击
if (filename.contains("..") || filename.contains("/") || filename.contains("\\")) {
return ResponseEntity.badRequest().build();
}
// 获取文件路径
String uploadDir = request.getSession().getServletContext().getRealPath("/upload/");
File file = new File(uploadDir, filename);
// 检查文件是否存在
if (!file.exists() || !file.isFile()) {
return ResponseEntity.notFound().build();
}
// 检查文件是否可读
if (!file.canRead()) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
}
// 对文件名进行编码处理
String encodedFilename = encodeFilename(request, filename);
// 设置响应头
HttpHeaders headers = new HttpHeaders();
headers.setContentDispositionFormData("attachment", encodedFilename);
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
headers.setContentLength(file.length());
// 添加缓存控制
headers.setCacheControl(CacheControl.noCache());
headers.setPragma("no-cache");
headers.setExpires(0);
// 读取文件内容并返回
byte[] fileContent = FileUtils.readFileToByteArray(file);
return new ResponseEntity<>(fileContent, headers, HttpStatus.OK);
}
/**
* 使用流式下载(适合大文件)
*/
@GetMapping("/streamDownload")
public void streamDownload(
HttpServletRequest request,
HttpServletResponse response,
@RequestParam("filename") String filename) throws Exception {
// 安全性检查
if (filename.contains("..") || filename.contains("/") || filename.contains("\\")) {
response.sendError(HttpServletResponse.SC_BAD_REQUEST);
return;
}
String uploadDir = request.getSession().getServletContext().getRealPath("/upload/");
File file = new File(uploadDir, filename);
if (!file.exists() || !file.isFile()) {
response.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
// 设置响应头
response.setContentType("application/octet-stream");
response.setContentLengthLong(file.length());
String encodedFilename = encodeFilename(request, filename);
response.setHeader("Content-Disposition", "attachment; filename=\"" + encodedFilename + "\"");
// 使用缓冲流进行文件传输
try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file));
BufferedOutputStream bos = new BufferedOutputStream(response.getOutputStream())) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = bis.read(buffer)) != -1) {
bos.write(buffer, 0, bytesRead);
}
bos.flush();
}
}
/**
* 根据浏览器类型对文件名进行编码
*/
private String encodeFilename(HttpServletRequest request, String filename)
throws UnsupportedEncodingException {
String userAgent = request.getHeader("User-Agent");
// 针对IE浏览器的处理
if (userAgent != null && (userAgent.contains("MSIE") ||
userAgent.contains("Trident") || userAgent.contains("Edge"))) {
// IE浏览器,使用URLEncoder编码
return URLEncoder.encode(filename, "UTF-8").replace("+", "%20");
}
// 其他现代浏览器
else {
// 使用ISO-8859-1编码
return new String(filename.getBytes("UTF-8"), "ISO-8859-1");
}
}
/**
* 批量下载(打包成zip)
*/
@PostMapping("/batchDownload")
public void batchDownload(
HttpServletRequest request,
HttpServletResponse response,
@RequestParam("files") List<String> filenames) throws Exception {
// 设置响应头
response.setContentType("application/zip");
response.setHeader("Content-Disposition",
"attachment; filename=\"batch_download.zip\"");
String uploadDir = request.getSession().getServletContext().getRealPath("/upload/");
// 创建ZIP输出流
try (ZipOutputStream zos = new ZipOutputStream(response.getOutputStream())) {
for (String filename : filenames) {
// 安全性检查
if (filename.contains("..") || filename.contains("/") || filename.contains("\\")) {
continue;
}
File file = new File(uploadDir, filename);
if (file.exists() && file.isFile()) {
// 添加文件到ZIP
ZipEntry entry = new ZipEntry(filename);
zos.putNextEntry(entry);
// 写入文件内容
try (FileInputStream fis = new FileInputStream(file)) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
zos.write(buffer, 0, bytesRead);
}
}
zos.closeEntry();
}
}
}
}
/**
* 获取文件信息(用于下载前预览)
*/
@GetMapping("/fileInfo")
@ResponseBody
public Map<String, Object> getFileInfo(
HttpServletRequest request,
@RequestParam("filename") String filename) {
Map<String, Object> info = new HashMap<>();
try {
String uploadDir = request.getSession().getServletContext().getRealPath("/upload/");
File file = new File(uploadDir, filename);
if (file.exists() && file.isFile()) {
info.put("name", filename);
info.put("size", formatFileSize(file.length()));
info.put("lastModified", new Date(file.lastModified()));
info.put("type", getFileType(filename));
info.put("exists", true);
} else {
info.put("exists", false);
}
} catch (Exception e) {
info.put("error", e.getMessage());
}
return info;
}
/**
* 获取文件类型
*/
private String getFileType(String filename) {
String extension = filename.substring(filename.lastIndexOf('.') + 1).toLowerCase();
Map<String, String> mimeTypes = new HashMap<>();
mimeTypes.put("pdf", "application/pdf");
mimeTypes.put("doc", "application/msword");
mimeTypes.put("docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document");
mimeTypes.put("xls", "application/vnd.ms-excel");
mimeTypes.put("xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
mimeTypes.put("jpg", "image/jpeg");
mimeTypes.put("jpeg", "image/jpeg");
mimeTypes.put("png", "image/png");
mimeTypes.put("gif", "image/gif");
mimeTypes.put("txt", "text/plain");
mimeTypes.put("zip", "application/zip");
mimeTypes.put("rar", "application/x-rar-compressed");
return mimeTypes.getOrDefault(extension, "application/octet-stream");
}
/**
* 格式化文件大小
*/
private String formatFileSize(long size) {
if (size <= 0) return "0 B";
final String[] units = {"B", "KB", "MB", "GB", "TB"};
int digitGroups = (int) (Math.log10(size) / Math.log10(1024));
return String.format("%.2f %s", size / Math.pow(1024, digitGroups), units[digitGroups]);
}
}
断点续传
实现支持断点续传的下载功能:
/**
* 支持断点续传的下载
*/
@GetMapping("/resumeDownload")
public void resumeDownload(
HttpServletRequest request,
HttpServletResponse response,
@RequestParam("filename") String filename) throws Exception {
String uploadDir = request.getSession().getServletContext().getRealPath("/upload/");
File file = new File(uploadDir, filename);
if (!file.exists() || !file.isFile()) {
response.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
long fileLength = file.length();
long start = 0;
long end = fileLength - 1;
// 解析Range请求头
String range = request.getHeader("Range");
if (range != null && range.startsWith("bytes=")) {
String[] ranges = range.substring(6).split("-");
start = Long.parseLong(ranges[0]);
if (ranges.length > 1 && !ranges[1].isEmpty()) {
end = Long.parseLong(ranges[1]);
}
}
// 计算内容长度
long contentLength = end - start + 1;
// 设置响应头
response.setContentType("application/octet-stream");
response.setHeader("Accept-Ranges", "bytes");
response.setHeader("Content-Length", String.valueOf(contentLength));
response.setHeader("Content-Range", "bytes " + start + "-" + end + "/" + fileLength);
String encodedFilename = encodeFilename(request, filename);
response.setHeader("Content-Disposition", "attachment; filename=\"" + encodedFilename + "\"");
// 如果是部分内容请求
if (range != null) {
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
}
// 传输文件内容
try (RandomAccessFile raf = new RandomAccessFile(file, "r");
OutputStream out = response.getOutputStream()) {
raf.seek(start);
byte[] buffer = new byte[4096];
long remaining = contentLength;
while (remaining > 0) {
int bytesToRead = (int) Math.min(buffer.length, remaining);
int bytesRead = raf.read(buffer, 0, bytesToRead);
if (bytesRead == -1) {
break;
}
out.write(buffer, 0, bytesRead);
remaining -= bytesRead;
}
}
}
批量下载
批量下载功能已在上面的代码中实现,这里补充前端实现:
// 批量下载的前端实现
function initBatchDownload() {
// 全选/反选功能
$('#selectAll').change(function() {
$('input[name="files"]').prop('checked', this.checked);
});
// 批量下载按钮
$('#batchDownloadBtn').click(function() {
var selectedFiles = [];
$('input[name="files"]:checked').each(function() {
selectedFiles.push($(this).val());
});
if (selectedFiles.length === 0) {
alert('请选择要下载的文件');
return;
}
// 创建表单并提交
var form = $('<form>', {
method: 'POST',
action: contextPath + '/batchDownload'
});
selectedFiles.forEach(function(file) {
form.append($('<input>', {
type: 'hidden',
name: 'files',
value: file
}));
});
form.appendTo('body').submit().remove();
});
}
安全性考虑
文件类型限制
/**
* 文件类型安全检查
*/
public class FileSecurityUtil {
// 允许的文件扩展名白名单
private static final Set<String> ALLOWED_EXTENSIONS = new HashSet<>(Arrays.asList(
"jpg", "jpeg", "png", "gif", "bmp", "pdf",
"doc", "docx", "xls", "xlsx", "ppt", "pptx",
"txt", "zip", "rar", "7z"
));
// 禁止的文件扩展名黑名单
private static final Set<String> BLOCKED_EXTENSIONS = new HashSet<>(Arrays.asList(
"exe", "bat", "cmd", "sh", "php", "jsp",
"asp", "aspx", "js", "jar", "war"
));
/**
* 检查文件是否安全
*/
public static boolean isFileSafe(String filename) {
if (filename == null || filename.isEmpty()) {
return false;
}
// 获取文件扩展名
String extension = getFileExtension(filename).toLowerCase();
// 检查黑名单
if (BLOCKED_EXTENSIONS.contains(extension)) {
return false;
}
// 检查白名单
return ALLOWED_EXTENSIONS.contains(extension);
}
/**
* 检查文件内容类型
*/
public static boolean isContentTypeSafe(MultipartFile file) {
String contentType = file.getContentType();
// 检查MIME类型
if (contentType == null || contentType.contains("text/html") ||
contentType.contains("text/javascript")) {
return false;
}
// 可以使用Apache Tika进行更深入的内容检测
return true;
}
/**
* 生成安全的文件名
*/
public static String sanitizeFilename(String filename) {
// 移除路径分隔符和特殊字符
return filename.replaceAll("[/\\\\:*?\"<>|]", "_")
.replaceAll("\\.\\.", "_");
}
}
文件大小限制
在Spring配置中设置文件大小限制:
<!-- 文件上传大小限制配置 -->
<bean id="multipartResolver"
class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<!-- 单个文件最大大小:10MB -->
<property name="maxUploadSizePerFile" value="10485760" />
<!-- 整个请求最大大小:50MB -->
<property name="maxUploadSize" value="52428800" />
<!-- 内存中最大大小:1MB,超过此大小将写入临时文件 -->
<property name="maxInMemorySize" value="1048576" />
</bean>
存储路径安全
/**
* 安全的文件存储管理
*/
@Component
public class FileStorageService {
@Value("${file.upload.path:/data/uploads}")
private String uploadPath;
/**
* 获取安全的存储路径
*/
public File getSecureStoragePath(String filename) throws IOException {
// 规范化文件名
String safeFilename = FileSecurityUtil.sanitizeFilename(filename);
// 创建日期目录
SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd");
String dateFolder = sdf.format(new Date());
// 构建完整路径
File directory = new File(uploadPath, dateFolder);
if (!directory.exists()) {
directory.mkdirs();
}
// 返回文件对象
File file = new File(directory, safeFilename);
// 确保文件路径在上传目录内(防止目录遍历)
if (!file.getCanonicalPath().startsWith(directory.getCanonicalPath())) {
throw new SecurityException("非法的文件路径");
}
return file;
}
/**
* 定期清理过期文件
*/
@Scheduled(cron = "0 0 2 * * ?")
public void cleanupExpiredFiles() {
// 清理30天前的临时文件
long thirtyDaysAgo = System.currentTimeMillis() - (30L * 24 * 60 * 60 * 1000);
File uploadDir = new File(uploadPath, "temp");
if (uploadDir.exists()) {
File[] files = uploadDir.listFiles();
if (files != null) {
for (File file : files) {
if (file.lastModified() < thirtyDaysAgo) {
file.delete();
}
}
}
}
}
}
文件名安全处理
防止文件名注入和路径遍历攻击:
/**
* 文件名安全处理工具
*/
public class FilenameUtils {
/**
* 验证文件名是否安全
*/
public static boolean isFilenameSafe(String filename) {
// 检查是否包含路径分隔符
if (filename.contains("/") || filename.contains("\\") ||
filename.contains("..")) {
return false;
}
// 检查是否包含特殊字符
String pattern = "^[a-zA-Z0-9._-]+$";
return filename.matches(pattern);
}
/**
* 生成唯一且安全的文件名
*/
public static String generateSafeFilename(String originalFilename) {
String extension = getFileExtension(originalFilename);
String timestamp = String.valueOf(System.currentTimeMillis());
String uuid = UUID.randomUUID().toString().replace("-", "");
return String.format("%s_%s.%s", timestamp, uuid, extension);
}
}
性能优化
异步上传
使用Spring的异步支持实现文件异步上传:
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean
public Executor asyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(5);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("FileUpload-");
executor.initialize();
return executor;
}
}
@Service
public class AsyncFileService {
@Async
public CompletableFuture<String> uploadFileAsync(MultipartFile file, String uploadPath) {
try {
// 模拟耗时操作
Thread.sleep(1000);
// 保存文件
String filename = generateUniqueFilename(file.getOriginalFilename());
File destFile = new File(uploadPath, filename);
file.transferTo(destFile);
return CompletableFuture.completedFuture(filename);
} catch (Exception e) {
return CompletableFuture.failedFuture(e);
}
}
}
分片上传
前端分片上传实现:
function uploadLargeFile(file) {
const chunkSize = 5 * 1024 * 1024; // 5MB per chunk
const totalChunks = Math.ceil(file.size / chunkSize);
const identifier = generateUUID();
let currentChunk = 0;
function uploadNextChunk() {
if (currentChunk >= totalChunks) {
console.log('Upload complete');
return;
}
const start = currentChunk * chunkSize;
const end = Math.min(start + chunkSize, file.size);
const chunk = file.slice(start, end);
const formData = new FormData();
formData.append('file', chunk);
formData.append('chunkNumber', currentChunk + 1);
formData.append('totalChunks', totalChunks);
formData.append('identifier', identifier);
formData.append('filename', file.name);
$.ajax({
url: '/chunk/upload',
type: 'POST',
data: formData,
processData: false,
contentType: false,
success: function(response) {
currentChunk++;
updateProgress(currentChunk, totalChunks);
if (response.status === 'uploading') {
uploadNextChunk();
} else if (response.status === 'complete') {
onUploadComplete(response);
}
},
error: function() {
onUploadError(currentChunk);
}
});
}
uploadNextChunk();
}
function updateProgress(current, total) {
const percent = (current / total) * 100;
$('#uploadProgress').css('width', percent + '%');
$('#uploadProgress').text(Math.round(percent) + '%');
}
function generateUUID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = Math.random() * 16 | 0;
const v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
CDN加速
配置CDN加速文件下载:
@Configuration
public class CdnConfig {
@Value("${cdn.enable:false}")
private boolean cdnEnabled;
@Value("${cdn.domain:}")
private String cdnDomain;
/**
* 获取文件访问URL
*/
public String getFileUrl(String filename) {
if (cdnEnabled && StringUtils.hasText(cdnDomain)) {
return cdnDomain + "/files/" + filename;
} else {
return "/download?filename=" + filename;
}
}
}
最佳实践
1. 使用配置文件管理上传参数
# application.properties
file.upload.path=/data/uploads
file.upload.max-size=10MB
file.upload.allowed-types=jpg,jpeg,png,gif,pdf,doc,docx
file.upload.temp-dir=/data/temp
file.upload.cleanup.enabled=true
file.upload.cleanup.max-age=30
2. 实现文件元数据管理
@Entity
@Table(name = "file_metadata")
public class FileMetadata {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "original_name")
private String originalName;
@Column(name = "stored_name")
private String storedName;
@Column(name = "file_size")
private Long fileSize;
@Column(name = "content_type")
private String contentType;
@Column(name = "upload_time")
private Date uploadTime;
@Column(name = "uploader")
private String uploader;
@Column(name = "download_count")
private Integer downloadCount = 0;
@Column(name = "file_hash")
private String fileHash;
// getters and setters...
}
3. 添加病毒扫描
@Component
public class VirusScanService {
/**
* 扫描文件是否包含病毒
*/
public boolean scanFile(File file) {
// 集成ClamAV或其他防病毒引擎
// 这里是示例代码
try {
ProcessBuilder pb = new ProcessBuilder(
"clamscan", "--no-summary", file.getAbsolutePath()
);
Process process = pb.start();
int exitCode = process.waitFor();
// 0 = no virus, 1 = virus found
return exitCode == 0;
} catch (Exception e) {
// 扫描失败,保守起见返回false
return false;
}
}
}
4. 实现存储策略
/**
* 文件存储策略接口
*/
public interface FileStorageStrategy {
String store(MultipartFile file) throws IOException;
Resource load(String filename) throws IOException;
void delete(String filename) throws IOException;
}
/**
* 本地存储实现
*/
@Component
@Profile("local")
public class LocalFileStorage implements FileStorageStrategy {
// 实现代码...
}
/**
* 云存储实现(如阿里云OSS)
*/
@Component
@Profile("cloud")
public class CloudFileStorage implements FileStorageStrategy {
// 实现代码...
}
常见问题
1. 文件上传大小限制问题
问题:上传大文件时报错”Maximum upload size exceeded”
解决方案:
<!-- 在web.xml中配置 -->
<filter>
<filter-name>MultipartFilter</filter-name>
<filter-class>org.springframework.web.multipart.support.MultipartFilter</filter-class>
<init-param>
<param-name>multipartResolverBeanName</param-name>
<param-value>multipartResolver</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>MultipartFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
2. 中文文件名乱码
问题:下载文件时中文名称显示乱码
解决方案:
- 确保请求和响应的字符编码设置正确
- 根据不同浏览器使用不同的编码方式
- 使用URL编码处理特殊字符
3. 并发上传问题
问题:多用户同时上传可能造成文件名冲突
解决方案:
- 使用UUID生成唯一文件名
- 添加时间戳前缀
- 使用数据库管理文件元信息
4. 内存溢出问题
问题:上传大文件时出现OutOfMemoryError
解决方案:
- 设置合适的maxInMemorySize
- 使用流式处理而非一次性读取
- 实现分片上传
5. 安全漏洞防护
常见漏洞:
- 文件上传漏洞(上传可执行文件)
- 路径遍历漏洞
- 文件包含漏洞
防护措施:
- 严格的文件类型校验
- 文件内容检测
- 安全的文件名处理
- 独立的文件存储目录
- 禁止直接访问上传文件
相关文章
相关章节
Spring MVC 相关
- Web程序设计笔记15——第十一章:Spring MVC
- Web程序设计笔记16——第十二章:Spring MVC 的核心类和注解
- Spring MVC 拦截器完整指南