【问题标题】:How to nicely handle file upload MaxUploadSizeExceededException with Spring Security如何使用 Spring Security 很好地处理文件上传 MaxUploadSizeExceededException
【发布时间】:2014-07-14 09:48:23
【问题描述】:

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

我已经多次看到类似的问题,其中一些解决方案在不使用 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 并使用捕获 MaxUploadSizeExceededException 并返回空 MultipartParsingResult 的东西覆盖 parseRequest() 方法,我会收到“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 是没有意义的,因为它永远不会被调用。

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

不过,我还是想看看这个问题的解决方案。

谢谢

编辑:

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

【问题讨论】:

  • 请发布您的完整堆栈跟踪
  • 一些进一步的调查显示,当使用唯一可行的方法是 ExtendedCommonsMultipartResolver 时,“403 Forbidden”是由 Spring Security 的 csrf 特性引起的。如果我在 WebSecurityConfigurerAdapter 中使用 http.csrf().disable() 禁用它,则不会生成 403。当然,我不希望禁用它,甚至不使用 requireCsrfProtectionMatcher() 部分禁用它,因为我不希望我的用户上传其他人的数据,除非 csrf 技巧不能用多部分播放。原来的问题仍然成立。请指教。
  • 我觉得这件事无法解决,除非 A)我在 url 上设置了 _csrf,如此处所述docs.spring.io/spring-security/site/docs/3.2.0.CI-SNAPSHOT/… 或 B)我编写了一个 ExtendedCommonsMultipartResolver,它捕获 MaxUploadSizeExceededException 并重复 parseRequest在禁用大小检查并以某种方式标记超出部分之后。选项 B 仍会加载整个文件,因此毫无意义。我想我们真的只有一个选择。

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


【解决方案1】:

问题是springSecurityFilterChain 必须在 多部分过滤器之后添加。这就是您获得 403 状态的原因。这里:

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

我想在你这样做之后,你将能够在包含@ExceptionHandler 注释方法的@ControllerAdvice 注释类中捕获FileUploadBase.SizeLimitExceededException

【讨论】:

  • 我已经这样做了。我认为没有它,文件上传将永远无法工作;我只有在超出文件限制时才会遇到问题。顺便说一句,您参考的文档似乎是错误的:“在 Spring Security 过滤器之后指定 MultipartFilter 意味着没有授权”在我看来应该是“在之前指定 MultipartFilter”。
  • 你说得对,MultipartFilter 应该在 springSecurityFilterChain 之前。这似乎是答案中所述的内容以及它在提供的链接中的记录方式。如果我在文档中遗漏了一些错误,请指出,我一定会修复它。
  • 对不起,我不清楚。提供的链接正确地说 MultipartFilter 应该在之前,但它接着说当 MultipartFilter 在之后时,没有授权。这里应该说“之前”,因为当 MultipartFilter 在 Security 过滤器之前时,这是有问题的,显然它不受授权保护。
【解决方案2】:

您可以通过添加额外的过滤器来处理 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 来更新文档以讨论错误处理

【讨论】:

    【解决方案3】:

    我在实验时想出的解决方案如下:

    1. 扩展 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);
              }
          }
      }
      
    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 标记添加到表单操作 URL(我在这里使用的是 thymeleaf)

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

      @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");
      

    这样,即使超出大小,您也可以使用通常的 Controller 方法来处理表单提交。

    我不喜欢这种方法的是,您必须弄乱外部库包(MultipartParsingResult 受保护),并且您必须记住在表单 url 上设置令牌(顺便说一句,这也不太安全)。

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

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

    【讨论】:

      【解决方案4】:

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

      无需为多部分解析器添加过滤器,只需在您的控制器方法中添加throws MaxUploadSizeExceededException 并在您的web.xml 中添加DelegatingFilterProxy 的过滤器,您可以直接在控制器中添加异常处理程序,而无需重定向请求。

      例如:

      方法(在控制器中):

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

      这就是你所需要的。

      【讨论】:

      • 启动时出现以下错误“org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'springSecurityFilterChain' is defined”
      猜你喜欢
      • 2015-03-17
      • 2012-02-18
      • 2013-11-12
      • 2012-01-03
      • 2011-02-11
      • 2015-04-25
      • 1970-01-01
      • 1970-01-01
      • 2011-12-23
      相关资源
      最近更新 更多