簡體   English   中英

Spring Boot 中的文件上傳:上傳、驗證和異常處理

[英]File upload in Spring Boot: Uploading, validation, and exception handling

我希望能夠將圖像上傳到服務器,優雅地處理錯誤和異常,並在表單中向用戶顯示錯誤消息,理想情況下只使用現有的准系統 Spring Boot 和 Thymeleaf 安裝。

使用示例項目gs-uploading-files我可以使用 Spring Boot 和 Thymeleaf 將文件上傳到服務器。 在 application.properties 我設置spring.http.multipart.max-file-size=1MBspring.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

回復如何檢查文件類型:我為此創建了一個自定義驗證器。

首先,創建一個注解:

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)測試過這個

對於最大文件大小,我還希望看到一個適用於“標准錯誤機制”的解決方案,以便您可以像其他字段錯誤一樣顯示錯誤,並且用戶可以糾正他的錯誤。

嘗試在 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.http. --> spring.servlet.

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM