Spring Boot中的文件上传:上传、验证和异常处理

问题描述 投票:0回答:3

我希望能够将图像上传到服务器,优雅地处理错误和异常,并以表单向用户显示错误消息,并且最好仅使用现有的准系统 Spring Boot 和 Thymeleaf 安装。

使用示例项目 gs-uploading-files 我可以使用 Spring Boot 和 Thymeleaf 将文件上传到服务器。在 application.properties 中我设置了

spring.http.multipart.max-file-size=1MB
spring.http.multipart.max-request-size=1MB
。 然而,当我上传大于 1MB 的文件时,一些安全和验证问题尚未解决。

  1. 任何文件都可以上传。例如,可以上传 html 文件并托管在服务器上。如何按类型限制文件?在发送请求之前可以在页面中验证它们吗?如果我有多种上传图像的方式,如何验证所有 MultipartFiles?

  2. 用户可以尝试上传大文件,超出 Spring 和嵌入式 Tomcat 的默认限制。这会导致 Spring 不处理

    org.springframework.web.multipart.MultipartException
    。在尝试上传之前如何验证文件大小?如果绕过这个问题,Spring 是否可以捕获任何文件上传异常,以便显示一条漂亮的错误消息?

  3. 默认的 Spring 错误页面并不用作所有异常的后备。 MultipartException 返回带有完整堆栈跟踪的 Tomcat 异常页面(请参阅日志 1)。


我尝试寻找并实施一组解决方案。

解决第 1 个问题的一个步骤是修改

handleFileUpload
检查内容类型,拒绝未通过此操作的文件:
!file.getContentType().toLowerCase().startsWith("image")
。这永远有效吗?恶意用户可以绕过这个吗?我如何检查每个 MultipartFile,以避免每次都要记住添加它?

@PostMapping("/")
public String handleFileUpload(@RequestParam("file") MultipartFile file, RedirectAttributes redirectAttributes)
        throws MultipartException, IllegalStateException {

    if (file != null && file.getContentType() != null && !file.getContentType().toLowerCase().startsWith("image"))
        throw new MultipartException("not img");

    storageService.store(file);
    redirectAttributes.addFlashAttribute("message",
            "You successfully uploaded " + file.getOriginalFilename() + "!");

    return "redirect:/";
}

添加

@ExceptionHandler
不起作用,它根本不会被调用。

@ExceptionHandler({ SizeLimitExceededException.class, MultipartException.class,
        java.lang.IllegalStateException.class })
public ModelAndView handleError(HttpServletRequest req, Exception e) {
    // error("Request: " + req.getRequestURL() + " raised " + ex);

    ModelAndView mav = new ModelAndView();
    mav.addObject("exception", e);
    mav.addObject("url", req.getRequestURL());
    mav.addObject("timestamp", new Date());
    mav.addObject("error", e.getClass());
    mav.addObject("message", e.getMessage());
    mav.addObject("status", HttpStatus.INTERNAL_SERVER_ERROR);
    mav.setViewName("error");
    return mav;
}

数字 3 可以通过所有异常的全局异常处理程序来解决。 (在这篇文章中有详细解释)。但是,我担心它可能会否决控制器级别处理程序。

package hello;

import java.util.Date;

import javax.servlet.http.HttpServletRequest;

import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.servlet.ModelAndView;

@ControllerAdvice
class GlobalDefaultExceptionHandler {
    public static final String DEFAULT_ERROR_VIEW = "error";

    @ExceptionHandler(value = Exception.class)
    public ModelAndView defaultErrorHandler(HttpServletRequest req, Exception e) throws Exception {
        // If the exception is annotated with @ResponseStatus rethrow it and let
        // the framework handle it - like the OrderNotFoundException example
        // at the start of this post.
        // AnnotationUtils is a Spring Framework utility class.
        if (AnnotationUtils.findAnnotation(e.getClass(), ResponseStatus.class) != null)
            throw e;

        // Otherwise setup and send the user to a default error-view.
        ModelAndView mav = new ModelAndView();
        mav.addObject("exception", e);
        mav.addObject("url", req.getRequestURL());
        mav.addObject("timestamp", new Date());
        mav.addObject("error", e.getClass());
        mav.addObject("message", e.getMessage());
        mav.addObject("status", HttpStatus.INTERNAL_SERVER_ERROR);
        mav.setViewName(DEFAULT_ERROR_VIEW);
        return mav;
    }
}

