Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Comment on lines +67 to +69
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

deploy-cloudrun 액션 v2와 flags 입력 호환성 검증

Cloud Run 배포 설정을 flags 블록으로 통합하셨습니다. google-github-actions/deploy-cloudrun@v2 액션이 이러한 flags 형식을 지원하는지 확인이 필요합니다.

다음 방법으로 액션 호환성을 확인해주세요:

또한 GCP_SQL_CONNECTION_NAME 시크릿이 GitHub 저장소의 Secrets에 올바르게 설정되어 있는지 확인해주세요.


🌐 Web query:

google-github-actions/deploy-cloudrun@v2 flags parameter documentation

💡 Result:

The flags input (optional) accepts a space-separated list of additional gcloud flags which the action appends to the underlying gcloud command:

  • For services it is passed to gcloud run deploy; for jobs it is passed to gcloud jobs deploy.
  • Use quotes when a flag value itself contains spaces or other flags (example: --args="-X=123").
  • The action does not parse/validate flags — you must ensure flags are valid for your gcloud version/subcommand.

Example:
with:
flags: '--add-cloudsql-instances=... "--args=-X=123"'

Sources: GitHub action README. [1][2]

Sources
[1] google-github-actions/deploy-cloudrun README (flags input).
[2] GitHub Marketplace — Deploy to Cloud Run (action inputs/flags).


YAML 플래그 형식 수정 필요

웹 검색 결과에 따르면 google-github-actions/deploy-cloudrun@v2flags 입력은 "space-separated list of additional gcloud flags"여야 합니다.

현재 코드의 문제점:

flags: |
  --min-instances=1
  --add-cloudsql-instances=${{ secrets.GCP_SQL_CONNECTION_NAME }}

YAML 멀티라인 문자열(|)은 개행 문자로 구분된 형식을 생성하지만, gcloud 명령은 공백으로 구분된 플래그를 기대합니다. 이는 배포 실패 또는 플래그 무시로 이어질 수 있습니다.

수정 방법:

flags: '--min-instances=1 --add-cloudsql-instances=${{ secrets.GCP_SQL_CONNECTION_NAME }}'

단일 라인으로 공백으로 구분하여 정의하거나, 여러 줄이 필요한 경우 > 폴드 연산자를 사용하여 개행을 공백으로 변환하세요.

🤖 Prompt for AI Agents
.github/workflows/cd.yml around lines 67 to 69: the flags value is using a YAML
block scalar (|) which preserves newlines, but
google-github-actions/deploy-cloudrun expects a space-separated list of gcloud
flags; replace the multiline block with a single space-separated string (or use
the folded scalar >) so flags become one line of space-separated options, e.g.
wrap the flags value in quotes and provide "--min-instances=1
--add-cloudsql-instances=${{ secrets.GCP_SQL_CONNECTION_NAME }}" so gcloud
receives the flags correctly.


# 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 }}
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ COPY ${JAR_FILE} app.jar

EXPOSE 9090

ENTRYPOINT ["java", "-jar", "/app.jar"]
ENTRYPOINT ["java", "-jar", "/app.jar"]
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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()
Expand All @@ -45,8 +66,6 @@ protected SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.userService(customOAuth2UserService)
)
);

return http.build();
}

@Bean
Expand Down
20 changes: 20 additions & 0 deletions src/main/java/com/precourse/openMission/web/GlobalModelAdvice.java
Original file line number Diff line number Diff line change
@@ -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());
}
}
2 changes: 1 addition & 1 deletion src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true

# H2
spring.h2.console.enabled=false
spring.h2.console.enabled=false
30 changes: 28 additions & 2 deletions src/main/resources/static/js/app/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ var main = {
$('#btn-delete').on('click', function () {
_this.delete();
});

$('#btn-logout').on('click', function (e) {
e.preventDefault(); // <a> 태그의 링크 이동을 막음
_this.logout();
});
},
save : function () {
// 1. [수정] DTO에 맞게 데이터 수집
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -100,8 +105,29 @@ var main = {
}).fail(function (error) {
alert(error.responseJSON.message || JSON.stringify(error));
});
}
},

logout : function () {
// 9. <meta> 태그에서 CSRF 파라미터 이름과 토큰 값을 읽어옴
var token = $("meta[name='_csrf']").attr("content");
var paramName = $("meta[name='_csrf_parameter']").attr("content");

// 10. 동적으로 <form>을 생성
var $form = $('<form></form>');
$form.attr('action', '/logout');
$form.attr('method', 'POST');

// 11. 폼에 CSRF 토큰(hidden input)을 추가
$form.append($('<input/>', {
type: 'hidden',
name: paramName,
value: token
}));

// 12. 폼을 body에 추가하고 즉시 submit
$form.appendTo('body');
$form.submit();
}
};

