Skip to content

Commit a8f22dd

Browse files
authored
Merge pull request #44 from choubung/dev
배포 오류 hot-fix
2 parents 3673444 + ddfa728 commit a8f22dd

8 files changed

Lines changed: 94 additions & 16 deletions

File tree

.github/workflows/cd.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ jobs:
7070
7171
# Cloud Run 컨테이너에 환경 변수 주입
7272
env_vars: |
73+
SPRING_PROFILES_ACTIVE=prod
74+
7375
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
7476
DB_USERNAME=${{ secrets.DB_USERNAME }}
7577
DB_PASSWORD=${{ secrets.DB_PASSWORD }}

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@ ARG JAR_FILE=build/libs/*.jar
44

55
COPY ${JAR_FILE} app.jar
66

7-
EXPOSE 8080
7+
EXPOSE 9090
88

99
ENTRYPOINT ["java", "-jar", "/app.jar"]

src/main/java/com/precourse/openMission/config/auth/SecurityConfig.java

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import lombok.RequiredArgsConstructor;
55
import org.springframework.context.annotation.Bean;
66
import org.springframework.context.annotation.Configuration;
7+
import org.springframework.context.annotation.Profile;
78
import org.springframework.http.HttpMethod;
89
import org.springframework.http.HttpStatus;
910
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
@@ -18,8 +19,28 @@
1819
public class SecurityConfig {
1920
private final CustomOAuth2UserService customOAuth2UserService;
2021

22+
// (운영/배포) 환경 전용 SecurityFilterChain
2123
@Bean
22-
protected SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
24+
@Profile("prod")
25+
public SecurityFilterChain prodFilterChain(HttpSecurity http) throws Exception {
26+
http.requiresChannel(channel -> channel
27+
.anyRequest().requiresSecure()
28+
);
29+
30+
commonSecurityConfig(http);
31+
32+
return http.build();
33+
}
34+
35+
@Bean
36+
@Profile("!prod")
37+
public SecurityFilterChain devFilterChain(HttpSecurity http) throws Exception {
38+
commonSecurityConfig(http);
39+
40+
return http.build();
41+
}
42+
43+
private void commonSecurityConfig(HttpSecurity http) throws Exception {
2344
http
2445
.authorizeHttpRequests((authz) -> authz
2546
.requestMatchers("/").permitAll()
@@ -45,8 +66,6 @@ protected SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
4566
.userService(customOAuth2UserService)
4667
)
4768
);
48-
49-
return http.build();
5069
}
5170

5271
@Bean
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.precourse.openMission.web;
2+
3+
import jakarta.servlet.http.HttpServletRequest;
4+
import org.springframework.security.web.csrf.CsrfToken;
5+
import org.springframework.web.bind.annotation.ControllerAdvice;
6+
import org.springframework.web.bind.annotation.ModelAttribute;
7+
import org.springframework.stereotype.Controller;
8+
9+
/**
10+
* 모든 컨트롤러(@Controller)의 Model에
11+
* 공통 속성(Attribute)을 추가하는 클래스
12+
*/
13+
@ControllerAdvice(annotations = Controller.class)
14+
public class GlobalModelAdvice {
15+
@ModelAttribute("_csrf")
16+
public CsrfToken csrfToken(HttpServletRequest request) {
17+
// Spring Security가 이미 request에 저장해 둔 _csrf 토큰을 꺼내서 Model에 담아줌
18+
return (CsrfToken) request.getAttribute(CsrfToken.class.getName());
19+
}
20+
}

src/main/resources/static/js/app/index.js

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ var main = {
1212
$('#btn-delete').on('click', function () {
1313
_this.delete();
1414
});
15+
16+
$('#btn-logout').on('click', function (e) {
17+
e.preventDefault(); // <a> 태그의 링크 이동을 막음
18+
_this.logout();
19+
});
1520
},
1621
save : function () {
1722
// 1. [수정] DTO에 맞게 데이터 수집
@@ -71,7 +76,7 @@ var main = {
7176
dataType: 'json',
7277
contentType:'application/json; charset=utf-8',
7378
data: JSON.stringify(data),
74-
beforeSend : function(xhr) { // 💡 (여기도)
79+
beforeSend : function(xhr) {
7580
xhr.setRequestHeader(header, token);
7681
}
7782
}).done(function() {
@@ -100,8 +105,29 @@ var main = {
100105
}).fail(function (error) {
101106
alert(error.responseJSON.message || JSON.stringify(error));
102107
});
103-
}
108+
},
104109

110+
logout : function () {
111+
// 9. <meta> 태그에서 CSRF 파라미터 이름과 토큰 값을 읽어옴
112+
var token = $("meta[name='_csrf']").attr("content");
113+
var paramName = $("meta[name='_csrf_parameter']").attr("content");
114+
115+
// 10. 동적으로 <form>을 생성
116+
var $form = $('<form></form>');
117+
$form.attr('action', '/logout');
118+
$form.attr('method', 'POST');
119+
120+
// 11. 폼에 CSRF 토큰(hidden input)을 추가
121+
$form.append($('<input/>', {
122+
type: 'hidden',
123+
name: paramName,
124+
value: token
125+
}));
126+
127+
// 12. 폼을 body에 추가하고 즉시 submit
128+
$form.appendTo('body');
129+
$form.submit();
130+
}
105131
};
106132

