CSRF AJAX 사용시 설정및 실행
haed 태그 사이에 아래와 같은 코드.
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"> <head> <meta name="_csrf" th:content="${_csrf.token}"/> <meta name="_csrf_header" th:content="${_csrf.headerName}"/> <title>CSRF TEST</title> </head>
HTML Body 안에 들어갈 내용입니다. input 안에있는 userName을 ajax로 보낼 예정.
<!--AJAX Submit--> <div class="panel-heading"></div> <input autocomplete="off" type="text" class="form-control" id="userName" name="name"> <button class="btn btn-md btn-danger btn-block" id="ajaxCall" type="button">AJAX Call</button>
meta 태그 안에있는 content를 가져와서 AJAX beforeSend(데이터를 전송하기전) 에 HTTP Header를 SET
<script type="text/javascript"> $(function() { $("#ajaxCall").click(function(){ ajaxCall(); }); }); function ajaxCall(){ var token = $("meta[name='_csrf']").attr("content"); var header = $("meta[name='_csrf_header']").attr("content"); var name = $("#userName").val(); var jsonData = { "name" : name } $.ajax({ type: 'POST', contentType: "application/json", url:'/csrf/ajax', data: JSON.stringify(jsonData), // String -> json 형태로 변환 beforeSend : function(xhr) { /*데이터를 전송하기 전에 헤더에 csrf값을 설정한다*/ xhr.setRequestHeader(header, token); }, dataType: 'json', // success 시 받아올 데이터 형 async: true, //동기, 비동기 여부 cache :false, // 캐시 여부 success: function(data){ console.log(data.name); }, error:function(xhr,status,error){ console.log('error:'+error); } }); } </script>
컨트롤러와 VO객체
AJAX로 받아온 컨트롤러에는 CSRF 관련 소스를 작성할 필요가 없습니다.
시큐리티가 내부적으로 다 해줍니다.
일반적인 AJAX 호출과 동일 합니다.
@PostMapping("/csrf/ajax") public @ResponseBody CsrfVO csrfAJAXSubmit(@RequestBody CsrfVO csrfVO) { return csrfVO; }
VO객체는 아래와같으며, Lombok을 사용해 자동으로 Getter와 Setter를 만들어 주었습니다.
package com.example.demo.vo; import lombok.Getter; import lombok.Setter; @Getter @Setter public class CsrfVO { private String name; }
끝, 결론은 AJAX호출시 토큰값을 헤더에 잘 넘겨주면됩니다.
FORM전송은 알아서 토큰값이 들어가 있습니다.
CSRF를 사용하기 싫다면!! Secutity를 설정한 Config 클래스에서 아래 코드와 같이 설정.
.and().csrf().disable()
혹은 특정 URL은 CSRF 토큰 없이 프로세스를 진행 시키려면 아래와 같은 코드를 같은 Config 클래스에서 설정.
.csrf().ignoringAntMatchers("/user/save")
출처:
https://hyunsangwon93.tistory.com/28
참조"
https://docs.spring.io/spring-security/site/docs/5.1.5.RELEASE/reference/htmlsingle/#csrf
예)
controller
@Controller @RequiredArgsConstructor public class OrderController { private final OrderService orderService; @PostMapping(value = "/order") public @ResponseBody ResponseEntity order(@RequestBody @Valid OrderDto orderDto , BindingResult bindingResult, Principal principal){ if(bindingResult.hasErrors()){ StringBuilder sb = new StringBuilder(); List<FieldError> fieldErrors = bindingResult.getFieldErrors(); for (FieldError fieldError : fieldErrors) { sb.append(fieldError.getDefaultMessage()); } return new ResponseEntity<String>(sb.toString(), HttpStatus.BAD_REQUEST); } String email = principal.getName(); Long orderId; try { orderId = orderService.order(orderDto, email); } catch(Exception e){ return new ResponseEntity<String>(e.getMessage(), HttpStatus.BAD_REQUEST); } return new ResponseEntity<Long>(orderId, HttpStatus.OK); } }
@Controller @RequiredArgsConstructor public class CartController { private final CartService cartService; @PostMapping(value = "/cart") public @ResponseBody ResponseEntity order(@RequestBody @Valid CartItemDto cartItemDto, BindingResult bindingResult, Principal principal){ if(bindingResult.hasErrors()){ StringBuilder sb = new StringBuilder(); List<FieldError> fieldErrors = bindingResult.getFieldErrors(); for (FieldError fieldError : fieldErrors) { sb.append(fieldError.getDefaultMessage()); } return new ResponseEntity<String>(sb.toString(), HttpStatus.BAD_REQUEST); } String email = principal.getName(); Long cartItemId; try { cartItemId = cartService.addCart(cartItemDto, email); } catch(Exception e){ return new ResponseEntity<String>(e.getMessage(), HttpStatus.BAD_REQUEST); } return new ResponseEntity<Long>(cartItemId, HttpStatus.OK); } }
view
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layouts/layout1}"> <head> <meta name="_csrf" th:content="${_csrf.token}"/> <meta name="_csrf_header" th:content="${_csrf.headerName}"/> </head> <!-- 사용자 스크립트 추가 --> <th:block layout:fragment="script"> <script th:inline="javascript"> $(document).ready(function(){ calculateToalPrice(); $("#count").change( function(){ calculateToalPrice(); }); }); function calculateToalPrice(){ var count = $("#count").val(); var price = $("#price").val(); var totalPrice = price*count; $("#totalPrice").html(totalPrice + '원'); } function order(){ var token = $("meta[name='_csrf']").attr("content"); var header = $("meta[name='_csrf_header']").attr("content"); var url = "/order"; var paramData = { itemId : $("#itemId").val(), count : $("#count").val() }; var param = JSON.stringify(paramData); $.ajax({ url : url, type : "POST", contentType : "application/json", data : param, beforeSend : function(xhr){ /* 데이터를 전송하기 전에 헤더에 csrf값을 설정 */ xhr.setRequestHeader(header, token); }, dataType : "json", cache : false, success : function(result, status){ alert("주문이 완료 되었습니다."); location.href='/'; }, error : function(jqXHR, status, error){ if(jqXHR.status == '401'){ alert('로그인 후 이용해주세요'); location.href='/members/login'; } else{ alert(jqXHR.responseText); } } }); } function addCart(){ var token = $("meta[name='_csrf']").attr("content"); var header = $("meta[name='_csrf_header']").attr("content"); var url = "/cart"; var paramData = { itemId : $("#itemId").val(), count : $("#count").val() }; var param = JSON.stringify(paramData); $.ajax({ url : url, type : "POST", contentType : "application/json", data : param, beforeSend : function(xhr){ /* 데이터를 전송하기 전에 헤더에 csrf값을 설정 */ xhr.setRequestHeader(header, token); }, dataType : "json", cache : false, success : function(result, status){ alert("상품을 장바구니에 담았습니다."); location.href='/'; }, error : function(jqXHR, status, error){ if(jqXHR.status == '401'){ alert('로그인 후 이용해주세요'); location.href='/members/login'; } else{ alert(jqXHR.responseText); } } }); } </script> </th:block> <!-- 사용자 CSS 추가 --> <th:block layout:fragment="css"> <style> .mgb-15{ margin-bottom:15px; } .mgt-30{ margin-top:30px; } .mgt-50{ margin-top:50px; } .repImgDiv{ margin-right:15px; height:auto; width:50%; } .repImg{ width:100%; height:400px; } .wd50{ height:auto; width:50%; } </style> </th:block> <div layout:fragment="content" style="margin-left:25%;margin-right:25%"> <input type="hidden" id="itemId" th:value="${item.id}"> <div class="d-flex"> <div class="repImgDiv"> <img th:src="${item.itemImgDtoList[0].imgUrl}" class = "rounded repImg" th:alt="${item.itemNm}"> </div> <div class="wd50"> <span th:if="${item.itemSellStatus == T(com.shop.constant.ItemSellStatus).SELL}" class="badge badge-primary mgb-15"> 판매중 </span> <span th:unless="${item.itemSellStatus == T(com.shop.constant.ItemSellStatus).SELL}" class="badge btn-danger mgb-15" > 품절 </span> <div class="h4" th:text="${item.itemNm}"></div> <hr class="my-4"> <div class="text-right"> <div class="h4 text-danger text-left"> <input type="hidden" th:value="${item.price}" id="price" name="price"> <span th:text="${item.price}"></span>원 </div> <div class="input-group w-50"> <div class="input-group-prepend"> <span class="input-group-text">수량</span> </div> <input type="number" name="count" id="count" class="form-control" value="1" min="1"> </div> </div> <hr class="my-4"> <div class="text-right mgt-50"> <h5>결제 금액</h5> <h3 name="totalPrice" id="totalPrice" class="font-weight-bold"></h3> </div> <div th:if="${item.itemSellStatus == T(com.shop.constant.ItemSellStatus).SELL}" class="text-right"> <button type="button" class="btn btn-light border border-primary btn-lg" onclick="addCart()">장바구니 담기</button> <button type="button" class="btn btn-primary btn-lg" onclick="order()">주문하기</button> </div> <div th:unless="${item.itemSellStatus == T(com.shop.constant.ItemSellStatus).SELL}" class="text-right"> <button type="button" class="btn btn-danger btn-lg">품절</button> </div> </div> </div> <div class="jumbotron jumbotron-fluid mgt-30"> <div class="container"> <h4 class="display-5">상품 상세 설명</h4> <hr class="my-4"> <p class="lead" th:text="${item.itemDetail}"></p> </div> </div> <div th:each="itemImg : ${item.itemImgDtoList}" class="text-center"> <img th:if="${not #strings.isEmpty(itemImg.imgUrl)}" th:src="${itemImg.imgUrl}" class="rounded mgb-15" width="800"> </div> </div> </html>
댓글 ( 4)
댓글 남기기