web-dev-qa-db-fra.com

Comment gérer correctement le téléchargement de fichiers MaxUploadSizeExceededException avec Spring Security

J'utilise Spring Web 4.0.5, Spring Security 3.2.4, Commons FileUpload 1.3.1, Tomcat 7 et j'obtiens un MaxUploadSizeExceededException déplaisant lorsque ma limite de taille de téléchargement est dépassée, ce qui entraîne une "500 erreur de serveur interne". . Je le traite avec un popup générique Nice, mais je préférerais que mon contrôleur s'en occupe en revenant à la forme d'origine avec le message d'explication approprié.

J'ai vu une question similaire posée à plusieurs reprises, avec quelques solutions qui pourraient fonctionner si vous n'utilisiez pas Spring Security; Aucune de celles que j'ai essayées n'a fonctionné pour moi.

Le problème peut être que, lorsque vous utilisez Spring Security, le CommonsMultipartResolver n'est pas ajouté en tant que bean "multipartResolver" mais en tant que "filterMultipartResolver":

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

Si je règle filterMultipartResolver.setResolveLazily(true); cela ne fait aucune différence.

Si je sous-classe le CommonsMultipartResolver avec le mien et substitue la méthode parseRequest() avec quelque chose qui piège le MaxUploadSizeExceededException et renvoie un MultipartParsingResult vide, j'obtiens une erreur "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);
        }
    }
}

Enfin, il est inutile de mettre en œuvre une sorte de ExceptionHandler local ou global car il n’est jamais appelé. 

Si je ne trouve pas de meilleure solution, je vais simplement supprimer la limite de taille de téléchargement et la gérer moi-même dans le contrôleur, avec l'inconvénient de demander à l'utilisateur d'attendre la fin du téléchargement avant de voir le message d'erreur relatif à la taille du fichier Je pourrais même ignorer tout cela parce que, s'agissant d'une image dans ce cas, je pourrais simplement la redimensionner à la valeur appropriée.

Néanmoins, j'aimerais voir une solution à ce problème.

Je vous remercie

MODIFIER:

J'ajoute la trace de la pile comme demandé. C'est le cas où un 500 est généré.

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
18
xtian

Vous pouvez gérer l'exception MaxUploadSizeExceededException en ajoutant un filtre supplémentaire pour intercepter l'exception et la redirection vers une page d'erreur. Par exemple, vous pouvez créer un filtre MultipartExceptionHandler comme suit:

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);
    }

}

NOTE: cette redirection suppose une hypothèse à propos de votre formulaire et de son téléchargement. Vous devrez peut-être modifier où rediriger. Spécifiquement si vous suivez le modèle de votre formulaire étant à GET et il est traité à POST cela fonctionnera.

Vous pouvez ensuite vous assurer d'ajouter ce filtre avant MultipartFilter. Par exemple, si vous utilisez web.xml, vous verrez quelque chose comme ceci:

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

Dans votre formulaire, vous pouvez ensuite détecter si l'erreur s'est produite en vérifiant si l'erreur de paramètre HTTP est présente. Par exemple, dans un fichier JSP, vous pouvez effectuer les opérations suivantes:

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

PS: j'ai créé SEC-2614 pour mettre à jour la documentation afin de discuter du traitement des erreurs

10
Rob Winch

Je sais que je suis en retard à la fête, mais j'ai trouvé une solution beaucoup plus élégante à mon humble avis.

Au lieu d’ajouter un filtre pour le résolveur en plusieurs parties, ajoutez simplement throws MaxUploadSizeExceededException à votre méthode de contrôleur et ajoutez le filtre pour DelegatingFilterProxy dans votre web.xml et vous pourrez ajouter un gestionnaire d’exceptions directement dans votre contrôleur sans avoir à rediriger la demande.

par exemple.:

Méthode (dans le contrôleur):

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

Gestionnaire d'exceptions (dans le même contrôleur):

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

Web.xml (merci à 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>

Et c'est tout ce dont vous avez besoin.

3
Taugenichts

La chose est springSecurityFilterChain doit être ajouté après le filtre multipart. C'est pourquoi vous obtenez le statut 403. Ici:

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

Je pense qu'après cela, vous pourrez attraper FileUploadBase.SizeLimitExceededException dans une classe annotée @ControllerAdvice contenant les méthodes @ExceptionHandler annotées.

1
px5x2

La solution que j'ai trouvée en expérimentant est la suivante:

  1. Étendez CommonsMultipartResolver afin d'avaler l'exception. J'ajoute l'exception à la demande au cas où vous voudriez l'utiliser dans le contrôleur, mais je ne pense pas que ce soit nécessaire

    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. Déclarez votre résolveur dans WebSecurityConfigurerAdapter, à la place de CommonsMultipartResolver (vous devez déclarer un filterMultipartResolver dans tous les cas, donc rien de nouveau ici)

    @Bean(name="filterMultipartResolver")
    CommonsMultipartResolver filterMultipartResolver() {
        CommonsMultipartResolver filterMultipartResolver = new ExtendedCommonsMultipartResolver();
        filterMultipartResolver.setMaxUploadSize(MAXBYTES);
        return filterMultipartResolver;
    }
    
  3. N'oubliez pas de définir la priorité de filtre correcte dans AbstractSecurityWebApplicationInitializer, comme indiqué dans la documentation (vous devez le faire dans tous les cas).

    @Order(1)
    public class SecurityWebApplicationInitializer extends AbstractSecurityWebApplicationInitializer {
        @Override
        protected void beforeSpringSecurityFilterChain(ServletContext servletContext) {
            insertFilters(servletContext, new MultipartFilter());
        }
    }
    
  4. Ajoutez le jeton _csrf à l'URL de l'action de formulaire (j'utilise thymeleaf ici)

    <form th:action="@{|/submitImage?${_csrf.parameterName}=${_csrf.token}|}" 
    
  5. Dans le contrôleur, il suffit de rechercher la valeur null dans MultipartFile, par exemple (l’extrait n’a pas été vérifié pour les erreurs):

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

De cette façon, vous pouvez utiliser la méthode habituelle du contrôleur pour gérer la soumission du formulaire, même en cas de dépassement de la taille.

Ce que je n'aime pas de cette approche, c'est que vous devez jouer avec un paquet de bibliothèque externe (MultipartParsingResult est protégé) et que vous devez vous rappeler de définir le jeton sur l'URL du formulaire (qui est également moins sécurisé).

Ce que j’aime, c’est que vous gérez la soumission du formulaire à un seul endroit du contrôleur.

Le problème d'un gros fichier entièrement téléchargé avant de revenir à l'utilisateur persiste également, mais je suppose qu'il est déjà abordé ailleurs. 

0
xtian