Подтвердить что ты не робот

Как хорошо обрабатывать загрузку файлов MaxUploadSizeExceededException с помощью Spring Безопасность

Я использую Spring Web 4.0.5, Spring Security 3.2.4, Commons FileUpload 1.3.1, Tomcat 7, и я получаю уродливый MaxUploadSizeExceededException, когда превышен лимит размера загрузки, который приводит к ошибке "500 Internal Server Error". Я обрабатываю его с хорошим общим всплывающим окном, но я бы предпочел, чтобы мой контролер позаботился об этом, вернувшись к исходной форме с соответствующим сообщением о разъяснении.

Я видел один и тот же вопрос много раз, с несколькими решениями, которые могут работать, если не использовать Spring Security; ни один из тех, которые я пробовал, не работал у меня.

Проблема может заключаться в том, что при использовании Spring Security, CommonsMultipartResolver не добавляется как "multipartResolver" bean, а как "filterMultipartResolver":

@Bean(name="filterMultipartResolver")
CommonsMultipartResolver filterMultipartResolver() {
    CommonsMultipartResolver filterMultipartResolver = new CommonsMultipartResolver();
    filterMultipartResolver.setMaxUploadSize(MAXSIZE);
    return filterMultipartResolver;
}

Если я устанавливаю filterMultipartResolver.setResolveLazily(true);, это не имеет значения.

Если я подклассифицирую CommonsMultipartResolver своим собственным и переопределяю метод parseRequest() с чем-то, что ловушки MaxUploadSizeExceededException и возвращает пустой MultipartParsingResult, я получаю ошибку "403 Forbidden":

public class ExtendedCommonsMultipartResolver extends CommonsMultipartResolver {
    protected MultipartParsingResult parseRequest(HttpServletRequest request) throws MultipartException {
        String encoding = determineEncoding(request);
        try {
            return super.parseRequest(request);
        } catch (MaxUploadSizeExceededException e) {
            return parseFileItems(Collections.<FileItem> emptyList(), encoding);
        }
    }
}

Наконец, нет смысла в реализации какого-то локального или глобального ExceptionHandler, потому что он никогда не вызывается.

Если я не найду лучшего решения, я просто удалю ограничение размера загрузки и сам обработаю его в контроллере, с недостатком ожидания пользователя до завершения загрузки до появления сообщения об ошибке размер. Я даже мог бы игнорировать все это, потому что, являясь им образ в этом случае, я мог бы просто изменить его до правильных значений.

Тем не менее, я хотел бы увидеть решение этой проблемы.

Спасибо

EDIT:

Я добавляю трассировку стека по запросу. Это тот случай, когда генерируется 500.

May 30, 2014 12:47:17 PM org.apache.catalina.core.StandardWrapperValve invoke
SEVERE: Servlet.service() for servlet [dispatcher] in context with path [/site] threw exception
org.springframework.web.multipart.MaxUploadSizeExceededException: Maximum upload size of 1000000 bytes exceeded; nested exception is org.apache.commons.fileupload.FileUploadBase$SizeLimitExceededException: the request was rejected because its size (3403852) exceeds the configured maximum (1000000)
    at org.springframework.web.multipart.commons.CommonsMultipartResolver.parseRequest(CommonsMultipartResolver.java:162)
    at org.springframework.web.multipart.commons.CommonsMultipartResolver.resolveMultipart(CommonsMultipartResolver.java:142)
    at org.springframework.web.multipart.support.MultipartFilter.doFilterInternal(MultipartFilter.java:110)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:243)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:210)
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:222)
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:123)
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:502)
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:171)
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:100)
    at org.apache.catalina.valves.AccessLogValve.invoke(AccessLogValve.java:953)
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:118)
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:409)
    at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1044)
    at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:607)
    at org.apache.tomcat.util.net.JIoEndpoint$SocketProcessor.run(JIoEndpoint.java:315)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1110)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:603)
    at java.lang.Thread.run(Thread.java:722)
