Skip to content

如何实现一个文件上传功能

这么简单的问题又来了,估计你会觉得面试官看不起谁?这不就是个送分题吗?其实他可能是个送命题!

简答:

实现一个文件上传功能通常涉及以下几个步骤:

  1. 前端页面 :创建一个File输入+一个按钮提交的 HTML 页面。
  2. 后端逻辑 :处理前端发送的请求。
    • 将这个文件存到某个角落
    • 然后告诉前端 你成功了!

好的可以回家等通知了,因为这是小学生的回答思路。。。

那要如何回答才算OK呢?关键看你面的岗位,具体要业务以及给多长时间开发。

作为高级或者架构,你可从如下几个方面聊。

整体架构

采用分层架构设计,分为表示层、业务逻辑层、数据访问层和持久层:

  • 表示层 :提供文件上传的界面,允许用户选择文件并提交上传请求。
  • 业务逻辑层 :处理文件上传的相关业务逻辑,包括文件类型验证、文件大小限制、文件存储路径生成等。
  • 数据访问层 :负责将文件信息(如文件名、文件路径、上传时间等)存储到数据库中。
  • 持久层 :用于存储上传的文件,一般将文件存储在服务器的文件系统中,也可考虑使用分布式文件系统或对象存储服务。

安全设计

  • 文件类型验证 :严格验证上传文件的类型,只允许特定类型的文件上传,防止恶意文件上传。
  • 文件内容扫描 :对上传的文件内容进行扫描,检查是否包含恶意代码或病毒。
  • 文件名处理 :对上传文件的文件名进行处理,避免文件名中包含特殊字符或路径信息,防止路径遍历攻击。
  • 权限控制 :设置文件存储目录的权限,确保只有授权用户才能访问和修改上传的文件。

技术选型

  • 前端 :可使用 HTML、CSS、JavaScript 构建文件上传页面,也可结合前端框架如 Vue.js、React 等提高开发效率。
  • 后端 :采用 Spring Boot 框架,利用其强大的依赖注入、AOP 等特性,简化开发。
  • 存储 :将文件存储在服务器的文件系统中,也可使用分布式文件系统如 FastDFS 或对象存储服务如阿里云 OSS。

系统流程

  1. 用户请求上传页面 :用户在浏览器中输入文件上传系统的网址,请求上传页面。服务器接收到请求后,返回上传页面给用户。
  2. 用户选择文件 :用户在上传页面中选择要上传的文件。
  3. 前端验证 :前端对用户选择的文件进行基本验证,如文件类型、文件大小等。验证通过后,用户点击上传按钮,将文件提交到后端。
  4. 后端接收文件 :后端接收到文件上传请求后,对文件进行进一步的安全检查,如文件类型验证、文件内容扫描等。
  5. 文件存储 :将文件存储到服务器的文件系统或分布式文件系统中,并生成唯一的文件存储路径。
  6. 信息存储 :将文件的相关信息(如文件名、文件路径、上传时间等)存储到数据库中。
  7. 返回上传结果 :后端将上传结果返回给前端,前端根据返回结果更新页面显示,如显示上传成功消息或失败原因。

性能优化

  • 分块上传 :对于大文件上传,可采用分块上传的方式,将文件分成多个小块依次上传,提高上传效率和可靠性。
  • 并发控制 :限制同时上传的文件数量,防止服务器资源被过度占用。
  • 缓存机制 :使用缓存存储频繁访问的文件信息,减少对数据库的访问。

扩展性设计

  • 支持多种存储方式 :除了本地文件系统存储外,支持分布式文件系统或对象存储服务,方便根据实际需求进行扩展。
  • 微服务架构 :将文件上传系统设计为一个独立的微服务,便于与其他系统集成和扩展。

实现代码

前端代码(HTML)

HTML

html
<!DOCTYPE html>
<html>
<head>
    <title>File Upload</title>
</head>
<body>
    <h2>File Upload</h2>
    <form id="uploadForm" enctype="multipart/form-data">
        <input type="file" id="file" name="file" required><br><br>
        <button type="submit">Upload</button>
    </form>
    <p id="message"></p>

    <script>
        document.getElementById('uploadForm').addEventListener('submit', function(event) {
            event.preventDefault();
            const file = document.getElementById('file').files[0];
            const formData = new FormData();
            formData.append('file', file);

            fetch('/api/upload', {
                method: 'POST',
                body: formData,
            })
            .then(response => response.json())
            .then(data => {
                document.getElementById('message').innerText = data.message;
            })
            .catch(error => console.error('Error:', error));
        });
    </script>
