티스토리 뷰
Restful API서버라면 이런 처리를 할 필요가 없겠지만, 앞뒤(frontend/backend)가 붙어있는 프로젝트라면 CSRF 공격 방지는 해놓는 게 좋다.
1. 디펜던시 추가 - Gradle 기준
implementation 'org.springframework.boot:spring-boot-starter-web'
// Spring Security
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-security', version: '2.6.1'
2. Configuration 설정
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.csrf.InvalidCsrfTokenException;
import org.springframework.security.web.csrf.MissingCsrfTokenException;
import lombok.extern.slf4j.Slf4j;
@Configuration
@EnableWebSecurity
@Slf4j
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().ignoringAntMatchers("/error/**") // 특정 URL에 한해 csrf 비활성화
// .and()
// .csrf().ignoringAntMatchers("/some2/**/**");
.and()
.exceptionHandling()
.accessDeniedHandler(new AccessDeniedHandler() {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException,
ServletException {
if (accessDeniedException instanceof MissingCsrfTokenException) {
log.error("*** 세션이 만료되어 서버에서 토큰 정보를 가져올 수 없습니다.", accessDeniedException);
response.sendRedirect("/error/csrf"); // 해당 requestmapping 받는 컨트롤러단에서 알아서 처리.
} else if (accessDeniedException instanceof InvalidCsrfTokenException) {
log.error("*** 토큰이 올바르지 않습니다.", accessDeniedException);
response.sendRedirect("/error/csrf");
}
}
});
// http.csrf().disable(); // csrf 비활성화
}
}
3. 토큰 넘기기
▽ jsp페이지의 헤더에 hidden태그 박아준다.
<input type="hidden" id="nmCsrf" value="${_csrf.parameterName}" />
<input type="hidden" id="token" value="${_csrf.token}" />
▽ form submit 전송을 위한 javascript 소스
document.addEventListener("DOMContentLoaded", function(event) {
// CSRF Form Submit을 위한 form 생성
let commonPageForm = document.createElement("form");
commonPageForm.id = "commonPageForm";
commonPageForm.name = "commonPageForm";
commonPageForm.method = "POST";
document.body.appendChild(commonPageForm);
});
var CsrfSubmit = {
/**
* CSRF 토큰 포함 Submit 함수.
* @param {string} urlParams 매개변수를 포함한 URL ex) "/test/main?someKey=someValue"
*/
fnCommonSubmit: function(urlParams) {
let url = CsrfSubmit.getOnlyUrl(urlParams);
let paramsTags = CsrfSubmit.getParamsTags(urlParams);
// 기존 토큰 태그 제거
let alreadyElemts = document.getElementById("commonPageForm").elements;
if(alreadyElemts.length > 0){
alreadyElemts.forEach(element => {
if(element.id == "csrfEle"){
// 기존 csrf 태그 제거!
element.remove();
}
});
}
if(paramsTags.length > 0){
paramsTags.forEach(element => {
document.getElementById("commonPageForm").appendChild(element);
});
}
document.getElementById("commonPageForm").appendChild(CsrfSubmit.getCsrfTokenInputEle()); // 토큰 넘김
document.commonPageForm.action = url;
document.commonPageForm.submit();
},
/**
* url에서 매개변수를 제외한 맨 앞의 url만 반환
* @param {string} urlParams
* @returns
*/
getOnlyUrl: function(urlParams){
let url = urlParams;
if (urlParams.indexOf("?") > 0) {
url = urlParams.substring(0, urlParams.indexOf("?"), 0);
}
return url;
},
/**
* url에서 매개변수만 뽑아내서 태그 엘리먼트 array로 반환
* @param {string} url
* @returns 매개변수로 전달할 태그들.
*/
getParamsTags: function(url){
let params = new Array();
url.replace(/[?&]+([^=&]+)=([^&]*)/gi, function (str, key, value) {
let inputEle = document.createElement("input");
inputEle.name = key;
inputEle.value = value;
inputEle.type = "hidden";
params.push(inputEle);
});
return params;
},
/**
* CSRF 값을 input hidden 태그로 반환
* @returns
*/
getCsrfTokenInputEle: function(){
let nmCsrf = document.getElementById("nmCsrf").value;
let token = document.getElementById("token").value;
let inputEle = document.createElement("input");
inputEle.id = "csrfEle"
inputEle.name = nmCsrf;
inputEle.value = token;
inputEle.type = "hidden";
return inputEle;
},
/**
* 팝업 POST 전송
* @param {string} urlParams 매개변수를 포함한 URL ex) "/test/main?someParam=someValue"
* @param {string} title 팝업 제목
* @param {string} option 팝업 옵션
*/
fnCommonPopup: function(urlParams, title, option){
let url = CsrfSubmit.getOnlyUrl(urlParams);
let paramsTags = CsrfSubmit.getParamsTags(urlParams);
window.open("", title, option);
let popupForm = document.createElement("form");
popupForm.action = url;
popupForm.target = title;
popupForm.method = "POST";
popupForm.appendChild(CsrfSubmit.getCsrfTokenInputEle()); // 토큰 넘김
if(paramsTags.length > 0){
paramsTags.forEach(element => {
popupForm.appendChild(element);
});
}
document.body.appendChild(popupForm);
popupForm.submit();
document.body.removeChild(popupForm);
}
}
// form submit은 이렇게 한다.
let hw = "helloWorld";
let gogo = "GoGo";
CsrfSubmit.fnCommonSubmit("/some/some?someParam=" + hw + "&someParam2=" + gogo);
// 팝업 form submit은 이렇게 한다.
var option = "width=600, height=500, resizable=no, scrollbars=no, status=no, location=no, directories=no;";
CsrfSubmit.fnCommonPopup("/some/popup", "popupTest", option);
▽ 비동기 호출 시, 토큰을 넘기기 위한 소스.
- fetch 사용 시
/**
* 비동기 호출 함수 (POST-일반)
* @param {string} url 비동기 호출할 주소
* @param {object} params 매개변수
* @param {method} callbackMethod 실행될 함수
*/
function apiFetchPost(url, params, callbackMethod) {
// *** csrf 토큰 같이 넘겨줌
let nmCsrf = document.getElementById("nmCsrf").value;
let token = document.getElementById("token").value;
params[nmCsrf] = token;
fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
"X-Requested-With": "XMLHttpRequest"
},
body: new URLSearchParams(params)
})
.then(res =>{
if (res.redirected) {
window.location.href = res.url;
res.redirect(res.url)
}
return res.json()
})
.then(res => {
callbackMethod(res); // 함수 실행
})
.catch((error) => {
console.log(error);
alert("에러가 발생했습니다. \r\n관리자에게 문의해주십시오.");
});
}
/**
* 비동기 호출 함수 (POST-파일)
* @param {string} url 비동기 호출할 주소
* @param {object} params 매개변수
* @param {method} callbackMethod 실행될 함수
*/
function apiFetchFile(url, params, callbackMethod) {
// *** csrf 토큰 같이 넘겨줌
let nmCsrf = document.getElementById("nmCsrf").value;
let token = document.getElementById("token").value;
params.append(nmCsrf, token);
fetch(url, {
method: "POST",
headers: {"X-Requested-With": "XMLHttpRequest"},
body: params
})
.then(res =>{
if (res.redirected) {
window.location.href = res.url;
res.redirect(res.url)
}
return res.json()
})
.then(res => {
callbackMethod(res); // 함수 실행
})
.catch((error) => {
console.log(error);
alert("에러가 발생했습니다. \r\n관리자에게 문의해주십시오.");
});
}
- Ajax 사용 시
$.ajax({
url: "/some/where",
beforeSend: function(request) {
request.setRequestHeader("X-CSRF-TOKEN", $("#token").val()); // 요렇게.
}
})
댓글