Caused by: org.apache.commons.fileupload.FileUploadBase$SizeLimitExceededException: the request was rejected because its size (3403852) exceeds the configured maximum (1000000)
    at org.apache.commons.fileupload.FileUploadBase$FileItemIteratorImpl.<init>(FileUploadBase.java:965)
    at org.apache.commons.fileupload.FileUploadBase.getItemIterator(FileUploadBase.java:310)
    at org.apache.commons.fileupload.FileUploadBase.parseRequest(FileUploadBase.java:334)
    at org.apache.commons.fileupload.servlet.ServletFileUpload.parseRequest(ServletFileUpload.java:115)
    at org.springframework.web.multipart.commons.CommonsMultipartResolver.parseRequest(CommonsMultipartResolver.java:158)
    ... 19 more
4b9b3361

Ответ 1

Вы можете обработать MaxUploadSizeExceededException, добавив дополнительный фильтр, чтобы поймать исключение и перенаправить на страницу с ошибкой. Например, вы можете создать фильтр MultipartExceptionHandler следующим образом:

public class MultipartExceptionHandler extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request,
            HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        try {
            filterChain.doFilter(request, response);
        } catch (MaxUploadSizeExceededException e) {
            handle(request, response, e);
        } catch (ServletException e) {
            if(e.getRootCause() instanceof MaxUploadSizeExceededException) {
                handle(request, response, (MaxUploadSizeExceededException) e.getRootCause());
            } else {
                throw e;
            }
        }
    }

    private void handle(HttpServletRequest request,
            HttpServletResponse response, MaxUploadSizeExceededException e) throws ServletException, IOException {

        String redirect = UrlUtils.buildFullRequestUrl(request) + "?error";
        response.sendRedirect(redirect);
    }

}

ПРИМЕЧАНИЕ. Это перенаправление делает предположение о вашей форме и загрузке. Возможно, вам придется изменить, куда перенаправить. В частности, если вы придерживаетесь шаблона вашей формы, находящегося в GET, и обрабатывается в POST, это будет работать.

Затем вы можете добавить этот фильтр перед MultipartFilter. Например, если вы используете web.xml, вы увидите что-то вроде этого:

<filter>
    <filter-name>meh</filter-name>
    <filter-class>org.example.web.MultipartExceptionHandler</filter-class>
</filter>
<filter>
    <description>
        Allows the application to accept multipart file data.
    </description>
    <display-name>springMultipartFilter</display-name>
    <filter-name>springMultipartFilter</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>
    <description>
        Secures access to web resources using the Spring Security framework.
    </description>
    <display-name>springSecurityFilterChain</display-name>
    <filter-name>springSecurityFilterChain</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>

<filter-mapping>
    <filter-name>meh</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
<filter-mapping>
    <filter-name>springMultipartFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