107133
main.init();

src/main/resources/templates/layout/header.mustache

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22
<html>
33
<head>
44
<title>메모 서비스</title>
5-
<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">
5+
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css">
6+
<meta name="_csrf" content="{{_csrf.token}}"/>
7+
<meta name="_csrf_header" content="{{_csrf.headerName}}"/>
8+
<meta name="_csrf_parameter" content="{{_csrf.parameterName}}"/>
69
</head>
710
<body>
811

@@ -20,10 +23,7 @@
2023
<li class="nav-item">
2124
{{#googleName}}
2225
<span class="navbar-text mr-2">Logged in as: {{googleName}}</span>
23-
<form action="/logout" method="post" style="display: inline;">
24-
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
25-
<button type="submit" class="btn btn-info btn-sm active">Logout</button>
26-
</form>
26+
<a href="#" class="btn btn-info btn-sm active" id="btn-logout" role="button">Logout</a>
2727
{{/googleName}}
2828
{{^googleName}}
2929
<a href="/oauth2/authorization/google" class="btn btn-success btn-sm active" role="button">Login</a>

src/test/java/com/precourse/openMission/integration/IndexControllerTest.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ public void setup() {
3737
@Test
3838
public void 메인페이지_로딩() throws Exception {
3939
// when & then
40-
mvc.perform(get("/"))
40+
mvc.perform(get("/")
41+
.secure(true))
4142
.andExpect(status().isOk())
4243
.andExpect(content().string(containsString("메모 서비스")));
4344
}

src/test/java/com/precourse/openMission/integration/MemoIntegrationTest.java

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ public void tearDown() throws Exception {
114114
ResultActions resultActions = mockMvc.perform(post("/home/memos")
115115
.with(csrf())
116116
.sessionAttr("user", sessionUser)
117+
.secure(true)
117118
.contentType(MediaType.APPLICATION_JSON)
118119
.content(json))
119120
.andExpect(status().isOk());
@@ -146,6 +147,7 @@ public void tearDown() throws Exception {
146147
// when, then
147148
mockMvc.perform(post("/home/memos")
148149
.with(csrf())
150+
.secure(true)
149151
.contentType(MediaType.APPLICATION_JSON)
150152
.content(json))
151153
.andExpect(status().isUnauthorized());
@@ -171,6 +173,7 @@ public void tearDown() throws Exception {
171173
mockMvc.perform(put("/home/memos/{targetId}", targetId)
172174
.with(csrf())
173175
.sessionAttr("user", sessionUser)
176+
.secure(true)
174177
.contentType(MediaType.APPLICATION_JSON)
175178
.content(json))
176179
.andExpect(status().isOk());
@@ -204,6 +207,7 @@ public void tearDown() throws Exception {
204207
mockMvc.perform(put("/home/memos/{invalidId}", invalidId)
205208
.with(csrf())
206209
.sessionAttr("user", sessionUser)
210+
.secure(true)
207211
.contentType(MediaType.APPLICATION_JSON)
208212
.content(json))
209213
.andExpect(status().isNotFound());
@@ -218,6 +222,7 @@ public void tearDown() throws Exception {
218222

219223
// when
220224
ResultActions resultActions = mockMvc.perform(get("/home/memos")
225+
.secure(true)
221226
.contentType(MediaType.APPLICATION_JSON));
222227

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

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

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

256262
// when, then
257-
mockMvc.perform(get("/home/memos/{invalidId}", invalidId))
263+
mockMvc.perform(get("/home/memos/{invalidId}", invalidId)
264+
.secure(true))
258265
.andExpect(status().isNotFound());
259266
}
260267

@@ -269,7 +276,8 @@ public void tearDown() throws Exception {
269276
// when
270277
mockMvc.perform(delete("/home/memos/{targetId}", targetId)
271278
.with(csrf())
272-
.sessionAttr("user", sessionUser))
279+
.sessionAttr("user", sessionUser)
280+
.secure(true))
273281
.andExpect(status().isNoContent());
274282

275283
// then
@@ -296,7 +304,8 @@ public void tearDown() throws Exception {
296304
// when, then
297305
mockMvc.perform(delete("/home/memos/{memoId}", memoId)
298306
.with(csrf())
299-
.sessionAttr("user", sessionUser))
307+
.sessionAttr("user", sessionUser)
308+
.secure(true))
300309
.andExpect(status().isBadRequest());
301310
}
302311

@@ -313,7 +322,8 @@ public void tearDown() throws Exception {
313322
// when, then
314323
mockMvc.perform(delete("/home/memos/{invalidId}", invalidId)
315324
.with(csrf())
316-
.sessionAttr("user", sessionUser))
325+
.sessionAttr("user", sessionUser)
326+
.secure(true))
317327
.andExpect(status().isNotFound());
318328
}
319329
}

0 commit comments

Comments
 (0)