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
2 changes: 2 additions & 0 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ jobs:

# 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 @@ -4,6 +4,6 @@ ARG JAR_FILE=build/libs/*.jar

COPY ${JAR_FILE} app.jar

EXPOSE 8080
EXPOSE 9090

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());
}
}
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