스프링 부트

[SpringBoot] ajax - POST방식으로 PUT, PATCH, DELETE 사용하기! (HiddenHttpMethodFilter)

h__hj 2022. 8. 15. 15:55

# ajax - POST방식으로 PUT, PATCH, DELETE 사용하기 (HiddenHttpMethodFilter)

 프로젝트를 진행하면, 보안상의 이유로 http method 중 GET, POST 만 사용 가능 할 때가 있다.
그렇지만 굳이 굳이 @PutMapping, @PatchMapping, @DeleteMapping 를 사용하고 싶을 때, HiddenHttpMethodFilter를 적용하여 POST방식으로 요청하고 PUT, PATCH, DELETE 메소드를 @RequestParam으로 파라미터로 맵핑!

# HTTP METHOD 종류

HTTP에서 지원하는 요청 메시지는 다음과 같다.
 - GET: 클라이언트가 서버에게 URL에 해당하는 자료의 전송을 요청한다.
 - HEAD: GET 요청으로 반환될 데이터 중 헤더 부분에 해당하는 데이터만 요청한다.
 - POST: 클라이언트가 서버에서 처리할 수 있는 자료를 보낸다. 예를 들어, 게시판에 글을 쓸 때 클라이언트의 문서가 서버로 전송되어야 한다. 멱등성을 보장하지 않는다.
 - PATCH: 클라이언트가 서버에게 지정한 URL의 데이터를 부분적으로 수정할 것을 요청한다.
 - PUT: 클라이언트가 서버에게 지정한 URL에 지정한 데이터를 저장할 것을 요청한다.
 - DELETE: 클라이언트가 서버에게 지정한 URL의 정보를 제거할 것을 요청한다.
 - TRACE: 클라이언트가 서버에게 송신한 요청의 내용을 반환해 줄 것을 요청한다.
 - CONNECT: 클라이언트가 특정 종류의 프록시 서버에게 연결을 요청한다.
 - OPTIONS: 해당 URL에서 지원하는 요청 메세지의 목록을 요청한다.
 이 중 GET 과 HEAD 요청은 원칙적으로 이를 호출한다고 해서 서버 측의 데이터에 변화가 있어서는 안 된다. 이를 Safe Method 라고 분류한다. 또한, GET, HEAD, PUT, DELETE 는 동일한 요청이 한번 전송되었을 때와 여러 번 연속하여 전송되었을 때의 서버 측의 처리 결과가 동일해야 한다. 이를 Idempotent Method 라고 분류한다.

참조:  https://namu.wiki/w/HTTP - (5.1)  라고 함.
 
이슈: https://softwareengineering.stackexchange.com/questions/114156/why-are-there-no-put-and-delete-methods-on-html-forms
 
조금 더 궁금한 사람들은 찾아보기 바람.

# 환경

/**
 * tool: STS 4.13.0
 * version: 2.7.3-SNAPSHOT
 * java: 1.8
 * type: MAVEN
 * view: THYMELEAF
 * jQuery: 3.6.0
 */

# HiddenHttpMethodFilter

public class CommonMethodFilter extends HiddenHttpMethodFilter {

	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException 
	{
		if(FilterConfig.isStatic(request.getRequestURI())) {
			filterChain.doFilter(request, response);
		} else {
			super.doFilterInternal(request, response, filterChain);
		}
	}
}

# Configuration

@Configuration
public class FilterConfig {
	
