From dd53f067ccb0e632e75306b9ad3e90af8a67114f Mon Sep 17 00:00:00 2001 From: choubung Date: Mon, 17 Nov 2025 23:56:16 +0900 Subject: [PATCH 1/3] =?UTF-8?q?fix:=20csrf=20=EC=98=A4=EB=A5=98=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../config/auth/SecurityConfig.java | 3 ++ .../openMission/web/GlobalModelAdvice.java | 20 +++++++++++++ src/main/resources/static/js/app/index.js | 30 +++++++++++++++++-- .../templates/layout/header.mustache | 10 +++---- 4 files changed, 56 insertions(+), 7 deletions(-) create mode 100644 src/main/java/com/precourse/openMission/web/GlobalModelAdvice.java diff --git a/src/main/java/com/precourse/openMission/config/auth/SecurityConfig.java b/src/main/java/com/precourse/openMission/config/auth/SecurityConfig.java index daee272..f2f3a30 100644 --- a/src/main/java/com/precourse/openMission/config/auth/SecurityConfig.java +++ b/src/main/java/com/precourse/openMission/config/auth/SecurityConfig.java @@ -21,6 +21,9 @@ public class SecurityConfig { @Bean protected SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http + .requiresChannel(channel -> channel + .anyRequest().requiresSecure() + ) .authorizeHttpRequests((authz) -> authz .requestMatchers("/").permitAll() diff --git a/src/main/java/com/precourse/openMission/web/GlobalModelAdvice.java b/src/main/java/com/precourse/openMission/web/GlobalModelAdvice.java new file mode 100644 index 0000000..e4f8486 --- /dev/null +++ b/src/main/java/com/precourse/openMission/web/GlobalModelAdvice.java @@ -0,0 +1,20 @@ +package com.precourse.openMission.web; + +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.security.web.csrf.CsrfToken; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.stereotype.Controller; + +/** + * 모든 컨트롤러(@Controller)의 Model에 + * 공통 속성(Attribute)을 추가하는 클래스 + */ +@ControllerAdvice(annotations = Controller.class) +public class GlobalModelAdvice { + @ModelAttribute("_csrf") + public CsrfToken csrfToken(HttpServletRequest request) { + // Spring Security가 이미 request에 저장해 둔 _csrf 토큰을 꺼내서 Model에 담아줌 + return (CsrfToken) request.getAttribute(CsrfToken.class.getName()); + } +} diff --git a/src/main/resources/static/js/app/index.js b/src/main/resources/static/js/app/index.js index bff5e10..5d23787 100644 --- a/src/main/resources/static/js/app/index.js +++ b/src/main/resources/static/js/app/index.js @@ -12,6 +12,11 @@ var main = { $('#btn-delete').on('click', function () { _this.delete(); }); + + $('#btn-logout').on('click', function (e) { + e.preventDefault(); // 태그의 링크 이동을 막음 + _this.logout(); + }); }, save : function () { // 1. [수정] DTO에 맞게 데이터 수집 @@ -71,7 +76,7 @@ var main = { dataType: 'json', contentType:'application/json; charset=utf-8', data: JSON.stringify(data), - beforeSend : function(xhr) { // 💡 (여기도) + beforeSend : function(xhr) { xhr.setRequestHeader(header, token); } }).done(function() { @@ -100,8 +105,29 @@ var main = { }).fail(function (error) { alert(error.responseJSON.message || JSON.stringify(error)); }); - } + }, + logout : function () { + // 9. 태그에서 CSRF 파라미터 이름과 토큰 값을 읽어옴 + var token = $("meta[name='_csrf']").attr("content"); + var paramName = $("meta[name='_csrf_parameter']").attr("content"); + + // 10. 동적으로
을 생성 + var $form = $('
'); + $form.attr('action', '/logout'); + $form.attr('method', 'POST'); + + // 11. 폼에 CSRF 토큰(hidden input)을 추가 + $form.append($('', { + type: 'hidden', + name: paramName, + value: token + })); + + // 12. 폼을 body에 추가하고 즉시 submit + $form.appendTo('body'); + $form.submit(); + } }; main.init(); \ No newline at end of file diff --git a/src/main/resources/templates/layout/header.mustache b/src/main/resources/templates/layout/header.mustache index df57c57..3f7827a 100644 --- a/src/main/resources/templates/layout/header.mustache +++ b/src/main/resources/templates/layout/header.mustache @@ -2,7 +2,10 @@ 메모 서비스 - + + + + @@ -20,10 +23,7 @@