我尝试过这个答案,它处理异常但返回错误页面。我想返回原始页面并显示一条不错的错误消息。


日志1:

HTTP Status 500 - Request processing failed; nested exception is org.springframework.web.multipart.MultipartException: Could not parse multipart servlet request; nested exception is java.lang.IllegalStateException: org.apache.tomcat.util.http.fileupload.FileUploadBase$SizeLimitExceededException: the request was rejected because its size (1292555) exceeds the configured maximum (1048576)

type Exception report

message Request processing failed; nested exception is org.springframework.web.multipart.MultipartException: Could not parse multipart servlet request; nested exception is java.lang.IllegalStateException: org.apache.tomcat.util.http.fileupload.FileUploadBase$SizeLimitExceededException: the request was rejected because its size (1292555) exceeds the configured maximum (1048576)

description The server encountered an internal error that prevented it from fulfilling this request.

exception

org.springframework.web.util.NestedServletException: Request processing failed; nested exception is org.springframework.web.multipart.MultipartException: Could not parse multipart servlet request; nested exception is java.lang.IllegalStateException: org.apache.tomcat.util.http.fileupload.FileUploadBase$SizeLimitExceededException: the request was rejected because its size (1292555) exceeds the configured maximum (1048576)
    org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:982)
    org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:872)
    javax.servlet.http.HttpServlet.service(HttpServlet.java:648)
    org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:846)
    javax.servlet.http.HttpServlet.service(HttpServlet.java:729)
    org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)
    org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:99)
    org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    org.springframework.web.filter.HttpPutFormContentFilter.doFilterInternal(HttpPutFormContentFilter.java:89)
    org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:77)
    org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:197)
    org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
root cause

org.springframework.web.multipart.MultipartException: Could not parse multipart servlet request; nested exception is java.lang.IllegalStateException: org.apache.tomcat.util.http.fileupload.FileUploadBase$SizeLimitExceededException: the request was rejected because its size (1292555) exceeds the configured maximum (1048576)
    org.springframework.web.multipart.support.StandardMultipartHttpServletRequest.parseRequest(StandardMultipartHttpServletRequest.java:111)
    org.springframework.web.multipart.support.StandardMultipartHttpServletRequest.<init>(StandardMultipartHttpServletRequest.java:85)
    org.springframework.web.multipart.support.StandardServletMultipartResolver.resolveMultipart(StandardServletMultipartResolver.java:76)
    org.springframework.web.servlet.DispatcherServlet.checkMultipart(DispatcherServlet.java:1099)
    org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:932)
    org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:897)
    org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:970)
    org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:872)
    javax.servlet.http.HttpServlet.service(HttpServlet.java:648)
    org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:846)
    javax.servlet.http.HttpServlet.service(HttpServlet.java:729)
    org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)
    org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:99)
    org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    org.springframework.web.filter.HttpPutFormContentFilter.doFilterInternal(HttpPutFormContentFilter.java:89)
    org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:77)
    org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:197)
    org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
root cause

java.lang.IllegalStateException: org.apache.tomcat.util.http.fileupload.FileUploadBase$SizeLimitExceededException: the request was rejected because its size (1292555) exceeds the configured maximum (1048576)
    org.apache.catalina.connector.Request.parseParts(Request.java:2871)
    org.apache.catalina.connector.Request.parseParameters(Request.java:3176)
    org.apache.catalina.connector.Request.getParameter(Request.java:1110)
    org.apache.catalina.connector.RequestFacade.getParameter(RequestFacade.java:381)
    org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:70)
    org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:197)
    org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
root cause

org.apache.tomcat.util.http.fileupload.FileUploadBase$SizeLimitExceededException: the request was rejected because its size (1292555) exceeds the configured maximum (1048576)
    org.apache.tomcat.util.http.fileupload.FileUploadBase$FileItemIteratorImpl.<init>(FileUploadBase.java:811)
    org.apache.tomcat.util.http.fileupload.FileUploadBase.getItemIterator(FileUploadBase.java:256)
    org.apache.tomcat.util.http.fileupload.FileUploadBase.parseRequest(FileUploadBase.java:280)
    org.apache.catalina.connector.Request.parseParts(Request.java:2801)
    org.apache.catalina.connector.Request.parseParameters(Request.java:3176)
    org.apache.catalina.connector.Request.getParameter(Request.java:1110)
    org.apache.catalina.connector.RequestFacade.getParameter(RequestFacade.java:381)
    org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:70)
    org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:197)
    org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