	public static boolean isStatic(String uri) {
		if(uri.startsWith("/js") || uri.startsWith("/css") || uri.startsWith("/images") || uri.startsWith("/favicon.ico")) {
			return true;
		} 
		return false;
	}
	/**
	 *  이 필터는 POST 본문 매개변수를 확인해야 하는 고유한 필요성 때문에
	 * 다중 부분 POST 요청의 경우 다중 부분 처리 후에 실행해야 합니다.
	 * 따라서 일반적으로 이 HiddenHttpMethodFilter 앞에 Spring MultipartFilter를 배치합니다.
	 * Only POST, Only application/x-www-form-urlencoded; charset=utf-8
	 */
	@Bean
	public FilterRegistrationBean<CommonMethodFilter> methodFilter() {
		
		String[] urlPatterns = new String[] {"/test/send/*"};
		
		CommonMethodFilter commonMethodFilter = new CommonMethodFilter();
		commonMethodFilter.setMethodParam("_methodName");
		
		FilterRegistrationBean<CommonMethodFilter> bean = new FilterRegistrationBean<>(commonMethodFilter);
		
		bean.addUrlPatterns(urlPatterns);
		bean.setOrder(1);
		bean.setEnabled(true);
		return bean;
	}
}

# view

# html (일부분 발췌)

<h3>PUT, PATCH, DELETE (application/x-www-form-urlencoded)</h3>
<div>
    <input type="text" id="requsetsNew" placeholder="Put, Delete Test" style="display: inline;"/>
    <br>
    <input type="button" onclick="test.requsetsNew('put')" value="PUT" style="display: inline;"/>
    <input type="button" onclick="test.requsetsNew('patch')" value="PATCH" style="display: inline;"/>
    <input type="button" onclick="test.requsetsNew('delete')" value="DELETE" style="display: inline;"/>
</div>

# ajax send function (일부분 발췌)

const test = {
    requsetsNew: function(method) {
        const input = {
            param1: $('#requsetsNew').val(),
            param2: method,
            _methodName: method
        }
        sendRequestNew(`/test/send/${method}`, 'post', input,
            function(result) {
                console.log(result);
            },
        );
    },
}
function sendRequest(url, method, input, cbSuccess) {
    $.ajax({
        // options
            url         : url,
            method      : (method) ? method : "POST",
            data        : input,
            dataType    : "json", 
            beforeSend  : function(jqXHR, settings) {}
        // success
        }).done(function(output, textStatus, jqXHR) {
            cbSuccess(output);
        // error	
        }).fail(function(jqXHR) {
            console.log("ERROR");
        // complete
        }).always(function(output, textStatus, jqXH) {
            console.log("COMPLETE");
        }
    );
}

# @Controller (일부분 발췌)

@Slf4j
@Controller
@RequestMapping("/test/send")
public class TestSendController {

	@PutMapping("/put")
	public @ResponseBody Map<String, String> put(@RequestParam Map<String, String> input) {
		log.debug("[PARAMETER] {}", input);
		return input;
	}
	@PatchMapping("/patch")
	public @ResponseBody Map<String, String> patch(@RequestParam Map<String, String> input) {
		log.debug("[PARAMETER] {}", input);
		return input;
	}
	@DeleteMapping("/delete")
	public @ResponseBody Map<String, String> delete(@RequestParam Map<String, String> input) {
		log.debug("[PARAMETER] {}", input);
		return input;
	}
}

# 내용

 HiddenHttpMethodFilter 를 적용하여 @PutMapping, @PatchMapping, @DeleteMapping 을 사용할때에는 몇가지 제약이 있다.

  • http method는 POST만 사용할 것.
  • contentType은 application/x-www-form-urlencoded 를 사용할 것.
  • setMethodParam("_methodName") 이용하여 파라미터명을 변경하면, 해당 변수명으로 메소드 값을 넣어줄 것.
    • (default) _method
  • POST에서 메소드 변경은 PUT, PATCH, DELETE만 가능하다!

# ajax 요청 방식에 따른 파라미터 맵핑 방법.

https://hjho95.tistory.com/14 ajax settings
https://hjho95.tistory.com/7 application/x-www-form-urlencoded
https://hjho95.tistory.com/8 application/json
https://hjho95.tistory.com/9 form tag
https://hjho95.tistory.com/10 multipart/form-data
https://hjho95.tistory.com/12 PUT, PATCH, DELETE
https://hjho95.tistory.com/11 POST 방식으로 PUT, PATCH, DELETE 사용하기

# ajax 시리즈의 참조 페이지.

HiddenHttpMethodFilter: https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/filter/HiddenHttpMethodFilter.html