main.init();
10 changes: 5 additions & 5 deletions src/main/resources/templates/layout/header.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
<html>
<head>
<title>메모 서비스</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" integrity="sha384-T3c6CoIi6uLrA9TneNEoa/RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc0MPISektM" crossorigin="anonymous">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css">
<meta name="_csrf" content="{{_csrf.token}}"/>
<meta name="_csrf_header" content="{{_csrf.headerName}}"/>
<meta name="_csrf_parameter" content="{{_csrf.parameterName}}"/>
</head>
<body>

Expand All @@ -20,10 +23,7 @@
<li class="nav-item">
{{#googleName}}
<span class="navbar-text mr-2">Logged in as: {{googleName}}</span>
<form action="/logout" method="post" style="display: inline;">
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
<button type="submit" class="btn btn-info btn-sm active">Logout</button>
</form>
<a href="#" class="btn btn-info btn-sm active" id="btn-logout" role="button">Logout</a>
{{/googleName}}
{{^googleName}}
<a href="/oauth2/authorization/google" class="btn btn-success btn-sm active" role="button">Login</a>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ public void setup() {
@Test
public void 메인페이지_로딩() throws Exception {
// when & then
mvc.perform(get("/"))
mvc.perform(get("/")
.secure(true))
.andExpect(status().isOk())
.andExpect(content().string(containsString("메모 서비스")));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ public void tearDown() throws Exception {
ResultActions resultActions = mockMvc.perform(post("/home/memos")
.with(csrf())
.sessionAttr("user", sessionUser)
.secure(true)
.contentType(MediaType.APPLICATION_JSON)
.content(json))
.andExpect(status().isOk());
Expand Down Expand Up @@ -146,6 +147,7 @@ public void tearDown() throws Exception {
// when, then
mockMvc.perform(post("/home/memos")
.with(csrf())
.secure(true)
.contentType(MediaType.APPLICATION_JSON)
.content(json))
.andExpect(status().isUnauthorized());
Expand All @@ -171,6 +173,7 @@ public void tearDown() throws Exception {
mockMvc.perform(put("/home/memos/{targetId}", targetId)
.with(csrf())
.sessionAttr("user", sessionUser)
.secure(true)
.contentType(MediaType.APPLICATION_JSON)
.content(json))
.andExpect(status().isOk());
Expand Down Expand Up @@ -204,6 +207,7 @@ public void tearDown() throws Exception {
mockMvc.perform(put("/home/memos/{invalidId}", invalidId)
.with(csrf())
.sessionAttr("user", sessionUser)
.secure(true)
.contentType(MediaType.APPLICATION_JSON)
.content(json))
.andExpect(status().isNotFound());
Expand All @@ -218,6 +222,7 @@ public void tearDown() throws Exception {

// when
ResultActions resultActions = mockMvc.perform(get("/home/memos")
.secure(true)
.contentType(MediaType.APPLICATION_JSON));

// then
Expand All @@ -237,6 +242,7 @@ public void tearDown() throws Exception {

// when
ResultActions resultActions = mockMvc.perform(get("/home/memos/{targetId}", targetId)
.secure(true)
.sessionAttr("user", sessionUser));

// then
Expand All @@ -254,7 +260,8 @@ public void tearDown() throws Exception {
Long invalidId = publicMemo.getId();

// when, then
mockMvc.perform(get("/home/memos/{invalidId}", invalidId))
mockMvc.perform(get("/home/memos/{invalidId}", invalidId)
.secure(true))
.andExpect(status().isNotFound());
}

Expand All @@ -269,7 +276,8 @@ public void tearDown() throws Exception {
// when
mockMvc.perform(delete("/home/memos/{targetId}", targetId)
.with(csrf())
.sessionAttr("user", sessionUser))
.sessionAttr("user", sessionUser)
.secure(true))
.andExpect(status().isNoContent());

// then
Expand All @@ -296,7 +304,8 @@ public void tearDown() throws Exception {
// when, then
mockMvc.perform(delete("/home/memos/{memoId}", memoId)
.with(csrf())
.sessionAttr("user", sessionUser))
.sessionAttr("user", sessionUser)
.secure(true))
.andExpect(status().isBadRequest());
}

Expand All @@ -313,7 +322,8 @@ public void tearDown() throws Exception {
// when, then
mockMvc.perform(delete("/home/memos/{invalidId}", invalidId)
.with(csrf())
.sessionAttr("user", sessionUser))
.sessionAttr("user", sessionUser)
.secure(true))
.andExpect(status().isNotFound());
}
}
Loading