note The full stack trace of the root cause is available in the Apache Tomcat/8.5.5 logs.

Apache Tomcat/8.5.5

java tomcat file-upload spring-boot thymeleaf
3个回答
11
投票

回复如何检查文件类型:我为此创建了一个自定义验证器。

首先,创建注释:

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {ImageFileValidator.class})
public @interface ValidImage {
    String message() default "Invalid image file";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

接下来,创建验证器本身:

import org.springframework.web.multipart.MultipartFile;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class ImageFileValidator implements ConstraintValidator<ValidImage, MultipartFile> {

    @Override
    public void initialize(ValidImage constraintAnnotation) {

    }

    @Override
    public boolean isValid(MultipartFile multipartFile, ConstraintValidatorContext context) {

        boolean result = true;

        String contentType = multipartFile.getContentType();
        if (!isSupportedContentType(contentType)) {
            context.disableDefaultConstraintViolation();
            context.buildConstraintViolationWithTemplate(
                    "Only PNG or JPG images are allowed.")
                   .addConstraintViolation();

            result = false;
        }

        return result;
    }

    private boolean isSupportedContentType(String contentType) {
        return contentType.equals("image/png")
                || contentType.equals("image/jpg")
                || contentType.equals("image/jpeg");
    }
}

最后应用注释:

public class CreateUserParameters {

    @NotNull
    @ValidImage
    private MultipartFile image;
...
}

我已经使用 Spring Boot 1.5.10(也使用 Thymeleaf)对此进行了测试

对于最大文件大小,我还希望看到一个与“标准错误机制”配合使用的解决方案,这样您就可以像其他字段错误一样显示错误,并且用户可以纠正他的错误。


4
投票

尝试在 application.properties 中添加以下内容来设置文件大小限制:

spring.http.multipart.max-file-size=256KB
spring.http.multipart.max-request-size=256KB

来源:https://spring.io/guides/gs/uploading-files/

编辑: 自 Spring boot 2.0 发布以来,属性名称已更改为:

spring.servlet.multipart.max-file-size=128KB
spring.servlet.multipart.max-request-size=128KB

注意区别

spring.http.
-->
spring.servlet.


0
投票

带有注释的 Spring Boot 文件内容类型验证:

CustomExceptionControllerAdvice.java

@ControllerAdvice
public class CustomExceptionControllerAdvice {

    @ExceptionHandler(MultipartException.class)
    void handleMultipartException(MultipartException ex,HttpServletResponse response) throws IOException {
        response.sendError(HttpStatus.BAD_REQUEST.value(),"Please select a file");
    }
  
    @ExceptionHandler(ConstraintViolationException.class)
    public void handleConstraintViolationException(ConstraintViolationException ex,HttpServletResponse response) throws IOException {
        response.sendError(HttpStatus.BAD_REQUEST.value());
    }

}

FileController.java

@RestController
@FieldDefaults(level = AccessLevel.PRIVATE,makeFinal=true)
@RequestMapping("/versions/1")
public class FileController {

    
    @PostMapping("/files")
    @ResponseStatus(HttpStatus.OK)
    public String createFile(@Validated @ValidFile @RequestPart("file") MultipartFile file {

        return file.getOriginalFilename()
    }

}

FileValidator.java

public class FileValidator implements ConstraintValidator<ValidFile, MultipartFile> {

    @Override
    public void initialize(ValidFile constraintAnnotation) {

    }

    @Override
    public boolean isValid(MultipartFile multipartFile, ConstraintValidatorContext context) {

        boolean result = true;

        String contentType = multipartFile.getContentType();
        if (!isSupportedContentType(contentType)) {
            result = false;
        }

        return result;
    }

    private boolean isSupportedContentType(String contentType) {
        return contentType.equals("text/xml")
                || contentType.equals("application/pdf")
                || contentType.equals("image/png")
                || contentType.equals("image/jpg")
                || contentType.equals("image/jpeg");
    }
}

ValidFile.java

@Documented
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {FileValidator.class})
public @interface ValidFile {
    String message() default "Only PDF,XML,PNG or JPG images are allowed";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}
© www.soinside.com 2019 - 2024. All rights reserved.