</body>
</html>

后端代码(Java Spring Boot)

  1. 添加依赖 :在 pom.xml 文件中添加 Spring Boot 相关的文件上传依赖。
xml
<dependencies>
    <!-- Other dependencies -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- Other dependencies -->
</dependencies>
  1. 文件上传控制器(FileUploadController)
java
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;
import java.util.UUID;
/**
 * @Auth:TianMing
 * @Description: 文件上传控制器
 */
@RestController
@RequestMapping("/api")
public class FileUploadController {

    private final String uploadDir = "/uploads/";

    @PostMapping("/upload")
    public ResponseEntity<?> uploadFile(@RequestParam("file") MultipartFile file) {
        // Validate file type
        String fileType = file.getContentType();
        if (!isValidFileType(fileType)) {
            return ResponseEntity.badRequest().body("Invalid file type");
        }

        // Validate file size
        long fileSize = file.getSize();
        if (fileSize > 10 * 1024 * 1024) { // 10MB limit
            return ResponseEntity.badRequest().body("File size exceeds limit");
        }

        // Generate unique file name
        String fileName = UUID.randomUUID().toString() + "." + getFileExtension(file.getOriginalFilename());

        // Save file to server
        File uploadFile = new File(uploadDir + fileName);
        try {
            file.transferTo(uploadFile);
        } catch (IOException e) {
            return ResponseEntity.internalServerError().body("File upload failed");
        }

        // Save file information to database
        FileEntity fileEntity = new FileEntity();
        fileEntity.setFileName(file.getOriginalFilename());
        fileEntity.setFilePath(uploadDir + fileName);
        fileEntity.setFileSize(fileSize);
        fileEntity.setFileType(fileType);
        fileEntity.setUploadTime(System.currentTimeMillis());
        fileRepository.save(fileEntity);

        return ResponseEntity.ok().body("File uploaded successfully");
    }

    private boolean isValidFileType(String fileType) {
        // Define allowed file types
        String[] allowedTypes = {"image/jpeg", "image/png", "application/pdf"};
        for (String type : allowedTypes) {
            if (type.equals(fileType)) {
                return true;
            }
        }
        return false;
    }

    private String getFileExtension(String fileName) {
        int dotIndex = fileName.lastIndexOf(".");
        if (dotIndex == -1) {
            return "";
        }
        return fileName.substring(dotIndex + 1);
    }
}
  1. 文件实体类(FileEntity)
java
import javax.persistence.*;
/**
 * @Auth:TianMing
 * @Description: 文件实体类
 */
@Entity
public class FileEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String fileName;
    private String filePath;
    private long fileSize;
    private String fileType;
    private long uploadTime;

    // Getters and setters
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getFileName() {
        return fileName;
    }

    public void setFileName(String fileName) {
        this.fileName = fileName;
    }

    public String getFilePath() {
        return filePath;
    }

    public void setFilePath(String filePath) {
        this.filePath = filePath;
    }

    public long getFileSize() {
        return fileSize;
    }

    public void setFileSize(long fileSize) {
        this.fileSize = fileSize;
    }

    public String getFileType() {
        return fileType;
    }

    public void setFileType(String fileType) {
        this.fileType = fileType;
    }

    public long getUploadTime() {
        return uploadTime;
    }

    public void setUploadTime(long uploadTime) {
        this.uploadTime = uploadTime;
    }
}
  1. 文件仓库接口(FileRepository)
java
import org.springframework.data.jpa.repository.JpaRepository;

public interface FileRepository extends JpaRepository<FileEntity, Long> {
}

安全增强

  • 文件类型验证

理论上又来了,前端文件类型,文件大小,文件数量等校验,但实际上只能防小白,对于安全当它不存在。

在后端代码中,通过 isValidFileType 方法严格验证上传文件的类型,只允许特定类型的文件上传,防止恶意文件上传。

还可加强类型校验,直接查看文件字节码的起始和结尾是否与所需格式匹配。对于某些特定类型的文件(如Office文档、PDF等),

