티스토리 뷰

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()); // 요렇게.
  }
})

 

댓글
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함