복붙노트

[SPRING] 스프링 CSRF 토큰과 함께 Struts2에서 파일 업로드

SPRING

스프링 CSRF 토큰과 함께 Struts2에서 파일 업로드

나는 사용한다,

CSRF 공격을 막기 위해 내장 된 보안 토큰을 사용합니다.

<s:form namespace="/admin_side"
        action="Category"
        enctype="multipart/form-data"
        method="POST"
        validate="true"
        id="dataForm"
        name="dataForm">

    <s:hidden name="%{#attr._csrf.parameterName}"
              value="%{#attr._csrf.token}"/>
</s:form>

다중 파트 요청이 Spring에 의해 처리되도록 MultipartResolver와 함께 MultipartFilter가 적절히 구성되어 있지 않으면 SpringFOR 보안에 CSRF 토큰을 사용할 수없는 다중 요청입니다.

web.xml의 MultipartFilter는 다음과 같이 구성됩니다.

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0"
         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">

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>
            /WEB-INF/applicationContext.xml
            /WEB-INF/spring-security.xml
        </param-value>
    </context-param>

    <filter>
        <filter-name>MultipartFilter</filter-name>
        <filter-class>org.springframework.web.multipart.support.MultipartFilter</filter-class>
    </filter>

    <filter>
        <filter-name>springSecurityFilterChain</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    </filter>

    <filter-mapping>
        <filter-name>MultipartFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    <filter-mapping>
        <filter-name>springSecurityFilterChain</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    <filter>
        <filter-name>AdminLoginNocacheFilter</filter-name>
        <filter-class>filter.AdminLoginNocacheFilter</filter-class>
    </filter>

    <filter-mapping>
        <filter-name>AdminLoginNocacheFilter</filter-name>
        <url-pattern>/admin_login/*</url-pattern>
    </filter-mapping>

    <filter>
        <filter-name>NoCacheFilter</filter-name>
        <filter-class>filter.NoCacheFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>NoCacheFilter</filter-name>
        <url-pattern>/admin_side/*</url-pattern>
    </filter-mapping>

    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    <listener>
        <description>Description</description>
        <listener-class>org.springframework.web.context.request.RequestContextListener</listener-class>
    </listener>

    <listener>
        <listener-class>org.springframework.security.web.session.HttpSessionEventPublisher</listener-class>
    </listener>

    <filter>
        <filter-name>struts2</filter-name>
        <filter-class>org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter</filter-class>
        <init-param>
            <param-name>struts.devMode</param-name>
            <param-value>true</param-value>
        </init-param>
    </filter>

    <filter-mapping>
        <filter-name>struts2</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    <session-config>
        <session-timeout>
            30
        </session-timeout>
    </session-config>
    <welcome-file-list>
        <welcome-file>index.jsp</welcome-file>
    </welcome-file-list>
</web-app>

그리고 applicationContext.xml에서 MultipartResolver는 다음과 같이 등록된다.

<bean id="filterMultipartResolver" 
      class="org.springframework.web.multipart.commons.CommonsMultipartResolver">

    <property name="maxUploadSize" value="-1" />
</bean>

이제 CSRF 토큰은 Spring 보안에 의해 수신되지만, 그렇게하면 Struts에서 또 다른 문제가 발생합니다.

업로드 된 파일은 다음과 같이 Struts 액션 클래스에서 null이되었습니다.

@Namespace("/admin_side")
@ResultPath("/WEB-INF/content")
@ParentPackage(value="struts-default")
public final class CategoryAction extends ActionSupport implements Serializable, ValidationAware, ModelDriven<Category>
{
    private File fileUpload;
    private String fileUploadContentType;
    private String fileUploadFileName;
    private static final long serialVersionUID = 1L;

    //Getters and setters.

    //Necessary validators as required.
    @Action(value = "AddCategory",
        results = {
            @Result(name=ActionSupport.SUCCESS, type="redirectAction", params={"namespace", "/admin_side", "actionName", "Category"}),
            @Result(name = ActionSupport.INPUT, location = "Category.jsp")},
        interceptorRefs={
            @InterceptorRef(value="defaultStack", "validation.validateAnnotatedMethodOnly", "true"})
        })
    public String insert(){
        //fileUpload, fileUploadContentType and fileUploadFileName are null here after the form is submitted.
        return ActionSupport.SUCCESS;
    }

    @Action(value = "Category",
            results = {
                @Result(name=ActionSupport.SUCCESS, location="Category.jsp"),
                @Result(name = ActionSupport.INPUT, location = "Category.jsp")},
            interceptorRefs={
                @InterceptorRef(value="defaultStack", params={ "validation.validateAnnotatedMethodOnly", "true", "validation.excludeMethods", "load"})})
    public String load() throws Exception{
        //This method is just required to return an initial view on page load.
        return ActionSupport.SUCCESS;
    }
}

이것은 내 생각에 다중 요청은 이미 Spring에 의해 처리되고 소비 되었기 때문에 다중 요청으로 Struts에서 사용할 수 없기 때문에 Struts 액션 클래스의 파일 객체는 null입니다.

이 상황을 극복 할 수있는 방법이 있습니까? 그렇지 않으면, 나는 지금 낙오되고 전혀 추천되지 않는 질의 - 문자열 매개 변수로서 토큰을 URL에 추가하는 유일한 옵션을 남겼다.

<s:form namespace="/admin_side"
        action="Category?%{#attr._csrf.parameterName}=%{#attr._csrf.token}"
        enctype="multipart/form-data"
        method="POST"
        validate="true"
        id="dataForm"
        name="dataForm">
    ...
<s:form>

긴 이야기 짧게 : Spring이 mulipart 요청을 처리하도록 만든다면 Struts 액션 클래스에서 파일을 얻는 방법? 반면에, Spring이 다중 요청을 처리하지 않으면 보안 토큰을 대기시킵니다. 이 상황을 극복하는 방법?

해결법

  1. ==============================

    1.가장 좋은 방법은 Spring의 MultipartRequest에 위임하는 커스텀 MultiPartRequest 구현을 만드는 것입니다. 다음은 구현 예입니다.

    가장 좋은 방법은 Spring의 MultipartRequest에 위임하는 커스텀 MultiPartRequest 구현을 만드는 것입니다. 다음은 구현 예입니다.

    샘플 / SpringMultipartParser.java

    package sample;
    
    import java.io.File;
    import java.io.IOException;
    import java.util.ArrayList;
    import java.util.Collections;
    import java.util.Enumeration;
    import java.util.List;
    import java.util.Map.Entry;
    
    import javax.servlet.http.HttpServletRequest;
    
    import org.apache.struts2.dispatcher.multipart.MultiPartRequest;
    import org.springframework.util.LinkedMultiValueMap;
    import org.springframework.util.MultiValueMap;
    import org.springframework.web.multipart.MultipartFile;
    import org.springframework.web.multipart.MultipartHttpServletRequest;
    import org.springframework.web.util.WebUtils;
    
    import com.opensymphony.xwork2.util.logging.Logger;
    import com.opensymphony.xwork2.util.logging.LoggerFactory;
    
    public class SpringMultipartParser implements MultiPartRequest {
        private static final Logger LOG = LoggerFactory.getLogger(MultiPartRequest.class);
    
        private List<String> errors = new ArrayList<String>();
    
        private MultiValueMap<String, MultipartFile> multipartMap;
    
        private MultipartHttpServletRequest multipartRequest;
    
        private MultiValueMap<String, File> multiFileMap = new LinkedMultiValueMap<String, File>();
    
        public void parse(HttpServletRequest request, String saveDir)
                throws IOException {
            multipartRequest =
                    WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class);
    
            if(multipartRequest == null) {
                LOG.warn("Unable to MultipartHttpServletRequest");
                errors.add("Unable to MultipartHttpServletRequest");
                return;
            }
            multipartMap = multipartRequest.getMultiFileMap();
            for(Entry<String, List<MultipartFile>> fileEntry : multipartMap.entrySet()) {
                String fieldName = fileEntry.getKey();
                for(MultipartFile file : fileEntry.getValue()) {
                    File temp = File.createTempFile("upload", ".dat");
                    file.transferTo(temp);
                    multiFileMap.add(fieldName, temp);
                }
            }
        }
    
        public Enumeration<String> getFileParameterNames() {
            return Collections.enumeration(multipartMap.keySet());
        }
    
        public String[] getContentType(String fieldName) {
            List<MultipartFile> files = multipartMap.get(fieldName);
            if(files == null) {
                return null;
            }
            String[] contentTypes = new String[files.size()];
            int i = 0;
            for(MultipartFile file : files) {
                contentTypes[i++] = file.getContentType();
            }
            return contentTypes;
        }
    
        public File[] getFile(String fieldName) {
            List<File> files = multiFileMap.get(fieldName);
            return files == null ? null : files.toArray(new File[files.size()]);
        }
    
        public String[] getFileNames(String fieldName) {
            List<MultipartFile> files = multipartMap.get(fieldName);
            if(files == null) {
                return null;
            }
            String[] fileNames = new String[files.size()];
            int i = 0;
            for(MultipartFile file : files) {
                fileNames[i++] = file.getOriginalFilename();
            }
            return fileNames;
        }
    
        public String[] getFilesystemName(String fieldName) {
            List<File> files = multiFileMap.get(fieldName);
            if(files == null) {
                return null;
            }
            String[] fileNames = new String[files.size()];
            int i = 0;
            for(File file : files) {
                fileNames[i++] = file.getName();
            }
            return fileNames;
        }
    
        public String getParameter(String name) {
            return multipartRequest.getParameter(name);
        }
    
        public Enumeration<String> getParameterNames() {
            return multipartRequest.getParameterNames();
        }
    
        public String[] getParameterValues(String name) {
            return multipartRequest.getParameterValues(name);
        }
    
        public List getErrors() {
            return errors;
        }
    
        public void cleanUp() {
            for(List<File> files : multiFileMap.values()) {
                for(File file : files) {
                    file.delete();
                }
            }
    
            // Spring takes care of the original File objects
        }
    }
    

    다음으로 Struts가이를 사용하는지 확인해야합니다. 아래와 같이 struts.xml 파일에서이 작업을 수행 할 수 있습니다.

    struts.xml

    <constant name="struts.multipart.parser" value="spring"/>
    <bean type="org.apache.struts2.dispatcher.multipart.MultiPartRequest" 
          name="spring" 
          class="sample.SpringMultipartParser"
          scope="default"/>
    

    경고 : 빈의 범위를 적절하게 설정하여 모든 멀티 파트 요청에 대해 MultipartRequest의 새 인스턴스가 생성되도록하는 것이 절대적으로 필요합니다. 그렇지 않으면 경쟁 조건이 표시됩니다.

    이렇게하면 Struts 액션은 이전과 마찬가지로 파일 정보를 추가하게됩니다. 파일 유효성 검사 (즉, 파일 크기)는 이제 Struts 대신 filterMultipartResolver를 사용하여 수행됩니다.

    테마를 사용하여 CSRF 토큰 자동 포함

    폼에 CSRF 토큰을 자동으로 포함 할 수 있도록 사용자 지정 테마를 만드는 것이 좋습니다. 이를 수행하는 방법에 대한 자세한 내용은 http://struts.apache.org/release/2.3.x/docs/themes-and-templates.html을 참조하십시오.

    Github에 대한 완전한 예

    github에 대한 전체 샘플은 https://github.com/rwinch/struts2-upload에서 찾을 수 있습니다.

  2. ==============================

    2.multipart / formdata를 인코딩하는 양식은 파일 업로드 시나리오에 사용하기위한 것이고, 이것은 W3C 문서에 따른 것입니다 :

    multipart / formdata를 인코딩하는 양식은 파일 업로드 시나리오에 사용하기위한 것이고, 이것은 W3C 문서에 따른 것입니다 :

    MultipartResolver 클래스는 다른 폼 필드가 아닌 파일 업로드만을 기대하며 이것은 javadoc에서 온 것입니다 :

    /**
     * A strategy interface for multipart file upload resolution in accordance
     * with <a href="http://www.ietf.org/rfc/rfc1867.txt">RFC 1867</a>.
     *
     */
    

    따라서 CSRF를 양식 필드로 추가하는 것이 효과적이지 않은 이유는 CSRF 공격에 대한 파일 업로드 요청을 보호하는 일반적인 방법은 POST 본문 대신 HTTP 요청 헤더에 CSRF 토큰을 보내는 것입니다. 이를 위해 ajax POST를 작성해야합니다.

    정상적인 POST의 경우이 방법은 없습니다.이 대답을 참조하십시오. POST에 아약스 요청을 만들고 일부 자바 스크립트와 함께 헤더를 추가하거나 앞에서 언급 한 것처럼 CSRF 토큰을 URL 매개 변수로 보냅니다.

    CSRF 토큰이 요청 사이에 있어야하기 때문에 자주 재생성되면 요청 매개 변수로 보내는 것이 문제가되지 않고 받아 들일 수 있습니다.

    서버 측에서는 헤더에서 토큰을 읽도록 CSRF 솔루션을 구성해야합니다. 이는 일반적으로 사용되는 CSRF 솔루션에 의해 예측됩니다.

  3. ==============================

    3.첫눈에 당신의 구성이 나에게 맞는 것처럼 보입니다. 그러므로 나는이 문제가 어딘가 약간의 잘못된 설정일지도 모른다고 생각한다.

    첫눈에 당신의 구성이 나에게 맞는 것처럼 보입니다. 그러므로 나는이 문제가 어딘가 약간의 잘못된 설정일지도 모른다고 생각한다.

    Spring Security 팀의 도움으로 해결할 수 있었던 Struts 대신 Spring MVC와 비슷한 문제에 직면했습니다. 자세한 내용은이 답변을 참조하십시오.

    Github에서 제공되는 작업 샘플과 설정을 비교할 수도 있습니다. Tomcat 7, JBoss AS 7, Jetty 및 Weblogic에서 테스트했습니다.

    이러한 방법으로 문제가 해결되지 않으면 구성을 단일 페이지 응용 프로그램으로 생성하여 문제를 보여주고 어딘가에 업로드 할 수 있다면 도움이 될 것입니다.

  4. ==============================

    4.Struts 사용자는 아니지만 Spring MultipartFilter가 요청을 MultipartHttpServletRequest로 래핑한다는 사실을 사용할 수 있다고 생각합니다.

    Struts 사용자는 아니지만 Spring MultipartFilter가 요청을 MultipartHttpServletRequest로 래핑한다는 사실을 사용할 수 있다고 생각합니다.

    먼저 HttpServletRequest를 잡아라. Struts에서는 다음과 같이 할 수 있다고 생각한다 :

    ServletRequest request = ServletActionContext.getRequest();
    

    그런 다음 필요한 경우 래퍼를 래핑하여 MultipartRequest를 검색합니다.

    MultipartRequest multipart = null;
    while (multipart == null)
    {
        if (request instanceof MultipartRequest)
            multipart = (MultipartRequest)request;
        else if (request instanceof ServletRequestWrapper)
            request = ((ServletRequestWrapper)request).getRequest();
        else
            break;                
    }
    

    이 요청이 멀티 파트 인 경우 양식 입력 이름으로 파일을 가져옵니다.

    if (multipart != null)
    {
        MultipartFile mf = multipart.getFile("forminputname");
        // do your stuff
    }
    
  5. from https://stackoverflow.com/questions/21533760/file-upload-in-struts2-along-with-the-spring-csrf-token by cc-by-sa and MIT license