可以使用相应的库来解析文件结构,检查是否存在异常的结构特征或嵌入的恶意代码。

  • 文件内容扫描

集成病毒扫描工具,对上传的文件内容进行扫描(例如,使用 Java 运行时执行 ClamAV 的命令行工具),检查是否包含恶意代码或病毒。

对文件名进行处理,去除路径信息,防止攻击者通过构造特殊的文件名来访问或覆盖服务器上的敏感文件。

当然常见操作还有就是将上传的文件单独丢到一台服务(隔离查杀),有毒跟你项目关系不大。OSS也是一个不错的选择。

  • 文件名处理

对上传文件的文件名进行处理,避免文件名中包含特殊字符或路径信息(如 ../../\ 等),防止路径遍历攻击。

对文件名长度进行限制,防止过长的文件名导致存储问题或性能问题。

使用唯一标识符(如 UUID)作为文件名,避免文件名冲突,并防止通过文件名猜测文件内容。

  • 权限控制 :设置文件存储目录的权限,确保只有授权用户才能访问和修改上传的文件。在服务器上,将文件存储目录的权限设置为仅允许应用程序用户读写。

关于文件名和路径附

几种具体方案:

  • 方案一:使用唯一标识符作为文件名
    • 步骤
      • 去除文件名中的特殊字符。
      • 生成一个唯一的标识符(如 UUID)作为文件名。
      • 将文件保存到固定的上传目录。
      • 将文件的原始名称和生成的唯一文件名存储到数据库中。
    • 优点:避免文件名冲突,防止通过文件名猜测文件内容。
    • 缺点:需要存储文件的原始名称和生成的唯一文件名,增加数据库存储量。
  • 方案二:使用哈希值作为文件名
    • 步骤
      • 去除文件名中的特殊字符。
      • 计算文件内容的哈希值(如 MD5、SHA-1)。
      • 将哈希值作为文件名保存文件。
      • 将文件的原始名称和哈希值存储到数据库中。
    • 优点:避免文件名冲突,防止通过文件名猜测文件内容,同时可以通过哈希值验证文件的完整性。
    • 缺点:需要计算文件的哈希值,增加一定的计算开销。
  • 方案三:使用时间戳和随机数生成文件名
    • 步骤
      • 去除文件名中的特殊字符。
      • 生成一个包含时间戳和随机数的唯一文件名。
      • 将文件保存到固定的上传目录。
      • 将文件的原始名称和生成的文件名存储到数据库中。
    • 优点:避免文件名冲突,具有一定的随机性。
    • 缺点:生成的文件名可能不够唯一,需要考虑冲突处理机制。
  • 方案四:保留原始文件名但进行编码
    • 步骤
      • 去除文件名中的特殊字符。
      • 对文件名进行编码(如 URL 编码、Base64 编码)。
      • 将编码后的文件名作为存储的文件名。
      • 将文件保存到固定的上传目录。
    • 优点:保留了原始文件名的信息,方便用户识别。
    • 缺点:编码后的文件名可能较长,且需要进行解码才能获取原始文件名。

以下是一个示例代码,用于处理文件名及路径:

java
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.UUID;
/**
 * @Auth:TianMing
 * @Description: 工具类
 */
public class FileUploadUtil {

    public static String generateUniqueFileName(String originalFileName) {
        // 去除文件名中的特殊字符
        String fileName = originalFileName.replaceAll("[\\\\/:*?\"<>|]", "");
        // 生成唯一标识符作为文件名
        String uniqueFileName = UUID.randomUUID().toString() + "-" + fileName;
        return uniqueFileName;
    }

    public static String calculateFileHash(File file) throws IOException {
        // 计算文件内容的哈希值
        byte[] fileContent = Files.readAllBytes(file.toPath());
        // 这里使用 SHA-256 哈希算法
        return java.security.MessageDigest.getInstance("SHA-256").digest(fileContent).toString();
    }

    public static String generateFileNameWithTimestamp(String originalFileName) {
        // 生成包含时间戳和随机数的唯一文件名
        long timestamp = System.currentTimeMillis();
        int randomNum = (int) (Math.random() * 1000);
        String fileName = originalFileName.replaceAll("[\\\\/:*?\"<>|]", "");
        return timestamp + "-" + randomNum + "-" + fileName;
    }

