文件上传

import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.util.StringUtils;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.path.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;

@RestController
@RequestMapping("/api/file")
public class FileController {

    private final Path rootLocation = Paths.get("uploads");

    public FileController() {
        // 确保上传目录存在
        try {
            Files.createDirectories(rootLocation);
        } catch (IOException e) {
            throw new RuntimeException("无法创建上传目录", e);
        }
    }

    @PostMapping("/upload")
    public ResponseEntity<String> uploadFile(@RequestParam("file") MultipartFile file) {
        if (file.isEmpty()) {
            return ResponseEntity.badRequest().body("文件为空");
        }

        try {
            String filename = StringUtils.cleanPath(file.getOriginalFilename());
            // 防止路径遍历攻击
            if (filename.contains("..")) {
                return ResponseEntity.badRequest().body("非法文件名");
            }

            Path target = this.rootLocation.resolve(filename);
            Files.copy(file.getInputStream(), target, StandardCopyOption.REPLACE_EXISTING);

            return ResponseEntity.ok("文件上传成功: " + filename);
        } catch (IOException e) {
            return ResponseEntity.internalServerError().body("上传失败: " + e.getMessage());
        }
    }
}

详细解释每个部分:

类定义和依赖注入

@RestController
@RequestMapping("/api/file")
public class FileController {
  • @RestController: 标记为RESTful控制器,自动将返回值序列化为JSON
  • @RequestMapping("/api/file"): 定义基础路径,所有方法都在/api/file

文件存储配置

private final Path rootLocation = Paths.get("uploads");
  • 定义文件存储目录为项目根目录下的uploads文件夹

构造函数

public FileController() {
    try {
        Files.createDirectories(rootLocation);
    } catch (IOException e) {
        throw new RuntimeException("无法创建上传目录", e);
    }
}
  • 在控制器创建时自动创建上传目录
  • 如果创建失败则抛出运行时异常

文件上传方法

方法签名

@PostMapping("/upload")
public ResponseEntity<String> uploadFile(@RequestParam("file") MultipartFile file)
  • @PostMapping("/upload"): 处理POST请求到/api/file/upload
  • @RequestParam("file"): 接收名为"file"的文件参数
  • MultipartFile: Spring提供的文件上传接口

文件验证

if (file.isEmpty()) {
    return ResponseEntity.badRequest().body("文件为空");
}
  • 检查文件是否为空

文件名安全处理

String filename = StringUtils.cleanPath(file.getOriginalFilename());
if (filename.contains("..")) {
    return ResponseEntity.badRequest().body("非法文件名");
}
  • cleanPath(): 清理路径中的非法字符
  • 防止路径遍历攻击(如../../../etc/passwd

文件保存

Path target = this.rootLocation.resolve(filename);
Files.copy(file.getInputStream(), target, StandardCopyOption.REPLACE_EXISTING);
  • resolve(): 将文件名拼接到目录路径后
  • Files.copy(): 将文件流复制到目标路径
  • REPLACE_EXISTING: 如果文件已存在则覆盖

完整请求流程

  1. 客户端发送POST请求/api/file/upload
  2. Spring接收multipart/form-data格式的文件
  3. 服务器验证文件是否为空
  4. 安全检查文件名防止路径遍历
  5. 保存文件到uploads目录
  6. 返回响应成功或错误信息

使用示例

# 使用curl上传文件
curl -X POST -F "file=@test.jpg" http://localhost:8080/api/file/upload

文件下载

@GetMapping("/download/{filename:.+}")
public ResponseEntity<Resource> downloadFile(@PathVariable String filename) {
    try {
        if (filename.contains("..")) {
            // 防止路径遍历
            throw new RuntimeException("非法文件名");
        }

        Path file = rootLocation.resolve(filename).normalize();
        Resource resource = new org.springframework.core.io.UrlResource(file.toUri());

        if (resource.exists() && resource.isReadable()) {
            return ResponseEntity.ok()
                    .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + resource.getFilename() + "\"")
                    .body(resource);
        } else {
            return ResponseEntity.notFound().build();
        }
    } catch (Exception e) {
        return ResponseEntity.internalServerError().build();
    }
}

这是一个文件下载的REST接口,我来详细解释每个部分:

方法签名和映射

@GetMapping("/download/{filename:.+}")
public ResponseEntity<Resource> downloadFile(@PathVariable String filename)
  • @GetMapping("/download/{filename:.+}"):

    • 处理GET请求到/api/file/download/{filename}
    • {filename:.+}: 特殊的路径变量模式,.后面的.+确保能匹配包含点号的文件名
    • 没有.+时,Spring会截断最后一个点号后的内容(如file.txt只会匹配到file
  • @PathVariable String filename: 从URL路径中提取文件名

安全验证

if (filename.contains("..")) {
    throw new RuntimeException("非法文件名");
}

防止路径遍历攻击

  • 检查文件名是否包含..(上级目录指示符)
  • 阻止恶意请求如:/download/../../../etc/passwd
  • 这是关键的安全措施

文件路径解析

Path file = rootLocation.resolve(filename).normalize();
Resource resource = new org.springframework.core.io.UrlResource(file.toUri());
  • rootLocation.resolve(filename): 将文件名拼接到上传目录路径
  • normalize(): 标准化路径,移除冗余的./../,进一步防止路径遍历
  • UrlResource: Spring提供的资源抽象,支持文件系统、URL等多种资源

文件存在性检查

if (resource.exists() && resource.isReadable())
  • exists(): 检查文件是否存在
  • isReadable(): 检查文件是否可读(权限检查)
  • 双重验证确保文件可访问

成功响应构建

return ResponseEntity.ok()
        .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + resource.getFilename() + "\"")
        .body(resource);
  • HttpHeaders.CONTENT_DISPOSITION: 设置内容处置头
  • "attachment; filename=\"filename\"":
    • attachment: 告诉浏览器下载文件而不是显示
    • filename: 指定下载时保存的文件名
  • body(resource): 将文件资源作为响应体

错误处理

} else {
    return ResponseEntity.notFound().build();  // 404 - 文件不存在
}
} catch (Exception e) {
    return ResponseEntity.internalServerError().build();  // 500 - 服务器错误
}

完整请求流程示例

请求

GET http://localhost:8080/api/file/download/document.pdf

处理过程

  1. Spring提取filename"document.pdf"
  2. 安全检查:"document.pdf".contains("..")false
  3. 构建路径:uploads/document.pdf
  4. 创建UrlResource对象
  5. 检查文件是否存在且可读
  6. 设置响应头,触发浏览器下载
  7. 返回文件内容

安全特性总结

  1. 路径遍历防护:检查..字符
  2. 路径标准化normalize()清理路径
  3. 文件验证:检查存在性和可读性
  4. 资源隔离:文件限制在uploads目录内
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