diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index e083d61..02e52d6 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -64,13 +64,14 @@ jobs: # Docker Hub에 푸시한 이미지를 지정 image: '${{ secrets.GCP_REGION }}-docker.pkg.dev/${{ secrets.GCP_PROJECT_ID }}/memo-app-repo/backend-deep-dive:${{ env.IMAGE_TAG }}' - # 9. [핵심] Cloud Run과 GCP SQL을 '내부 터널'로 연결 - add-cloudsql-instances: ${{ secrets.GCP_SQL_CONNECTION_NAME }} - - min_instances: 1 + flags: | + --min-instances=1 + --add-cloudsql-instances=${{ secrets.GCP_SQL_CONNECTION_NAME }} # Cloud Run 컨테이너에 환경 변수 주입 env_vars: | + SPRING_PROFILES_ACTIVE=prod + DB_URL=jdbc:mysql://google/mydb?socketFactory=com.google.cloud.sql.mysql.SocketFactory&cloudSqlInstance=${{ secrets.GCP_SQL_CONNECTION_NAME }}&useSSL=false&serverTimezone=UTC&characterEncoding=UTF-8 DB_USERNAME=${{ secrets.DB_USERNAME }} DB_PASSWORD=${{ secrets.DB_PASSWORD }} diff --git a/Dockerfile b/Dockerfile index a8b9c38..c7b6584 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,4 +6,4 @@ COPY ${JAR_FILE} app.jar EXPOSE 9090 -ENTRYPOINT ["java", "-jar", "/app.jar"] \ No newline at end of file +ENTRYPOINT ["java", "-jar", "/app.jar"] 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..d66c069 100644 --- a/src/main/java/com/precourse/openMission/config/auth/SecurityConfig.java +++ b/src/main/java/com/precourse/openMission/config/auth/SecurityConfig.java @@ -4,6 +4,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; @@ -18,8 +19,28 @@ public class SecurityConfig { private final CustomOAuth2UserService customOAuth2UserService; + // (운영/배포) 환경 전용 SecurityFilterChain @Bean - protected SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + @Profile("prod") + public SecurityFilterChain prodFilterChain(HttpSecurity http) throws Exception { + http.requiresChannel(channel -> channel + .anyRequest().requiresSecure() + ); + + commonSecurityConfig(http); + + return http.build(); + } + + @Bean + @Profile("!prod") + public SecurityFilterChain devFilterChain(HttpSecurity http) throws Exception { + commonSecurityConfig(http); + + return http.build(); + } + + private void commonSecurityConfig(HttpSecurity http) throws Exception { http .authorizeHttpRequests((authz) -> authz .requestMatchers("/").permitAll() @@ -45,8 +66,6 @@ protected SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .userService(customOAuth2UserService) ) ); - - return http.build(); } @Bean 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/application.properties b/src/main/resources/application.properties index 83f2591..eb71436 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -11,4 +11,4 @@ spring.jpa.hibernate.ddl-auto=update spring.jpa.show-sql=true # H2 -spring.h2.console.enabled=false \ No newline at end of file +spring.h2.console.enabled=false 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 @@