    public static String encodeFileName(String originalFileName) {
        // 对文件名进行 URL 编码
        return java.net.URLEncoder.encode(originalFileName, java.nio.charset.StandardCharsets.UTF_8);
    }

    public static void main(String[] args) throws IOException {
        // 示例用法
        String originalFileName = "example_file.txt";
        String uploadDir = "/uploads/";

        // 方案一:使用唯一标识符作为文件名
        String uniqueFileName = generateUniqueFileName(originalFileName);
        System.out.println("唯一文件名:" + uniqueFileName);

        // 方案二:使用哈希值作为文件名
        File tempFile = File.createTempFile("temp", ".txt");
        String fileHash = calculateFileHash(tempFile);
        System.out.println("文件哈希值:" + fileHash);

        // 方案三:使用时间戳和随机数生成文件名
        String fileNameWithTimestamp = generateFileNameWithTimestamp(originalFileName);
        System.out.println("带时间戳的文件名:" + fileNameWithTimestamp);

        // 方案四:保留原始文件名但进行编码
        String encodedFileName = encodeFileName(originalFileName);
        System.out.println("编码后的文件名:" + encodedFileName);

        // 创建上传目录
        File uploadDirectory = new File(uploadDir);
        if (!uploadDirectory.exists()) {
            uploadDirectory.mkdirs();
        }

        // 保存文件到服务器(示例)
        Path filePath = Paths.get(uploadDir + uniqueFileName);
        Files.write(filePath, "文件内容".getBytes());
    }
}

性能优化

  • 分块上传 :对于大文件上传,可采用分块上传的方式,将文件分成多个小块依次上传,提高上传效率和可靠性。在后端,可以使用 Spring Boot 的 @RequestPart 注解来处理分块上传的文件。
  • 并发控制 :在后端代码中,限制同时上传的文件数量,防止服务器资源被过度占用。可以通过配置线程池或使用半导体等机制来实现并发控制。
  • 缓存机制 :使用缓存存储频繁访问的文件信息,减少对数据库的访问。例如,可以使用 Redis 缓存文件的元数据,如文件名、文件路径、文件大小等。

扩展性设计

  • 支持多种存储方式 :除了本地文件系统存储外,支持分布式文件系统或对象存储服务,方便根据实际需求进行扩展。例如,可以集成阿里云 OSS 或腾讯云 COS 等对象存储服务。
  • 微服务架构 :将文件上传系统设计为一个独立的微服务,便于与其他系统集成和扩展。通过 API 网关进行统一的请求路由和安全控制。

面试官可能深入提问及回答

安全方面

  • 如何防止恶意文件上传? 除了严格验证文件类型外,还应集成病毒扫描工具,对上传的文件内容进行扫描,检查是否包含恶意代码或病毒。同时,设置文件存储目录的权限,确保只有授权用户才能访问和修改上传的文件。
  • 如何防止路径遍历攻击? 对上传文件的文件名进行处理,避免文件名中包含特殊字符或路径信息。在生成文件存储路径时,使用 UUID 生成唯一的文件名,并结合文件的原始扩展名。

性能方面

  • 如何处理大文件上传? 采用分块上传的方式,将文件分成多个小块依次上传,提高上传效率和可靠性。在后端,可以使用 Spring Boot 的 @RequestPart 注解来处理分块上传的文件。

扩展性方面

  • 如何支持多种存储方式? 可以将文件上传系统设计为一个独立的微服务,通过配置文件或环境变量来指定存储方式。例如,可以集成阿里云 OSS 或腾讯云 COS 等对象存储服务,方便根据实际需求进行扩展。

数据一致性方面

  • 如何保证文件信息的存储与文件的实际存储一致? 在后端代码中,先将文件存储到服务器的文件系统或分布式文件系统中,然后将文件信息存储到数据库中。如果文件存储失败,则直接返回错误信息,不进行数据库操作。如果文件信息存储失败,则删除已存储的文件,确保文件信息的存储与文件的实际存储一致。

更多文件上传性能方面深入的细节 请移步

网盘系统设计:万亿 GB 网盘如何实现秒传与限速

以及 OA办公项目优化 中关于文件方面的问答

更新: 2025-04-28 16:45:00
原文: https://www.yuque.com/tulingzhouyu/db22bv/df1cwzic7lp7m84y