<filter-mapping>
    <filter-name>springSecurityFilterChain</filter-name>
    <url-pattern>/*</url-pattern>
    <dispatcher>ERROR</dispatcher>
    <dispatcher>REQUEST</dispatcher>
</filter-mapping>

В вашей форме вы можете обнаружить, произошла ли ошибка, проверив, присутствует ли ошибка параметра HTTP. Например, в JSP вы можете сделать следующее:

<c:if test="${param.error != null}">
    <p>Failed to upload...too big</p>
</c:if>

PS: я создал SEC-2614, чтобы обновить документацию, чтобы обсудить обработку ошибок

Ответ 2

Я знаю, что опаздываю на вечеринку, но я нашел гораздо более элегантное решение imho.

Вместо добавления фильтра для многочастного распознавателя просто добавьте throws MaxUploadSizeExceededException в свой метод контроллера и добавьте фильтр для DelegatingFilterProxy в web.xml, и вы можете добавить обработчик исключений прямо в ваш контроллер, не имея перенаправить запрос.

например:.

Метод (в контроллере):

@RequestMapping(value = "/uploadFile", method = RequestMethod.POST)
public ResponseEntity<String> uploadFile(MultipartHttpServletRequest request) throws MaxUploadSizeExceededException {
    //code
}

Обработчик исключений (в том же контроллере):

@ExceptionHandler(MaxUploadSizeExceededException.class)
public ResponseEntity handleSizeExceededException(HttpServletRequest request, Exception ex) {
    //code
}

Web.xml(спасибо Rob Winch):

<filter>
    <description>
        Secures access to web resources using the Spring Security framework.
    </description>
    <display-name>springSecurityFilterChain</display-name>
    <filter-name>springSecurityFilterChain</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
    <filter-name>springSecurityFilterChain</filter-name>
    <url-pattern>/*</url-pattern>
    <dispatcher>ERROR</dispatcher>
    <dispatcher>REQUEST</dispatcher>
</filter-mapping>

И это все, что вам нужно.

Ответ 3

Вещь springSecurityFilterChain должна быть добавлена ​​в фильтр после. Вот почему вы получаете статус 403. Здесь:

http://docs.spring.io/spring-security/site/docs/3.2.0.CI-SNAPSHOT/reference/html/csrf.html#csrf-multipartfilter

Я думаю, что после этого вы сможете поймать FileUploadBase.SizeLimitExceededException в аннотированном классе @ControllerAdvice, содержащем аннотированные методы @ExceptionHandler.

Ответ 4

Решение, которое я придумал, заключается в следующем:

  • Расширьте CommonsMultipartResolver, чтобы усвоить исключение. Я добавляю исключение в запрос только в том случае, если вы хотите использовать его в контроллере, но я не думаю, что он нужен

    package org.springframework.web.multipart.commons;
    
    import java.util.Collections;
    
    import javax.servlet.http.HttpServletRequest;
    
    import org.apache.commons.fileupload.FileItem;
    import org.springframework.web.multipart.MaxUploadSizeExceededException;
    import org.springframework.web.multipart.MultipartException;
    
    public class ExtendedCommonsMultipartResolver extends CommonsMultipartResolver {
        @Override
        protected MultipartParsingResult parseRequest(HttpServletRequest request) throws MultipartException {
            try {
                return super.parseRequest(request);
            } catch (MaxUploadSizeExceededException e) {
                request.setAttribute("MaxUploadSizeExceededException", e);
                return parseFileItems(Collections.<FileItem> emptyList(), null);
            }
        }
    }
    
  • Объявите свой резольвер в WebSecurityConfigurerAdapter вместо CommonsMultipartResolver (вы должны объявить filterMultipartResolver в любом случае, так что ничего нового здесь)

    @Bean(name="filterMultipartResolver")
    CommonsMultipartResolver filterMultipartResolver() {
        CommonsMultipartResolver filterMultipartResolver = new ExtendedCommonsMultipartResolver();
        filterMultipartResolver.setMaxUploadSize(MAXBYTES);
        return filterMultipartResolver;
    }
    
  • Не забудьте указать правильный приоритет фильтра в AbstractSecurityWebApplicationInitializer, как указано в документах (вы сделаете это в любом случае)

    @Order(1)
    public class SecurityWebApplicationInitializer extends AbstractSecurityWebApplicationInitializer {
        @Override
        protected void beforeSpringSecurityFilterChain(ServletContext servletContext) {
            insertFilters(servletContext, new MultipartFilter());
        }
    }
    
  • Добавьте токен _csrf в URL-адрес формы (я использую здесь тимелеар)

    <form th:action="@{|/submitImage?${_csrf.parameterName}=${_csrf.token}|}" 
    
  • В контроллере просто проверьте значение null на MultipartFile, что-то вроде (snippet not checked for errors):

    @RequestMapping(value = "/submitImage", method = RequestMethod.POST)
    public String submitImage(MyFormBean myFormBean, BindingResult bindingResult, HttpServletRequest request, Model model) {
        MultipartFile multipartFile = myFormBean.getImage();
        if (multipartFile==null) {
            bindingResult.rejectValue("image", "validation.image.filesize");
        } else if (multipartFile.isEmpty()) {
            bindingResult.rejectValue("image", "validation.image.missing");
    

Таким образом, вы можете использовать обычный метод контроллера для обработки отправки формы даже в случае превышения размера.

Что мне не нравится в этом подходе, так это то, что вам нужно возиться с внешним библиотечным пакетом (MultipartParsingResult защищен) и что вам нужно запомнить установку токена на url формы (что также менее безопасно btw).

Мне нравится, что вы обрабатываете представление формы только в одном месте в контроллере.

Проблема большого файла, который будет полностью загружен перед возвратом пользователю, также сохраняется, но я думаю, что он уже адресован где-то еще.