如何使用Spring Security很好地处理文件上载MaxUploadSizeExceededException

时间:2014-05-25 14:22:28

标签: spring spring-mvc file-upload spring-security

我使用Spring Web 4.0.5,Spring Security 3.2.4,Commons FileUpload 1.3.1,Tomcat 7,当我超出上传大小限制时,我得到一个丑陋的MaxUploadSizeExceededException ,这会导致" 500内部服务器错误"。我用一个很好的通用弹出窗口来处理它,但我宁愿让我的Controller通过返回原始形式并使用正确的解释信息来处理它。

我已经多次看过一个类似的问题,一些解决方案在不使用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,我会得到一个&#34 ; 403禁止"错误:

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没有意义,因为它从未被调用过。

如果我找不到更好的解决方案,我只需删除上传大小限制并在控制器中自行处理,其缺点是让用户等到上传完成后再看到有关文件大小的错误消息。 我甚至可能会忽略所有这一切,因为在这种情况下它是一个图像,我可以将其调整到适当的值。

尽管如此,我还是想看看这个问题的解决方案。

谢谢

编辑:

我按要求添加堆栈跟踪。这是生成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

4 个答案:

答案 0 :(得分:10)

您可以通过添加额外的Filter来捕获异常并重定向到错误页面来处理MaxUploadSizeExceededException。例如,您可以创建一个MultipartExceptionHandler Filter,如下所示:

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来更新文档以讨论错误处理

答案 1 :(得分:3)

我知道我迟到了,但我找到了一个更优雅的解决方案。

不要为多部分解析器添加过滤器,只需在控制器方法上添加throws MaxUploadSizeExceededException,然后在DelegatingFilterProxy中为web.xml添加过滤器,就可以向右添加异常处理程序在您的控制器中,无需重定向请求。

e.g:

方法(在控制器中):

@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>

这就是你所需要的一切。

答案 2 :(得分:1)

必须在多部分过滤器之后添加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

答案 3 :(得分:0)

我在试验时提出的解决方案如下:

  1. 扩展CommonsMultipartResolver以吞下异常。我在请求中添加了例外,以防您想在Controller中使用它,但我认为不需要

    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);
            }
        }
    }
    
  2. 在WebSecurityConfigurerAdapter中声明你的解析器,代替CommonsMultipartResolver(你应该在任何情况下都声明一个filterMultipartResolver,所以这里没什么新东西)

    @Bean(name="filterMultipartResolver")
    CommonsMultipartResolver filterMultipartResolver() {
        CommonsMultipartResolver filterMultipartResolver = new ExtendedCommonsMultipartResolver();
        filterMultipartResolver.setMaxUploadSize(MAXBYTES);
        return filterMultipartResolver;
    }
    
  3. 请记住在文档中说明的AbstractSecurityWebApplicationInitializer中定义正确的过滤器优先级(您无论如何都要这样做)

    @Order(1)
    public class SecurityWebApplicationInitializer extends AbstractSecurityWebApplicationInitializer {
        @Override
        protected void beforeSpringSecurityFilterChain(ServletContext servletContext) {
            insertFilters(servletContext, new MultipartFilter());
        }
    }
    
  4. 将_csrf标记添加到表单操作网址(我在这里使用百里香花)

    <form th:action="@{|/submitImage?${_csrf.parameterName}=${_csrf.token}|}" 
    
  5. 在Controller中,只需在MultipartFile上检查null,例如(不检查错误的代码段):

    @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");
    
  6. 这样,即使超出大小,也可以使用通常的Controller方法处理表单提交。

    我不喜欢这种方法是你必须弄乱外部库包(MultipartParsingResult受保护),你必须记住在表单url上设置令牌(这也是不太安全btw )。

    我喜欢的是你只在控制器的一个地方处理表单提交。

    在返回用户之前完全下载大文件的问题也会持续存在,但我想它已经在其他地方解决了。