5.17. ファイルアップロード¶
Caution
本バージョンの内容は既に古くなっています。最新のガイドラインはこちらからご参照ください。
目次
5.17.1. Overview¶
Note
本節では、Servlet3.0でサポートされたファイルアップロード機能を使用しているため、Servletのバージョンは、3.0以上であることが前提となる。
Warning
使用するアプリケーションサーバのファイルアップロードの実装が、Apache Commons FileUploadの実装に依存している場合、CVE-2014-0050で報告されているセキュリティの脆弱性が発生する可能性がある。 使用するアプリケーションサーバに同様の脆弱性がない事を確認されたい。
Tomcatを使用する場合、7.0系は7.0.52以上、8.0系は8.0.3以上を使用する必要がある。
5.17.1.1. アップロード処理の基本フロー¶
Servlet3.0からサポートされたファイルアップロード機能と、Spring Webのクラスを使って、ファイルをアップロードする際の基本フローを、以下に示す。
項番 説明 (1) アップロードするファイルを選択し、アップロードを実行する。 (2) サーブレットコンテナは、multipart/form-data
リクエストを受け取り、org.springframework.web.servlet.DispatcherServlet
を呼び出す。 (3)DispatcherServlet
は、org.springframework.web.multipart.support.StandardServletMultipartResolver
のメソッドを呼び出し、Servlet3.0のファイルアップロード機能を、Spring MVCで扱えるようにする。StandardServletMultipartResolver
は、Servlet3.0から導入されたAPI(javax.servlet.http.Part
)をラップするorg.springframework.web.multipart.MultipartFile
のオブジェクトを生成する。 (4)DispatcherServlet
は、Controllerの処理メソッドを呼び出す。(3)で生成されたMultipartFile
オブジェクトは、 Controllerの引数またはフォームオブジェクトに、バインドされる。 (5) Controllerは、MultipartFile
オブジェクトのメソッドを呼び出し、アップロードされたファイルの中身と、メタ情報(ファイル名など)を取得する。 (6)MultipartFile
は、Servlet3.0から導入されたPart
オブジェクトのメソッドを呼び出し、アップロードされたファイルの中身と、メタ情報(ファイル名など)を取得し、Controllerに返却する。 (7) Controllerは、Serviceのメソッドを呼び出し、アップロード処理を実行する。MultipartFile
オブジェクトより取得した、ファイルの中身と、メタ情報(ファイル名など)は、Serviceのメソッドの引数として、引き渡す。 (8) Serviceは、アップロードされたファイルの中身と、メタ情報(ファイル名など)を、ファイルまたはデータベースに格納する。 (9)DispatcherServlet
は、StandardServletMultipartResolver
を呼び出し、Servlet3.0のファイルアップロード機能で使用される一時ファイルを削除する。 (10)StandardServletMultipartResolver
は、Servlet3.0から導入されたPart
オブジェクトのメソッドを呼び出し、ディスクに保存されている一時ファイルを削除する。Note
Controllerでは、Spring Webから提供されている
MultipartFile
オブジェクトに対して処理を行うため、Servlet3.0から提供されたファイルアップロード用のAPIに依存した実装を、排除することができる。
5.17.1.2. Spring Webから提供されているクラスについて¶
Spring Webから提供されているファイルアップロード用のクラスについて、説明する。
項番 クラス名 説明
org.springframework.web.multipart.MultipartFile アップロードされたファイルであることを示すインタフェース。利用するファイルアップロード機能で扱うファイルオブジェクトを、抽象化する役割をもつ。
org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile Servlet3.0から導入されたファイルアップロード機能用のMultipartFile
クラス。Servlet3.0から導入されたPart
オブジェクトに、処理を委譲している。
org.springframework.web.multipart.MultipartResolvermultipart/form-data
リクエストの解析方法を解決するためのインタフェース。ファイルアップロード機能の、実装に対応するMultipartFile
オブジェクトを生成する役割をもつ。
org.springframework.web.multipart.support.StandardServletMultipartResolver Servlet3.0から導入されたファイルアップロード機能用のMultipartResolver
クラス。
org.springframework.web.multipart.support.MultipartFilter multipart/form-dataリクエストの時に、Servlet Filterの処理内でリクエストパラメータを取得できるようにするためのクラス。このクラスを使用しないと、Servlet Filterでリクエストパラメータの取得ができないため、Spring Securityから提供されているCSRFトークンチェック機能が正しく動作しない。具体的には、CSRFトークンが取得できないため、常にCSRFトークンエラーとなりファイルのアップロードが出来ない。Tip
本ガイドラインでは、Servlet3.0から導入されたファイルアップロード機能を使うことを前提としているが、Spring Webでは、「Apache Commons FileUpload」用の実装クラスも提供している。 アップロード処理の実装の違いは、
MultipartResolver
と、MultipartFile
オブジェクトによって吸収されるため、Controllerの実装に影響を与えることはない。 Servlet3.0に、対応していないサーブレットコンテナ(Tomcat6など)を使用する必要がある場合は、こちらを使用すること。Warning
Apache Commons FileUploadを使用する場合、CVE-2014-0050で報告されているセキュリティの脆弱性が発生する可能性がある。 使用するApache Commons FileUploadのバージョンに脆弱性がない事を確認されたい。
Apache Commons FileUploadを使用する場合、1.2系は1.2.1以上、1.3系は1.3.1以上を使用する必要がある。
5.17.2. How to use¶
5.17.2.1. アプリケーションの設定¶
5.17.2.1.1. Servlet3.0のアップロード機能を有効化するための設定¶
Servlet3.0のアップロード機能を有効化するために、以下の設定を行う。
web.xml
<web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" version="3.0"> <!-- (1) (2) --> <servlet> <servlet-class> org.springframework.web.servlet.DispatcherServlet </servlet-class> <!-- omitted --> <multipart-config> <!-- (3) --> <max-file-size>5242880</max-file-size> <!-- (4) --> <max-request-size>27262976</max-request-size> <!-- (5) --> <file-size-threshold>0</file-size-threshold> <!-- (6) --> </multipart-config> </servlet> <!-- ommited --> </web-app>
項番 説明 (1)<web-app>
要素のxsi:schemaLocation
属性に、Servlet3.0以上のXSDファイルを指定する。 (2)<web-app>
要素のversion
属性に、3.0
以上のバージョンを指定する。 (3) ファイルアップロードを使用するServletの<servlet>
要素に、<multipart-config>
要素を追加する。 (4) アップロードを許可する1ファイルの最大バイト数を指定する。指定がない場合、-1 (制限なし)が設定される。指定した値を超えた場合、org.springframework.web.multipart.MultipartException
が発生する。上記例では、 5MBを指定している。 (5)multipart/form-data
リクエストのContent-Lengthの最大値を指定する。指定がない場合、-1 (制限なし)が設定される。指定した値を超えた場合、org.springframework.web.multipart.MultipartException
が発生する。本パラメータに設定する値は、以下の計算式で算出される値を設定する必要がある。(「アップロードを許可する1ファイルの最大バイト数」 * 「同時にアップロードを許可するファイル数」 ) + 「その他のフォーム項目のデータサイズ」 + 「multipart/form-dataリクエストのメタ情報サイズ」上記例では、 26MBを指定している。内訳は、25MB(5MB * 5 files)と、1MB(メタ情報のバイト数 + フォーム項目のバイト数)である。 (6) アップロードされたファイルの中身を、一時ファイルとして保存するかの閾値(1ファイルのバイト数)を指定する。このパラメータを明示的に指定しないと<max-file-size>
要素や<max-request-size>
要素で指定した値が有効にならないアプリケーションサーバが存在するため、デフォルト値(0)を明示的に指定している。Warning
Dos攻撃に対する攻撃耐性を高めるため、
max-file-size
と、max-request-size
は、かならず指定すること。Dos攻撃については、アップロード機能に対するDos攻撃を参照されたい。
Note
デフォルトの設定では、アップロードされたファイルは必ず一時ファイルに出力されるが、
<multipart-config>
の子要素である<file-size-threshold>
要素の設定値によって、出力有無を制御することができる。<!-- omitted --> <multipart-config> <!-- omitted --> <file-size-threshold>32768</file-size-threshold> <!-- (7) --> </multipart-config> <!-- omitted -->
項番 説明 (7) アップロードされたファイルの中身を、一時ファイルとして保存するかの閾値(1ファイルのバイト数)を指定する。指定がない場合、0が設定される。指定値を超えるサイズのファイルがアップロードされた場合、アップロードされたファイルは、一時ファイルとしてディスクに出力され、リクエストが完了した時点で削除される。上記例では、 32KBを指定している。Warning
本パラメータは、以下の点でトレードオフの関係となっているため、システム特性にあった設定値を指定すること。
- 設定値を大きくすると、メモリ内で処理が完結するため、処理性能は向上するが、 Dos攻撃などによって
OutOfMemoryError
が発生する可能性が高くなる。- 設定値を小さくすると、メモリを使用率を最小限に抑えることができるため、Dos攻撃などによって
OutOfMemoryError
が発生する可能性を抑えることができるが、 ディスクIOの発生頻度が高くなるため、性能劣化が発生する可能性が高くなる。一時ファイルの出力ディレクトリを変更したい場合は、
<multipart-config>
の子要素である<location>
要素にディレクトリパスを指定する。<!-- omitted --> <multipart-config> <location>/tmp</location> <!-- (8) --> <!-- omitted --> </multipart-config> <!-- omitted -->
項番 説明 (8) 一時ファイルを出力するディレクトリのパスを指定する。省略した場合、アプリケーションサーバの一時ファイルを格納するためのディレクトに出力される。上記例では、/tmp
を指定している。Warning
<location>
要素で指定するディレクトリは、アプリケーションサーバ(サーブレットコンテナ)が利用するディレクトリであり、アプリケーションからアクセスする場所ではない。アプリケーションとしてアップロードされたファイルを一時的なファイルとして保存しておきたい場合は、
<location>
要素で指定するディレクトリとは、別のディレクトリに出力すること。
5.17.2.1.2. Servlet Filterの処理内でリクエストパラメータを取得できるようにするための設定¶
multipart/form-dataリクエストの時に、Servlet Filterの処理内でリクエストパラメータを取得できるようにするために、以下の設定を行う。
web.xml
<!-- (1) --> <filter> <filter-name>MultipartFilter</filter-name> <filter-class>org.springframework.web.multipart.support.MultipartFilter</filter-class> </filter> <!-- (2) --> <filter-mapping> <filter-name>MultipartFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
項番 説明 (1) Servlet FliterとしてMultipartFilter
を定義する。 (2)MultipartFilter
を適用するURLのパターンを指定する。Warning
MultipartFilterは、リクエストパラメータにアクセスするServlet Filterより前に定義する必要がある。
Spring Securityを使ってセキュリティ対策を行う場合は、
springSecurityFilterChain
より前に定義すること。 また、プロジェクト独自で作成するServlet Filterでリクエストパラメータにアクセスするものがある場合は、そのServlet Filterより前に定義すること。
5.17.2.1.3. Servlet3.0のアップロード機能とSpring MVCを連携するための設定¶
Servlet3.0のアップロード機能と、Spring MVCを連携するために、以下の設定を行う。
spring-mvc.xml
<bean id="multipartResolver" class="org.springframework.web.multipart.support.StandardServletMultipartResolver"> <!-- (1) --> </bean>
項番 説明 (1) Servlet3.0用のMultipartResolverであるStandardServletMultipartResolver
を、bean定義する。beanIDは、"multipartResolver"
とすること。この設定を行うことで、アップロードされたファイルをorg.springframework.web.multipart.MultipartFile
として、Controllerの引数およびフォームオブジェクトのプロパティとして、受け取ることができる。
5.17.2.1.4. 例外ハンドリングの設定¶
許可されないサイズのファイルやマルチパートのリクエストが行われた際に発生するMultipartException
の例外ハンドリングの定義を追加する。
MultipartException
は、クライアントが指定するファイルサイズに起因して発生する例外なので、クライアントエラー(HTTPレスポンスコード=4xx)として扱うことを推奨する。MultipartException
をハンドリングするための設定は、MultipartFilter
を使用するか否かによって異なる。MultipartFilter
を使用する場合は、サーブレットコンテナの<error-page>
機能を使って例外ハンドリングを行う。web.xml
<error-page> <!-- (1) --> <exception-type>org.springframework.web.multipart.MultipartException</exception-type> <!-- (2) --> <location>/WEB-INF/views/common/error/fileUploadError.jsp</location> </error-page>
項番 説明 (1) ハンドリング対象の例外クラスとして、MultipartException
を指定する。 (2)MultipartException
が発生した際に表示するファイルを指定する。上記例では、"/WEB-INF/views/common/error/fileUploadError.jsp"
を指定している。
fileUploadError.jsp
<%-- (3) --%> <% response.setStatus(HttpServletResponse.SC_BAD_REQUEST); %> <!DOCTYPE html> <html> <!-- omitted --> </html>
項番 説明 (3) HTTPステータスコードは、HttpServletResponse
のAPIを呼び出して設定する。上記例では、"400"
(Bad Request) を設定している。明示的に設定しない場合、HTTPステータスコードは"500"
(Internal Server Error)となる。
MultipartFilter
を使用しない場合は、SystemExceptionResolver
を使用して例外ハンドリングを行う。spring-mvc.xml
<bean class="org.terasoluna.gfw.web.exception.SystemExceptionResolver"> <!-- omitted --> <property name="exceptionMappings"> <map> <!-- omitted --> <!-- (4) --> <entry key="MultipartException" value="common/error/fileUploadError" /> </map> </property> <property name="statusCodes"> <map> <!-- omitted --> <!-- (5) --> <entry key="common/error/fileUploadError" value="400" /> </map> </property> <!-- omitted --> </bean>
項番 説明 (4)SystemExceptionResolver
のexceptionMappings
に、MultipartException
が発生した際に表示するView(JSP)の定義を追加する。上記例では、"common/error/fileUploadError"
を指定している。 (5)MultipartException
が発生した際に応答するHTTPステータスコードの定義を追加する。上記例では、"400"
(Bad Request) を指定している。クライアントエラー(HTTPレスポンスコード = 4xx)を指定することで、共通ライブラリの例外ハンドリング機能から提供しているクラス(HandlerExceptionResolverLoggingInterceptor
)によって出力されるログは、ERROR
レベルではなく、WARN
レベルとなる。
MultipartException
に対する例外コードを設ける場合は、例外コードの設定を追加する。applicationContext.xml
<bean id="exceptionCodeResolver" class="org.terasoluna.gfw.common.exception.SimpleMappingExceptionCodeResolver"> <property name="exceptionMappings"> <map> <!-- (6) --> <entry key="MultipartException" value="e.xx.fw.6001" /> <!-- omitted --> </map> </property> <property name="defaultExceptionCode" value="e.xx.fw.9001" /> <!-- omitted --> </bean>
項番 説明 (6)SimpleMappingExceptionCodeResolver
のexceptionMappings
に、MultipartException
が発生した際に適用する、例外コードを追加する。上記例では、"e.xx.fw.6001"
を指定している。個別に定義を行わない場合は、defaultExceptionCode
に指定した例外コードが適用される。
5.17.2.2. 単一ファイルのアップロード¶
単一ファイルをアップロードする方法について、説明する。
org.springframework.web.multipart.MultipartFile
オブジェクトを、フォームオブジェクトにバインドして受け取る方法と、Controllerの引数として直接受け取る2つの方法があるが、本ガイドラインでは、フォームオブジェクトにバインドして受け取る方法を推奨する。以下に、フォームオブジェクトにバインドして受け取る方法について、説明する。
5.17.2.2.1. フォームの実装¶
public class FileUploadForm implements Serializable { // omitted private MultipartFile file; // (1) @NotNull @Size(min = 0, max = 100) private String description; // omitted getter/setter methods. }
項番 説明 (1) フォームオブジェクトに、org.springframework.web.multipart.MultipartFile
のプロパティを定義する。
5.17.2.2.2. JSPの実装¶
<form:form action="${pageContext.request.contextPath}/article/uploadFile" method="post" modelAttribute="fileUploadForm" enctype="multipart/form-data"> <!-- (1) (2) --> <table> <tr> <th width="35%">File to upload</th> <td width="65%"> <form:input type="file" path="file" /> <!-- (3) --> <form:errors path="file" /> </td> </tr> <tr> <th width="35%">Description</th> <td width="65%"> <form:input path="description" /> <form:errors path="description" /> </td> </tr> <tr> <td> </td> <td><form:button>Upload</form:button></td> </tr> </table> </form:form>
項番 説明 (1)<form:form>
要素のenctype属性に、"multipart/form-data"
を指定する。 (2)<form:form>
要素のmodelAttribute属性に、フォームオブジェクトの属性名を指定する。上記例では、"fileUploadForm"
を指定している。 (3)<form:input>
要素type属性に、"file"
を指定し、path属性に、MultipartFile
プロパティ名を指定する。上記例では、アップロードされたファイルは、FileUploadForm
オブジェクトの"file"
プロパティに格納される。
5.17.2.2.3. Controllerの実装¶
@RequestMapping("article") @Controller public class ArticleController { @Value("${upload.allowableFileSize}") private int uploadAllowableFileSize; // omitted // (1) @ModelAttribute public FileUploadForm setFileUploadForm() { return new FileUploadForm(); } // (2) @RequestMapping(value = "upload", method = RequestMethod.GET, params = "form") public String uploadForm() { return "article/uploadForm"; } // (3) @RequestMapping(value = "upload", method = RequestMethod.POST) public String upload(@Validated FileUploadForm form, BindingResult result, RedirectAttributes redirectAttributes) { if (result.hasErrors()) { return "article/uploadForm"; } MultipartFile uploadFile = form.getFile(); // (4) if (!StringUtils.hasLength(uploadFile.getOriginalFilename())) { result.rejectValue(uploadFile.getName(), "e.xx.at.6002"); return "article/uploadForm"; } // (5) if (uploadFile.isEmpty()) { result.rejectValue(uploadFile.getName(), "e.xx.at.6003"); return "article/uploadForm"; } // (6) if (uploadAllowableFileSize < uploadFile.getSize()) { result.rejectValue(uploadFile.getName(), "e.xx.at.6004", new Object[] { uploadAllowableFileSize }, null); return "article/uploadForm"; } // (7) // omit processing of upload. // (8) redirectAttributes.addFlashAttribute(ResultMessages.success().add( "i.xx.at.0001")); // (9) return "redirect:/article/upload?complete"; } @RequestMapping(value = "upload", method = RequestMethod.GET, params = "complete") public String uploadComplate() { return "article/uploadComplete"; } // omitted }
項番 説明 (1) ファイルアップロード用のフォームオブジェクトを、Model
に格納するためのメソッド。上記例では、Model
に格納するための属性名は、"fileUploadForm"
となる。 (2) アップロード画面を表示するための処理メソッド。 (3) ファイルをアップロードするための処理メソッド。 (4) アップロードファイルが選択されているかのチェックを行っている。ファイルが選択されたかチェックする場合は、MultipartFile#getOriginalFilename
メソッドを呼び出し、ファイル名の指定有無で判断する。上記例では、ファイルが選択されていない場合は、入力チェックエラーとしている。 (5) 空のファイルが選択されているかのチェックを行っている。選択されたファイルの中身が空でないことをチェックする場合は、MultipartFile#isEmpty
メソッドを呼び出し、中身の存在チェックを行う。上記例では、 空のファイルが選択されている場合は、入力チェックエラーとしている。 (6) ファイルのサイズが、許容サイズ内かどうかのチェックを行っている。選択されたファイルのサイズをチェックする場合は、MultipartFile#getSize
メソッドを呼び出し、サイズが許容範囲内かチェックを行う。上記例では、 ファイルのサイズが許容サイズを超えている場合は、入力チェックエラーとしている。 (7) アップロード処理を実装する。上記例では、具体的な実装は省略しているが、共有ディスクやデータベースへ保存する処理を行うことになる。 (8) 要件に応じて、アップロードが成功したことを通知する、処理結果メッセージを格納する。 (9) アップロード処理完了後の画面表示は、リダイレクトして表示する。Note
重複アップロードの防止
ファイルのアップロードを行う場合は、PRGパターンによる画面遷移と、トランザクショントークンチェックを行うことを推奨する。 PRGパターンによる画面遷移と、トランザクショントークンチェックを行うことで、重複送信に伴う、同一ファイルのアップロードを防ぐことができる。
重複送信の防止方法について、詳細は、二重送信防止を参照されたい。
Note
MultipartFileについて
MultipartFileには、アップロードされたファイルを操作するためのメソッドが用意されている。 各メソッドの利用方法については、MultipartFileクラスのJavaDocを参照されたい。
5.17.2.3. ファイルアップロードのBean Validation¶
Note
Bean Validationの仕組みでチェックすることで、Controllerの処理をシンプルに保つことができるため、Bean Validationの仕組みを使うことを推奨する。
5.17.2.3.1. ファイルが選択されていることを検証するためのバリデーションの実装¶
// (1) @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE }) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = UploadFileRequiredValidator.class) public @interface UploadFileRequired { String message() default "{com.examples.upload.UploadFileRequired.message}"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE }) @Retention(RetentionPolicy.RUNTIME) @Documented @interface List { UploadFileRequired[] value(); } }// (2) public class UploadFileRequiredValidator implements ConstraintValidator<UploadFileRequired, MultipartFile> { @Override public void initialize(UploadFileRequired constraint) { } @Override public boolean isValid(MultipartFile multipartFile, ConstraintValidatorContext context) { return multipartFile != null && StringUtils.hasLength(multipartFile.getOriginalFilename()); } }
項番 説明 (1) ファイルが、選択されていることを検証するための、アノテーションを作成する。 (2) ファイルが、選択されていることを検証するための、実装を行うクラスを作成する。
5.17.2.3.2. ファイルが空でないことを検証するためのバリデーションの実装¶
// (3) @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE }) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = UploadFileNotEmptyValidator.class) public @interface UploadFileNotEmpty { String message() default "{com.examples.upload.UploadFileNotEmpty.message}"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE }) @Retention(RetentionPolicy.RUNTIME) @Documented @interface List { UploadFileNotEmpty[] value(); } }// (4) public class UploadFileNotEmptyValidator implements ConstraintValidator<UploadFileNotEmpty, MultipartFile> { @Override public void initialize(UploadFileNotEmpty constraint) { } @Override public boolean isValid(MultipartFile multipartFile, ConstraintValidatorContext context) { if (multipartFile == null || !StringUtils.hasLength(multipartFile.getOriginalFilename())) { return true; } return !multipartFile.isEmpty(); } }
項番 説明 (3) ファイルが、空でないことを検証するための、アノテーションを作成する。 (4) ファイルが、空でないことを検証するための、実装を行うクラスを作成する。
5.17.2.3.3. ファイルのサイズが許容サイズ内であることを検証するためのバリデーションの実装¶
// (5) @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE }) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = UploadFileMaxSizeValidator.class) public @interface UploadFileMaxSize { String message() default "{com.examples.upload.UploadFileMaxSize.message}"; long value() default (1024 * 1024); Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE }) @Retention(RetentionPolicy.RUNTIME) @Documented @interface List { UploadFileMaxSize[] value(); } }// (6) public class UploadFileMaxSizeValidator implements ConstraintValidator<UploadFileMaxSize, MultipartFile> { private UploadFileMaxSize constraint; @Override public void initialize(UploadFileMaxSize constraint) { this.constraint = constraint; } @Override public boolean isValid(MultipartFile multipartFile, ConstraintValidatorContext context) { if (constraint.value() < 0 || multipartFile == null) { return true; } return multipartFile.getSize() <= constraint.value(); } }
項番 説明 (5) ファイルのサイズが、許容サイズ内であることを検証するための、アノテーションを作成する。 (6) ファイルのサイズが、許容サイズ内であることを検証するための、実装を行うクラスを作成する。
5.17.2.3.4. フォームの実装¶
public class FileUploadForm implements Serializable { // omitted // (7) @UploadFileRequired @UploadFileNotEmpty @UploadFileMaxSize private MultipartFile file; @NotNull @Size(min = 0, max = 100) private String description; // omitted getter/setter methods. }
項番 説明 (7)MultipartFile
のフィールドに、アップロードファイルのバリデーションを行うための、アノテーションを付与する。
5.17.2.3.5. Controllerの実装¶
@RequestMapping(value = "uploadFile", method = RequestMethod.POST) public String uploadFile(@Validated FileUploadForm form, BindingResult result, RedirectAttributes redirectAttributes) { // (8) if (result.hasErrors()) { return "article/uploadForm"; } MultipartFile uploadFile = form.getFile(); // omit processing of upload. redirectAttributes.addFlashAttribute(ResultMessages.success().add( "i.xx.at.0001")); return "redirect:/article/upload"; }
項番 説明 (8) アップロードファイルのバリデーションの結果は、BindingResult
に格納される。
5.17.2.4. 複数ファイルのアップロード¶
複数ファイルを同時にアップロードする方法について説明する。
複数ファイルを同時にアップロードする場合は、org.springframework.web.multipart.MultipartFile
オブジェクトを、フォームオブジェクトにバインドして受け取る必要がある。
以降の説明では、単一ファイルのアップロードと重複する箇所の説明については、省略する。
5.17.2.4.1. フォームの実装¶
// (1) public class FileUploadForm implements Serializable { // omitted @UploadFileRequired @UploadFileNotEmpty @UploadFileMaxSize private MultipartFile file; @NotNull @Size(min = 0, max = 100) private String description; // omitted getter/setter methods. }public class FilesUploadForm implements Serializable { // omitted @Valid // (2) private List<FileUploadForm> fileUploadForms; // (3) // omitted getter/setter methods. }
項番 説明 (1) ファイル単位の情報(アップロードファイル自体と、関連するフォーム項目)を保持するクラス。上記例では、単一ファイルのアップロードの説明で作成したフォームオブジェクトを再利用している。 (2) リスト内で保持しているオブジェクトに対して、Bean Validationによる入力チェックを行うために、@Valid
アノテーションを付与する。 (3) ファイル単位の情報(アップロードファイル自体と、関連するフォーム項目)を保持するオブジェクトを、List型のプロパティとして定義する。Note
ファイルのみアップロードする場合は、
MultipartFile
オブジェクトを、List型のプロパティとして定義することもできるが、 Bean Validationを使用してアップロードファイルの入力チェックを行う場合は、ファイル単位の情報を保持するオブジェクトを、List型のプロパティとして定義する方が相性がよい。
5.17.2.4.2. JSPの実装¶
<form:form action="${pageContext.request.contextPath}/article/uploadFiles" method="post" modelAttribute="filesUploadForm" enctype="multipart/form-data"> <table> <tr> <th width="35%">File to upload</th> <td width="65%"> <form:input type="file" path="fileUploadForms[0].file" /> <!-- (1) --> <form:errors path="fileUploadForms[0].file" /> </td> </tr> <tr> <th width="35%">Description</th> <td width="65%"> <form:input path="fileUploadForms[0].description" /> <form:errors path="fileUploadForms[0].description" /> </td> </tr> </table> <table> <tr> <th width="35%">File to upload</th> <td width="65%"> <form:input type="file" path="fileUploadForms[1].file" /> <!-- (1) --> <form:errors path="fileUploadForms[1].file" /> </td> </tr> <tr> <th width="35%">Description</th> <td width="65%"> <form:input path="fileUploadForms[1].description" /> <form:errors path="fileUploadForms[1].description" /> </td> </tr> </table> <div> <form:button>Upload</form:button> </div> </form:form>
項番 説明 (1) アップロードファイルをバインドするList内の位置を指定する。バインドするリスト内の位置は、[]
の中に指定する。開始位置は、0
開始となる。
5.17.2.4.3. Controllerの実装¶
@RequestMapping(value = "uploadFiles", method = RequestMethod.POST) public String uploadFiles(@Validated FilesUploadForm form, BindingResult result, RedirectAttributes redirectAttributes) { if (result.hasErrors()) { return "article/uploadForm"; } // (1) for (FileUploadForm fileUploadForm : form.getFileUploadForms()) { MultipartFile uploadFile = fileUploadForm.getFile(); // omit processing of upload. } redirectAttributes.addFlashAttribute(ResultMessages.success().add( "i.xx.at.0001")); return "redirect:/article/upload?complete"; }
項番 説明 (1) ファイル単位の情報(アップロードファイル自体と関連するフォーム項目)を保持するオブジェクトからMultipartFile
を取得し、アップロード処理を実装する。上記例では、具体的な実装は省略しているが、共有ディスクやデータベースへ保存する処理を行うことになる。
5.17.2.5. HTML5のmultiple属性を使った複数ファイルのアップロード¶
HTML5でサポートされたinputタグのmultiple属性を使用して、複数ファイルを同時にアップロードする方法について説明する。
以降の説明では、単一ファイルのアップロード及び複数ファイルのアップロードと重複する箇所の説明については、省略する。
5.17.2.5.1. フォームの実装¶
HTML5のinputタグのmultiple属性を使用して、複数ファイルを同時にアップロードする場合は、org.springframework.web.multipart.MultipartFile
オブジェクトのコレクションを、フォームオブジェクトにバインドして受け取る必要がある。
// (1) public class FilesUploadForm implements Serializable { // omitted // (2) @UploadFileNotEmpty private List<MultipartFile> files; // omitted getter/setter methods. }
項番 説明 (1) 複数のアップロードファイルを保持するためのフォームオブジェクト。 (2)MultipartFile
クラスをリストとして宣言する。上記例では、入力チェックとして、ファイルが空でないことを検証するためのアノテーションを指定している。本来は他の必須チェックやファイルのサイズチェックなども必要であるが、上記例では割愛している。
5.17.2.5.2. Validatorの実装¶
コレクションに格納されている複数の MultipartFile
オブジェクトに対して入力チェックを行う場合は、コレクション用のValidatorを実装する必要がある。
以下では、単一ファイル用に作成したValidatorを利用してコレクション用のValidatorを作成する方法について説明する。
// (1) public class UploadFileNotEmptyForCollectionValidator implements ConstraintValidator<NotEmptyUploadFile, Collection<MultipartFile>> { // (2) private final UploadFileNotEmptyValidator validator = new UploadFileNotEmptyValidator(); // (3) @Override public void initialize(NotEmptyUploadFile constraintAnnotation) { validator.initialize(constraintAnnotation); } // (4) @Override public boolean isValid(Collection<MultipartFile> values, ConstraintValidatorContext context) { for (MultipartFile file : values) { if (!validator.isValid(file, context)) { return false; } } return true; } }
項番 説明 (1) 全てのファイルが空でないことを検証するための実装を行うクラス。検証対象となる値の型として、Collection<MultipartFile>
を指定する。 (2) 実際の処理は単一ファイル用のValidatorに委譲するため、単一ファイル用のValidatorのインスタンスを作成しておく。 (3) Validatorを初期化する。上記例では、実際の処理を行う単一ファイル用のValidatorの初期化を行っている。 (4) 全てのファイルが空でないことを検証する。上記例では、単一ファイル用のValidatorのメソッドを呼び出して、1ファイルずつ検証を行っている。@Target({ ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE }) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = {UploadFileNotEmptyValidator.class, UploadFileNotEmptyForCollectionValidator.class}) // (5) public @interface UploadFileNotEmpty { // omitted }
項番 説明 (5) 複数のファイルに対してチェックを行うValidatorクラスを、検証用アノテーションに追加する。@Constraint
アノテーションのvalidatedBy属性に、(1)で作成したクラスを指定する。こうすることで、@NotEmptyUploadFile
アノテーションを付与したプロパティに対する妥当性チェックを行う際に、(1)で作成したクラスが実行される。
5.17.2.5.3. JSPの実装¶
<form:form action="${pageContext.request.contextPath}/article/uploadFiles" method="post" modelAttribute="filesUploadForm2" enctype="multipart/form-data"> <table> <tr> <th width="35%">File to upload</th> <td width="65%"> <form:input type="file" path="files" multiple="multiple" /> <!-- (1) --> <form:errors path="files" /> </td> </tr> </table> <div> <form:button>Upload</form:button> </div> </form:form>
項番 説明 (1) path属性には フォームオブジェクトのプロパティ名を指定し、 multiple属性を指定する。multiple属性を指定すると、HTML5をサポートしているブラウザで複数のファイルを選択しアップロードすることができる。
5.17.2.5.4. Controllerの実装¶
@RequestMapping(value = "uploadFiles", method = RequestMethod.POST) public String uploadFiles(@Validated FilesUploadForm form, BindingResult result, RedirectAttributes redirectAttributes) { if (result.hasErrors()) { return "article/uploadForm"; } // (1) for (MultipartFile file : form.getFiles()) { // omit processing of upload. } redirectAttributes.addFlashAttribute(ResultMessages.success().add( "i.xx.at.0001")); return "redirect:/article/upload?complete"; }
項番 説明 (1) フォームオブジェクトからMultipartFile
オブジェクトが格納されているリストを取得し、アップロード処理を実装する。上記例では、具体的な実装は省略しているが、共有ディスクやデータベースへ保存する処理を行うことになる。
5.17.2.6. 仮アップロード¶
アップロード結果の確認画面など、画面遷移の途中でファイルをアップロードする場合、仮アップロードという考え方が必要になる。
Note
MultipartFile
オブジェクトで保持しているファイルの中身は、アップロードしたリクエストが完了した時点で消滅する可能性がある。 そのため、ファイルの中身をリクエストを跨いで扱いたい場合は、MultipartFile
オブジェクトで保持しているファイルの中身や、メタ情報(ファイル名など)をファイルやフォームに退避する必要がある。
MultipartFile
オブジェクトで保持しているファイルの中身は、下記処理フローの(3)が完了した時点で、消滅する。
項番 説明 (1) 入力画面にて、アップロードするファイルを選択し、確認画面に遷移するためのリクエストを送信する。 (2) Controllerは、アップロードされたファイルの中身を、アプリケーション用の仮ディレクトリに一時保存する。 (3) Controllerは、確認画面のView名を返却し、確認画面に遷移する。 (4) 確認画面にて、処理を実行するためのリクエストを送信する。 (5) Controllerは、Serviceのメソッドを呼び出し、処理を実行する。 (6) Serviceは、仮ディレクトリに格納されている一時ファイルを、本ディレクトリまたはデータベースに移動する。 (7) Controllerは、完了画面を表示するためのView名を返却し、完了画面に遷移する。Note
仮アップロードの処理は、アプリケーション層の役割なので、Controller又はHelperクラスで実装することになる。
5.17.2.6.1. Controllerの実装¶
以下に、アップロードされたファイルを仮ディレクトリに一時保存する実装例を示す。
@Component public class UploadHelper { // (2) @Value("${app.upload.temporaryDirectory}") private File uploadTemporaryDirectory; // (1) public String saveTemporaryFile(MultipartFile multipartFile) throws IOException { String uploadTemporaryFileId = UUID.randomUUID().toString(); File uploadTemporaryFile = new File(uploadTemporaryDirectory, uploadTemporaryFileId); // (2) FileUtils.copyInputStreamToFile(multipartFile.getInputStream(), uploadTemporaryFile); return uploadTemporaryFileId; } }
項番 説明 (1) 仮アップロードを行うためのメソッドをHelperクラスに作成する。ファイルアップロードを行う処理が複数ある場合は、共通的なHelperメソッドを用意し、仮アップロード処理を共通化することを推奨する。 (2) アップロードしたファイルを一時ファイルとして保存する。上記例では、org.apache.commons.io.FileUtils
クラスの copyInputStreamToFileメソッドを呼び出し、アップロードしたファイルの中身をファイルに保存している。// omitted @Inject UploadHelper uploadHelper; @RequestMapping(value = "upload", method = RequestMethod.POST, params = "confirm") public String uploadConfirm(@Validated FileUploadForm form, BindingResult result) throws IOException { if (result.hasErrors()) { return "article/uploadForm"; } // (3) String uploadTemporaryFileId = uploadHelper.saveTemporaryFile(form .getFile()); // (4) form.setUploadTemporaryFileId(uploadTemporaryFileId); form.setFileName(form.getFile().getOriginalFilename()); return "article/uploadConfirm"; }
項番 説明 (3) アップロードファイルを一時保存するためのHelperメソッドを呼び出す。上記例では、一時保存したファイルの識別するためのIDがHelperメソッドの返り値として返却される。 (4) アップロードしたファイルのメタ情報(ファイルを識別するためのID、ファイル名など)をフォームオブジェクトに格納する。上記例では、アップロードファイルのファイル名と一時保存したファイルを識別するためのIDをフォームオブジェクトに格納している。Note
仮ディレクトリのディレクトリは、アプリケーションをデプロイする環境によって異なる可能性があるため、外部プロパティから取得すること。 外部プロパティの詳細については、プロパティ管理を参照されたい。
Warning
上記例では、アプリケーションサーバ上のローカルディスクに一時保存する例としているが、アプリケーションサーバがクラスタ化されている場合は、 データベース又は共有ディスクに保存する必要がでてくるので、非機能要件も考慮して保存先を設計する必要がある。
データベースに保存する場合は、トランザクション管理が必要となるため、 データベースに保存す処理をServiceのメソッドに委譲することになる。
5.17.3. How to extend¶
5.17.3.1. 仮アップロード時の不要ファイルのHousekeeping¶
- 仮アップロード後の画面操作を中止した場合
- 仮アップロード後の画面操作中にシステムエラーが発生した場合
- 仮アップロード後の画面操作中にサーバが停止した場合
- etc …
Warning
不要なファイルを残したままにすると、ディスクを圧迫する可能性があるため、必ず不要なファイルを削除する仕組みを用意すること。
本ガイドラインでは、Spring Frameworkから提供されている「Task Scheduler」機能を使用して、不要なファイルを削除する方法について説明する。 「Task Scheduler」の詳細については、公式リファレンスの”Task Execution and Scheduling”を参照されたい。
Note
ガイドラインとしては、Spring Frameworkから提供されている「Task Scheduler」機能を使用する方法について説明するが、使用を強制するものではない。 実際のプロジェクトでは、インフラチームによって不要なファイルを削除するバッチアプリケーション(Shellアプリケーション)が提供されるケースがある。 その場合は、インフラチーム作成のバッチアプリケーションを使用して不要なファイルを削除することを推奨する。
5.17.3.1.1. 不要ファイルを削除するコンポーネントクラスの実装¶
不要なファイルを削除するコンポーネントクラスを実装する。
package com.examples.common.upload; import java.io.File; import java.util.Collection; import java.util.Date; import javax.inject.Inject; import org.apache.commons.io.FileUtils; import org.apache.commons.io.filefilter.FileFilterUtils; import org.apache.commons.io.filefilter.IOFileFilter; import org.springframework.beans.factory.annotation.Value; import org.terasoluna.gfw.common.date.DateFactory; // (1) public class UnnecessaryFilesCleaner { @Inject private DateFactory dateFactory; @Value("${app.upload.temporaryFileSavedPeriodMinutes}") private int savedPeriodMinutes; @Value("${app.upload.temporaryDirectory}") private File targetDirectory; // (2) public void cleanup() { // calculate cutoff date. Date cutoffDate = dateFactory.newDateTime().minusMinutes( savedPeriodMinutes).toDate(); // collect target files. IOFileFilter fileFilter = FileFilterUtils.ageFileFilter(cutoffDate); Collection<File> targetFiles = FileUtils.listFiles(targetDirectory, fileFilter, null); if (targetFiles.isEmpty()) { return; } // delete files. for (File targetFile : targetFiles) { FileUtils.deleteQuietly(targetFile); } } }
項番 説明 (1) 不要なファイルを削除するためのコンポーネントクラスを作成する。 (2) 不要なファイルを削除するメソッドを実装する。上記例では、ファイルの最終更新日時から、一定期間更新がないファイルを、不要ファイルとして削除している。Note
削除対象ファイルが格納されているディレクトリのパスや、削除基準となる時間などは、アプリケーションをデプロイする環境によって異なる可能性があるため、外部プロパティから取得すること。 外部プロパティの詳細については、プロパティ管理を参照されたい。
5.17.3.1.2. 不要ファイルを削除する処理のスケジューリング設定¶
不要ファイルを削除するPOJOクラスを、bean登録とタスクスケジュールの設定を行う。
applicationContext.xml
<!-- omitted --> <!-- (3) --> <bean id="uploadTemporaryFileCleaner" class="com.examples.common.upload.UnnecessaryFilesCleaner" /> <!-- (4) --> <task:scheduler id="fileCleanupTaskScheduler" /> <!-- (5) --> <task:scheduled-tasks scheduler="fileCleanupTaskScheduler"> <!-- (6)(7)(8) --> <task:scheduled ref="uploadTemporaryFileCleaner" method="cleanup" cron="${app.upload.temporaryFilesCleaner.cron}"/> </task:scheduled-tasks> <!-- omitted -->
項番 説明 (3) 不要ファイルを削除するPOJOクラスをbean登録する。上記例では、"uploadTemporaryFileCleaner"
というIDで登録している。 (4) 不要ファイルを削除する処理を、実行するためのタスクスケジューラのbeanを、登録する。上記例では、pool-size属性を省略しているため、このタスクスケジュールは、シングルスレッドでタスクを実行する。複数のタスクを同時に実行する必要がある場合は、 pool-size属性に任意の数字を指定すること。 (5) 不要ファイルを削除するタスクスケジューラに、タスクを追加する。上記例では、(4)でbean登録したタスクスケジューラに対して、タスクを追加している。 (6) ref属性に、不要ファイルを削除する処理が実装されているbeanを、指定する。上記例では、(3)で登録したbeanを指定している。 (7) method属性に、不要ファイルを削除する処理が実装されているメソッド名を、指定する。上記例では、(3)で登録したbeanのcleanupメソッドを指定している。 (8) cron属性に、不要ファイルを削除する処理の実行タイミングを指定する。上記例では、外部プロパティよりcron定義を取得している。Note
cron属性の設定値は、「秒 分 時 月 年 曜日」の形式で指定する。
設定例)
0 */15 * * * *
: 毎時 0分,15分,30分,45分に実行される。0 0 * * * *
: 毎時 0分に実行される。0 0 9-17 * * MON-FRI
: 平日9時~17時の間の毎時0分に実行される。cronの指定値の詳細については、CronSequenceGeneratorのJavaDocを参照されたい。
実行タイミングは、アプリケーションをデプロイする環境によって異なる可能性があるため、外部プロパティから取得すること。 外部プロパティの詳細については、プロパティ管理を参照されたい。
Tip
上記例では、タスクの実行トリガーとしてcronを使用しているが、cron以外に、fixed-delayとfixed-rateが、デフォルトで用意されているので、要件に応じて使い分けること。
デフォルトで用意されているトリガーでは要件を満たせない場合は、trigger属性に
org.springframework.scheduling.Trigger
を実装したbeanを指定することで、独自のトリガーを設けることもできる。
5.17.4. Appendix¶
以下に、対策方針について説明する。
5.17.4.1. アップロード機能に対するDos攻撃¶
アップロード機能に対するDos攻撃とは、巨大なサイズのファイルを連続してアップロードしてサーバに対して負荷を掛けることで、 サーバのダウンや、レスポンス速度の低下を狙った攻撃方法のことである。
<multipart-config>
要素を用いて、リクエストのサイズ制限を設ける必要がある。5.17.4.2. アップロードしたファイルをWebサーバ上で実行する攻撃¶
この攻撃への対策方法は、以下の通りである。
- アップロードされたファイルを、Webサーバ(アプリケーションサーバ)上の公開ディレクトリに配置せず、ファイルの中身を表示するための処理を経由して、アップロードしたファイルの中身を閲覧させる。
- アップロード可能なファイルの拡張子を制限し、Webサーバ(アプリケーションサーバ)で実行可能なスクリプトファイルが、アップロードされないようにする。
いずれかの対策を行うことで攻撃を防ぐことができるが、両方とも対策しておくことを推奨する。