Spring SecurityのCSRF対策とServlet 3.0のMultipartをJava Configで設定する

はまったのでメモ。

やりたかったこと

HTMLのFORMをPOSTするとき <input hidden name="_csrf" value="..." />CSRFトークンを送りつつMultipartでファイルアップロードしたい。 Springの設定はJava Configでやっていて、web.xmlは作っていない。今さらいろいろな設定をxmlにしたくない。

やってみたこと

Springのリファレンス 16.11 Spring’s multipart (file upload) support にある通り、MultipartResolverをコンテキストに登録してみた。

@Bean
public MultipartResolver multipartResolver() {
    return new StandardServletMultipartResolver();
}

HTML側からPOSTすると、サーバ側のログに Invalid csrf token というメッセージが出て、ステータスコード403が返ってきた。

原因は、Spring SecurityのCsrfFilterがリクエストパラメータからトークンを取得するときに、Multipartリクエストに対し getParameter をしているため。

解決

しばらくググってみたところ、 TERASOLUNA Global Framework のCSRF対策 に方法が書いてあった。

6.7.2.4.1. MultipartFilterを使用する方法

通常、マルチパートリクエストの場合、formから送信された値はFilter内で取得できない。 org.springframework.web.multipart.support.MultipartFilterを使用することで、マルチパートリクエストでも、Filter内で、 formから送信された値を取得することができる。

サイトにはweb.xmlでの設定例が載っているが、要はspringSecurityFilterChainより前にMultipartFilterを動かす必要があるということ。 Java Configでやるには、SecurityWebApplicationInitializer#beforeSpringSecurityFilterChain()でフィルタを登録する。

public class SecurityWebApplicationInitializer
        extends AbstractSecurityWebApplicationInitializer {
    @Override
    protected void beforeSpringSecurityFilterChain(ServletContext servletContext) {
        final MultipartFilter multipartFilter = new MultipartFilter();
        servletContext.addFilter("multipartFilter", multipartFilter)
                .addMappingForUrlPatterns(
                        EnumSet.allOf(DispatcherType.class), false, "/*");
    }
}

MultipartFilterは、デフォルトでServlet 3.0のAPIを使うStandardServletMultipartResolverを使うようになっているため、自前でコンテキストに登録する必要はない。もしcommons-fileuploadを使いたい場合は、MultipartFilter#DEFAULT_MULTIPART_RESOLVER_BEAN_NAMEで定義された名前 ("filterMultipartResolver") でコンテキストに CommonsMultipartResolver を登録すればよい。

Servlet 3.0のMultipart有効化

web.xmlでの設定例や、自前Servletにつけるアノテーションはすぐに見つかるのだけど、DispatcherServletJava Configの設定例が見当たらない。けど、サーブレット<multipart-config> をつければ良いだろうということで、WebApplicationInitializerに設定を追加。

public class WebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    @Override
    protected void customizeRegistration(Dynamic registration) {
        registration.setMultipartConfig(new MultipartConfigElement(
                System.getProperty("java.io.tmpdir"), -1, -1, 1024 * 1024));
    }
}

MultipartConfigElementで、アップロード可能な最大ファイルサイズや、添付ファイルに書きだすサイズ閾値など指定可能。

これで、Java Configのみで、CSRFとMultipartを設定して動かすことができた。