From 17568aa2038d0bb55050d7f7a42a7437824499a5 Mon Sep 17 00:00:00 2001 From: shawn9272 Date: Mon, 23 Mar 2026 15:27:19 +0900 Subject: [PATCH 01/11] =?UTF-8?q?refac=20-=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EC=A3=BC=EC=84=9D=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CUK_Compasser/global/security/config/SecurityConfig.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/global/security/config/SecurityConfig.java b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/global/security/config/SecurityConfig.java index 284649a..e230b6c 100644 --- a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/global/security/config/SecurityConfig.java +++ b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/global/security/config/SecurityConfig.java @@ -1,8 +1,6 @@ package Comprehensive_Design_Project.CUK_Compasser.global.security.config; import Comprehensive_Design_Project.CUK_Compasser.global.security.filter.JWTAuthenticationFilter; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -16,7 +14,6 @@ import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; -import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; @Configuration @RequiredArgsConstructor From c7265191a100efaf2376c8223239ce30f5eb5ee6 Mon Sep 17 00:00:00 2001 From: shawn9272 Date: Mon, 23 Mar 2026 15:34:34 +0900 Subject: [PATCH 02/11] =?UTF-8?q?fix=20-=20prometheus=20=EC=A0=91=EA=B7=BC?= =?UTF-8?q?=20=ED=97=88=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/security/config/SecurityConfig.java | 12 ++++++++++-- .../security/filter/JWTAuthenticationFilter.java | 2 +- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/global/security/config/SecurityConfig.java b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/global/security/config/SecurityConfig.java index e230b6c..2f51b13 100644 --- a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/global/security/config/SecurityConfig.java +++ b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/global/security/config/SecurityConfig.java @@ -1,9 +1,12 @@ package Comprehensive_Design_Project.CUK_Compasser.global.security.config; import Comprehensive_Design_Project.CUK_Compasser.global.security.filter.JWTAuthenticationFilter; +import Comprehensive_Design_Project.CUK_Compasser.global.security.jwt.JWTProvider; +import Comprehensive_Design_Project.CUK_Compasser.global.security.userDetails.CustomUserDetailsService; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.core.RedisTemplate; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; @@ -19,10 +22,14 @@ @RequiredArgsConstructor public class SecurityConfig { - private final JWTAuthenticationFilter jwtAuthenticationFilter; + @Bean + public JWTAuthenticationFilter jwtAuthenticationFilter(JWTProvider jwtProvider, RedisTemplate redisTemplate, CustomUserDetailsService customUserDetailsService) { + return new JWTAuthenticationFilter(jwtProvider, redisTemplate, customUserDetailsService); + } @Bean - SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + SecurityFilterChain securityFilterChain(HttpSecurity http, JWTAuthenticationFilter jwtAuthenticationFilter) throws Exception { + http .csrf(AbstractHttpConfigurer::disable) .formLogin(AbstractHttpConfigurer::disable) @@ -52,6 +59,7 @@ SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { return http.build(); } + @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception { return configuration.getAuthenticationManager(); diff --git a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/global/security/filter/JWTAuthenticationFilter.java b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/global/security/filter/JWTAuthenticationFilter.java index f8ca4f2..9b8637d 100644 --- a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/global/security/filter/JWTAuthenticationFilter.java +++ b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/global/security/filter/JWTAuthenticationFilter.java @@ -30,7 +30,7 @@ @Slf4j @RequiredArgsConstructor -@Component +//@Component -> prometheus 접근을 허용하기 위해 제거 public class JWTAuthenticationFilter extends OncePerRequestFilter { private final JWTProvider jwtProvider; From 5e0af38d2f77d644dbdbbe64f1f660a7a9780308 Mon Sep 17 00:00:00 2001 From: shawn9272 Date: Mon, 23 Mar 2026 20:22:46 +0900 Subject: [PATCH 03/11] =?UTF-8?q?feat=20-=20=EB=B0=B0=ED=8F=AC=20=EC=84=B1?= =?UTF-8?q?=EA=B3=B5=20=EC=97=AC=EB=B6=80=20=EB=94=94=EC=8A=A4=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=95=8C=EB=A6=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 30 ++++++++++++++++++- .../filter/JWTAuthenticationFilter.java | 2 +- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 6b2532f..594d8ef 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -88,4 +88,32 @@ jobs: sudo IMAGE_TAG=$IMAGE_TAG docker-compose -f docker-compose-prod.yml up -d --force-recreate # 5. 미사용 이미지 정리 - sudo docker image prune -f \ No newline at end of file + sudo docker image prune -f + + # 6. 디스코드 알림 (성공 시) + - name: Discord Notification - Success + if: success() + uses: sarisia/actions-status-discord@v1 + with: + webhook: ${{ secrets.DISCORD_WEBHOOK_URL }} + status: success + title: "🚀 배포 완료 (cuk-compasser)" + description: | + **브랜치:** `${{ github.ref_name }}` + **커밋 메시지:** ${{ github.event.head_commit.message }} + **태그:** `${{ github.sha }}` + 성공적으로 EC2에 배포되었습니다! + color: 0x00ff00 + + # 7. 디스코드 알림 (실패 시) + - name: Discord Notification - Failure + if: failure() + uses: sarisia/actions-status-discord@v1 + with: + webhook: ${{ secrets.DISCORD_WEBHOOK_URL }} + status: failure + title: "❌ 배포 실패 (cuk-compasser)" + description: | + **브랜치:** `${{ github.ref_name }}` + 배포 중 에러가 발생했습니다. Actions 탭에서 로그를 확인해주세요. + color: 0xff0000 \ No newline at end of file diff --git a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/global/security/filter/JWTAuthenticationFilter.java b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/global/security/filter/JWTAuthenticationFilter.java index db117ed..405d704 100644 --- a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/global/security/filter/JWTAuthenticationFilter.java +++ b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/global/security/filter/JWTAuthenticationFilter.java @@ -30,7 +30,7 @@ @Slf4j @RequiredArgsConstructor -//@Component -> prometheus 접근을 허용하기 위해 제거 +@Component public class JWTAuthenticationFilter extends OncePerRequestFilter { private final JWTProvider jwtProvider; From 8fdba858a5dfcd232f30b998e98e7f23e31ef56a Mon Sep 17 00:00:00 2001 From: shawn9272 Date: Mon, 23 Mar 2026 20:32:25 +0900 Subject: [PATCH 04/11] =?UTF-8?q?style=20-=20=EB=93=A4=EC=97=AC=EC=93=B0?= =?UTF-8?q?=EA=B8=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 62 +++++++++++++++++------------------- 1 file changed, 30 insertions(+), 32 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 594d8ef..b0a82a4 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -55,13 +55,12 @@ jobs: envs: IMAGE_TAG script: | cd ~/BE - - # 1. [핵심] 깃허브의 모든 최신 파일(promtail-config 등) 강제 동기화 + + # 1. 깃허브 최신 파일 동기화 git fetch --all git reset --hard origin/deploy - # 2. .env 파일 생성 (보안상 필요한 변수들만 주입) - # 기존에 작성하신 echo 방식을 유지하되, 덮어쓰기(>)와 추가(>>)를 구분합니다. + # 2. .env 파일 생성 echo "MYSQL_ROOT_PASSWORD=${{ secrets.MYSQL_ROOT_PASSWORD }}" > .env echo "MYSQL_DATABASE=${{ secrets.MYSQL_DATABASE }}" >> .env echo "MYSQL_USER=${{ secrets.MYSQL_USER }}" >> .env @@ -88,32 +87,31 @@ jobs: sudo IMAGE_TAG=$IMAGE_TAG docker-compose -f docker-compose-prod.yml up -d --force-recreate # 5. 미사용 이미지 정리 - sudo docker image prune -f - - # 6. 디스코드 알림 (성공 시) - - name: Discord Notification - Success - if: success() - uses: sarisia/actions-status-discord@v1 - with: - webhook: ${{ secrets.DISCORD_WEBHOOK_URL }} - status: success - title: "🚀 배포 완료 (cuk-compasser)" - description: | - **브랜치:** `${{ github.ref_name }}` - **커밋 메시지:** ${{ github.event.head_commit.message }} - **태그:** `${{ github.sha }}` - 성공적으로 EC2에 배포되었습니다! - color: 0x00ff00 + sudo docker image prune -f - # 7. 디스코드 알림 (실패 시) - - name: Discord Notification - Failure - if: failure() - uses: sarisia/actions-status-discord@v1 - with: - webhook: ${{ secrets.DISCORD_WEBHOOK_URL }} - status: failure - title: "❌ 배포 실패 (cuk-compasser)" - description: | - **브랜치:** `${{ github.ref_name }}` - 배포 중 에러가 발생했습니다. Actions 탭에서 로그를 확인해주세요. - color: 0xff0000 \ No newline at end of file + # 6. 디스코드 알림 (성공 시) - SSH 액션 밖으로 분리 + - name: Discord Notification - Success + if: success() + uses: sarisia/actions-status-discord@v1 + with: + webhook: ${{ secrets.DISCORD_WEBHOOK_URL }} + status: success + title: "🚀 배포 완료 (cuk-compasser)" + description: | + **브랜치:** `${{ github.ref_name }}` + **커밋 메시지:** ${{ github.event.head_commit.message }} + 성공적으로 EC2에 배포되었습니다! + color: 0x00ff00 + + # 7. 디스코드 알림 (실패 시) - SSH 액션 밖으로 분리 + - name: Discord Notification - Failure + if: failure() + uses: sarisia/actions-status-discord@v1 + with: + webhook: ${{ secrets.DISCORD_WEBHOOK_URL }} + status: failure + title: "❌ 배포 실패 (cuk-compasser)" + description: | + **브랜치:** `${{ github.ref_name }}` + 배포 중 에러가 발생했습니다. Actions 탭에서 로그를 확인해주세요. + color: 0xff0000 \ No newline at end of file From 0f5f3c3538e72f7272efa5f82e37575384182cd2 Mon Sep 17 00:00:00 2001 From: shawn9272 Date: Mon, 23 Mar 2026 21:06:48 +0900 Subject: [PATCH 05/11] =?UTF-8?q?fix:=20JWT=20=ED=95=84=ED=84=B0=20?= =?UTF-8?q?=EC=98=88=EC=99=B8=20=EC=B2=98=EB=A6=AC=20=EB=B0=A9=EC=8B=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EB=AA=A8=EB=8B=88=ED=84=B0?= =?UTF-8?q?=EB=A7=81=20=EA=B2=BD=EB=A1=9C=20=EB=A1=9C=EA=B7=B8=20=EC=B0=A8?= =?UTF-8?q?=EB=8B=A8=20-=20Filter=20=EB=82=B4=EB=B6=80=EC=97=90=EC=84=9C?= =?UTF-8?q?=20GeneralException=EC=9D=84=20throw=20=ED=95=98=EB=8D=98=20?= =?UTF-8?q?=EB=B0=A9=EC=8B=9D=EC=9D=84=20=EC=A7=81=EC=A0=91=20Response?= =?UTF-8?q?=EC=97=90=20=EC=9E=91=EC=84=B1=ED=95=98=EB=8A=94=20=EB=B0=A9?= =?UTF-8?q?=EC=8B=9D=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD=20=20=20(Filte?= =?UTF-8?q?r=20=EC=98=88=EC=99=B8=EA=B0=80=20DispatcherServlet=EA=B9=8C?= =?UTF-8?q?=EC=A7=80=20=EC=A0=84=ED=8C=8C=EB=90=98=EC=96=B4=20Servlet.serv?= =?UTF-8?q?ice()=20=EC=97=90=EB=9F=AC=20=EB=A1=9C=EA=B7=B8=EA=B0=80=20?= =?UTF-8?q?=EB=B0=9C=EC=83=9D=ED=95=98=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0)=20-=20JWTAuthenticationFilter=EC=97=90?= =?UTF-8?q?=EC=84=9C=20@Component=20=EC=96=B4=EB=85=B8=ED=85=8C=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EC=A0=9C=EA=B1=B0=20=20=20(=EC=84=9C=EB=B8=94?= =?UTF-8?q?=EB=A6=BF=20=ED=95=84=ED=84=B0=20=EC=9E=90=EB=8F=99=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D=20=EB=B0=A9=EC=A7=80=20=EB=B0=8F=20SecurityConfig?= =?UTF-8?q?=EC=9D=98=20=ED=95=84=ED=84=B0=20=EC=B2=B4=EC=9D=B8=20=EC=A0=9C?= =?UTF-8?q?=EC=96=B4=EA=B6=8C=20=EA=B0=95=ED=99=94)=20-=20Prometheus(/actu?= =?UTF-8?q?ator)=20=EB=93=B1=20=EB=AA=A8=EB=8B=88=ED=84=B0=EB=A7=81=20?= =?UTF-8?q?=EA=B2=BD=EB=A1=9C=20=ED=98=B8=EC=B6=9C=20=EC=8B=9C=20=EB=B0=9C?= =?UTF-8?q?=EC=83=9D=ED=95=98=EB=8A=94=20=EB=AC=B4=EC=9D=98=EB=AF=B8?= =?UTF-8?q?=ED=95=9C=20WARN=20=EB=A1=9C=EA=B7=B8=20=EC=B0=A8=EB=8B=A8=20-?= =?UTF-8?q?=20=EB=A6=AC=ED=94=84=EB=A0=88=EC=8B=9C=20=ED=86=A0=ED=81=B0(RT?= =?UTF-8?q?)=20=EB=A7=8C=EB=A3=8C/=EB=B6=80=EC=9E=AC=20=EC=8B=9C=20401=20U?= =?UTF-8?q?nauthorized=20=EC=9D=91=EB=8B=B5=EC=9D=84=20JSON=20=ED=98=95?= =?UTF-8?q?=ED=83=9C=EB=A1=9C=20=EC=A7=81=EC=A0=91=20=EB=B0=98=ED=99=98?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../filter/JWTAuthenticationFilter.java | 115 +++++++++--------- 1 file changed, 55 insertions(+), 60 deletions(-) diff --git a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/global/security/filter/JWTAuthenticationFilter.java b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/global/security/filter/JWTAuthenticationFilter.java index 405d704..7b18645 100644 --- a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/global/security/filter/JWTAuthenticationFilter.java +++ b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/global/security/filter/JWTAuthenticationFilter.java @@ -1,7 +1,6 @@ package Comprehensive_Design_Project.CUK_Compasser.global.security.filter; import Comprehensive_Design_Project.CUK_Compasser.global.common.apiPayload.code.generalStatus.GeneralErrorCode; -import Comprehensive_Design_Project.CUK_Compasser.global.common.apiPayload.exception.GeneralException; import Comprehensive_Design_Project.CUK_Compasser.global.security.jwt.JWT; import Comprehensive_Design_Project.CUK_Compasser.global.security.jwt.JWTProvider; import Comprehensive_Design_Project.CUK_Compasser.global.security.userDetails.CustomUserDetailsService; @@ -22,7 +21,6 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; @@ -30,7 +28,6 @@ @Slf4j @RequiredArgsConstructor -@Component public class JWTAuthenticationFilter extends OncePerRequestFilter { private final JWTProvider jwtProvider; @@ -41,52 +38,44 @@ public class JWTAuthenticationFilter extends OncePerRequestFilter { protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String accessToken = resolveAccessToken(request); - /// jwtProvider 에서 인증 조회 + 토큰 검증이 필요! - try{ - if(accessToken != null && jwtProvider.validateToken(accessToken)){ // 비었거나, 올바르지 않거나 - + try { + if (accessToken != null && jwtProvider.validateToken(accessToken)) { String isBlackListed = (String) redisTemplate.opsForValue().get("blacklist:" + accessToken); - if (isBlackListed != null){ + if (isBlackListed != null) { log.warn("[JWTAuthenticationFilter] - Using BlackListed Token!"); - throw new GeneralException(GeneralErrorCode.BLACKLIST_TOKEN); + // 직접 응답을 보내고 필터 체인을 종료합니다. + sendErrorResponse(response, GeneralErrorCode.BLACKLIST_TOKEN); + return; } Authentication authentication = jwtProvider.getAuthentication(accessToken); if (authentication != null) { SecurityContextHolder.getContext().setAuthentication(authentication); - } else { - log.info("[JWTAuthenticationFilter] - Invalid Token, Dont save authentication!"); } } -/* else { - log.warn("[JWTAuthenticationFilter] - This is request with Empty or Invalid Token"); -// throw new GeneralException(ErrorCode.AUTH_UNAUTHORIZED); - }*/ - } - catch (ExpiredJwtException e){ + } catch (ExpiredJwtException e) { log.warn("[JWTAuthenticationFilter] - AT expired, attempting to refresh token"); handleExpiredAccessToken(request, response, e); - } - catch(JwtException | IllegalArgumentException e){ - log.info("[JWTAuthenticationFilter] - Invalid Refresh Token! "); + return; // 갱신 로직에서 응답을 이미 보냈으므로 종료 + } catch (JwtException | IllegalArgumentException e) { + log.info("[JWTAuthenticationFilter] - Invalid Token! "); + // 필요 시 여기서도 에러 응답을 보낼 수 있습니다. } filterChain.doFilter(request, response); } - private String resolveAccessToken(HttpServletRequest request){ + private String resolveAccessToken(HttpServletRequest request) { String bearerToken = request.getHeader("Authorization"); if (bearerToken != null && bearerToken.startsWith("Bearer ")) { - return bearerToken.substring(7).trim(); // 앞 뒤 공백 제거, "Bearer ~~~" 형식으로 통일 + return bearerToken.substring(7).trim(); } return null; } private String resolveRefreshToken(HttpServletRequest request) { Cookie[] cookies = request.getCookies(); - if (cookies == null) { - return null; - } + if (cookies == null) return null; return Arrays.stream(cookies) .filter(cookie -> "refreshToken".equals(cookie.getName())) @@ -95,55 +84,61 @@ private String resolveRefreshToken(HttpServletRequest request) { .orElse(null); } - private void handleExpiredAccessToken (HttpServletRequest request, HttpServletResponse response, ExpiredJwtException e){ - + private void handleExpiredAccessToken(HttpServletRequest request, HttpServletResponse response, ExpiredJwtException e) throws IOException { Claims claims = e.getClaims(); String username = claims.getSubject(); - // String authorities = claims.get("auth").toString(); 어차피 userDetails에서 검색하니까 필요 X String requestRT = resolveRefreshToken(request); - String savedRT = (String) redisTemplate.opsForValue().get("refresh:"+username); // 이메일 + String savedRT = (String) redisTemplate.opsForValue().get("refresh:" + username); - if (savedRT == null){ - log.warn("[JWTAuthenticationFilter] - savedRT is null!"); - throw new GeneralException(GeneralErrorCode.RT_NOT_FOUND); + if (savedRT == null || requestRT == null || !requestRT.equals(savedRT)) { + log.warn("[JWTAuthenticationFilter] - RT invalid or missing!"); + sendErrorResponse(response, GeneralErrorCode.RT_NOT_FOUND); + return; } - if (requestRT != null && requestRT.equals(savedRT)){ - UserDetails userDetails = customUserDetailsService.loadUserByUsername(username); - Authentication authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); - JWT newJWT = jwtProvider.generateToken(authentication); - - // AT 덮어쓰기 - response.setHeader("Authorization", "Bearer " + newJWT.getAccessToken()); - - // RT 덮어쓰기 - ResponseCookie cookie = ResponseCookie.from("refreshToken", newJWT.getRefreshToken()) - .httpOnly(true) - .secure(true) - .path("/") - .maxAge(7 * 24 * 60 * 60) // 7일 - .sameSite("Strict") - .build(); - response.setHeader(HttpHeaders.SET_COOKIE, cookie.toString()); - redisTemplate.opsForValue().set("refresh:"+username, newJWT.getRefreshToken()); - - SecurityContextHolder.getContext().setAuthentication(authentication); - log.info("Successfully refreshed token and set security context for user: {}", username); - } - else{ - log.warn("[JWTAuthenticationFilter] - requestRT is null! || requestRT is not equal to savedRT!"); - throw new GeneralException(GeneralErrorCode.RT_NOT_FOUND); - } + // RT가 유효한 경우 새로운 토큰 발급 + UserDetails userDetails = customUserDetailsService.loadUserByUsername(username); + Authentication authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); + JWT newJWT = jwtProvider.generateToken(authentication); + + response.setHeader("Authorization", "Bearer " + newJWT.getAccessToken()); + ResponseCookie cookie = ResponseCookie.from("refreshToken", newJWT.getRefreshToken()) + .httpOnly(true) + .secure(true) + .path("/") + .maxAge(7 * 24 * 60 * 60) + .sameSite("Strict") + .build(); + response.setHeader(HttpHeaders.SET_COOKIE, cookie.toString()); + redisTemplate.opsForValue().set("refresh:" + username, newJWT.getRefreshToken()); + + SecurityContextHolder.getContext().setAuthentication(authentication); + log.info("Successfully refreshed token for user: {}", username); + + // 토큰 갱신 후에도 요청을 계속 진행하려면 filterChain.doFilter를 호출해야 하지만, + // 보통은 헤더만 실어서 401 대신 200으로 처리하거나 클라이언트에게 다시 요청하라고 안내합니다. + } + + private void sendErrorResponse(HttpServletResponse response, GeneralErrorCode errorCode) throws IOException { + response.setContentType("application/json;charset=UTF-8"); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + + String body = String.format( + "{\"isSuccess\": false, \"code\": \"%s\", \"message\": \"%s\"}", + errorCode.getCode(), + errorCode.getMessage() + ); + + response.getWriter().write(body); } @Override protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException { String path = request.getRequestURI(); - // 이 경로들은 필터 로직을 아예 실행하지 않음 return path.startsWith("/actuator") || path.startsWith("/swagger-ui") || path.startsWith("/v3/api-docs") || path.startsWith("/favicon.ico"); } -} +} \ No newline at end of file From 360525c0fa40f5a8a2b7627d6e33445b551b68e2 Mon Sep 17 00:00:00 2001 From: shawn9272 Date: Wed, 25 Mar 2026 15:27:33 +0900 Subject: [PATCH 06/11] =?UTF-8?q?fix=20-=20promtail=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=20=EC=A0=80=EC=9E=A5=20=EA=B2=BD=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose-prod.yml | 8 +++-- promtail/promtail-config.yml | 39 +++++++++++++------------ src/main/resources/application-prod.yml | 2 +- 3 files changed, 28 insertions(+), 21 deletions(-) diff --git a/docker-compose-prod.yml b/docker-compose-prod.yml index adc4e85..5fe4bae 100644 --- a/docker-compose-prod.yml +++ b/docker-compose-prod.yml @@ -35,6 +35,10 @@ services: options: max-size: "10m" max-file: "3" + + volumes: + - ./logs:/app/logs + networks: - cuk-compasser-net @@ -118,9 +122,9 @@ services: restart: always volumes: - ./promtail/promtail-config.yml:/etc/promtail/config.yml - - /var/run/docker.sock:/var/run/docker.sock + - ./logs:/var/log/spring-app:ro - /var/lib/docker/containers:/var/lib/docker/containers:ro - - /var/log:/var/log + - /var/run/docker.sock:/var/run/docker.sock command: -config.file=/etc/promtail/config.yml networks: - cuk-compasser-net diff --git a/promtail/promtail-config.yml b/promtail/promtail-config.yml index 28ca9d8..826a161 100644 --- a/promtail/promtail-config.yml +++ b/promtail/promtail-config.yml @@ -9,29 +9,32 @@ clients: - url: http://loki:3100/loki/api/v1/push scrape_configs: - # 1. 기존 시스템 로그 (EC2 자체 로그) - - job_name: system - static_configs: - - targets: - - localhost - labels: - job: varlogs - application: compasser-api - __path__: /var/log/*.log - - # 2. 도커 컨테이너 로그 (스프링 부트 로그 포함) - - job_name: docker_logs + # 1. 도커 컨테이너 로그 직접 수집 (강력 추천 방식) + - job_name: docker_containers docker_sd_configs: - host: unix:///var/run/docker.sock refresh_interval: 5s relabel_configs: - - source_labels: [__meta_docker_container_id] - target_label: __path__ - replacement: /var/lib/docker/containers/$1/*-json.log + # 컨테이너 이름을 추출해서 'container_name' 라벨로 저장 - source_labels: ['__meta_docker_container_name'] regex: '/(.*)' target_label: 'container_name' - - source_labels: ['__meta_docker_container_name'] - regex: '/cuk-compasser-service' + # 로그 파일 경로 설정 + - source_labels: [__meta_docker_container_id] + target_label: __path__ + replacement: /var/lib/docker/containers/$1/*-json.log + # 특정 컨테이너(cuk-compasser-service)를 'spring-boot' job으로 식별 + - source_labels: ['container_name'] + regex: 'cuk-compasser-service' target_label: 'job' - replacement: 'spring-boot' \ No newline at end of file + replacement: 'spring-boot' + + # 2. 백업: 파일 직접 수집 (컨테이너 내부 logs 폴더) + - job_name: spring-boot-file + static_configs: + - targets: + - localhost + labels: + job: spring-boot + container_name: cuk-compasser-service + __path__: /var/log/spring-app/*.log \ No newline at end of file diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 5510ff1..1c6cf1f 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -89,7 +89,7 @@ management: logging: file: - name: /var/log/compasser-api.log + name: /app/log/compasser-api.log logback: rollingpolicy: max-history: 7 From bb7773e3e4210f2b4b376d49a240974b87318d0b Mon Sep 17 00:00:00 2001 From: shawn9272 Date: Sat, 28 Mar 2026 17:49:09 +0900 Subject: [PATCH 07/11] =?UTF-8?q?chore=20-=20nginx=20=EB=8F=84=EC=9E=85?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=9D=B8=ED=95=9C=20grafana=20=EC=A3=BC?= =?UTF-8?q?=EC=86=8C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose-prod.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose-prod.yml b/docker-compose-prod.yml index 5fe4bae..6e49986 100644 --- a/docker-compose-prod.yml +++ b/docker-compose-prod.yml @@ -95,7 +95,7 @@ services: environment: - GF_SECURITY_ADMIN_PASSWORD=${GF_SECURITY_ADMIN_PASSWORD} - GF_SERVER_DOMAIN=compasser.site - - GF_SERVER_ROOT_URL=%(protocol)s://%(domain)s:%(http_port)s/grafana/ + - GF_SERVER_ROOT_URL=https://compasser.site/grafana/ - GF_SERVER_SERVE_FROM_SUB_PATH=true volumes: - grafana_data:/var/lib/grafana From 64a01e597d8cca7f8a2060471a27234147291e92 Mon Sep 17 00:00:00 2001 From: shawn9272 Date: Sat, 28 Mar 2026 20:32:13 +0900 Subject: [PATCH 08/11] =?UTF-8?q?chore=20-=20grafana=20https=20=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose-prod.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-compose-prod.yml b/docker-compose-prod.yml index 6e49986..f7c09d4 100644 --- a/docker-compose-prod.yml +++ b/docker-compose-prod.yml @@ -95,6 +95,7 @@ services: environment: - GF_SECURITY_ADMIN_PASSWORD=${GF_SECURITY_ADMIN_PASSWORD} - GF_SERVER_DOMAIN=compasser.site + - GF_SERVER_PROTOCOL=https - GF_SERVER_ROOT_URL=https://compasser.site/grafana/ - GF_SERVER_SERVE_FROM_SUB_PATH=true volumes: From b6c8769124f8df0bf4ab397aad3638369d59976e Mon Sep 17 00:00:00 2001 From: shawn9272 Date: Sat, 28 Mar 2026 20:37:39 +0900 Subject: [PATCH 09/11] =?UTF-8?q?chore=20-=20grafana=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=ED=86=A0=EC=BD=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose-prod.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose-prod.yml b/docker-compose-prod.yml index f7c09d4..c90bc01 100644 --- a/docker-compose-prod.yml +++ b/docker-compose-prod.yml @@ -95,7 +95,7 @@ services: environment: - GF_SECURITY_ADMIN_PASSWORD=${GF_SECURITY_ADMIN_PASSWORD} - GF_SERVER_DOMAIN=compasser.site - - GF_SERVER_PROTOCOL=https + - GF_SERVER_PROTOCOL=http - GF_SERVER_ROOT_URL=https://compasser.site/grafana/ - GF_SERVER_SERVE_FROM_SUB_PATH=true volumes: From 6b1bc6984c1f0219ff3f59126a97f8bbc763d161 Mon Sep 17 00:00:00 2001 From: shawn9272 Date: Mon, 30 Mar 2026 14:43:01 +0900 Subject: [PATCH 10/11] =?UTF-8?q?chore=20-=20=EC=84=9C=EB=B2=84=20?= =?UTF-8?q?=EC=9D=B8=ED=94=84=EB=9D=BC=20=EC=B5=9C=EC=A0=81=ED=99=94=20?= =?UTF-8?q?=EB=B0=8F=20t3.micro=20=EB=8B=A4=EC=9A=B4=EC=82=AC=EC=9D=B4?= =?UTF-8?q?=EC=A7=95=20=EC=A4=80=EB=B9=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 4455f9f..8c3c18e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,12 +12,13 @@ RUN chmod +x ./gradlew RUN ./gradlew clean bootJar -x test # 2. 실행 스테이지 -FROM eclipse-temurin:21-jre-jammy +FROM eclipse-temurin:21-jre-alpine WORKDIR /app -COPY --from=build /app/build/libs/*.jar app.jar - +RUN apk add --no-cache tzdata ENV TZ=Asia/Seoul +COPY --from=build /app/build/libs/*SNAPSHOT.jar app.jar + # 실행 시 'prod' 프로파일 활성화 ENTRYPOINT ["java", "-jar", "-Dspring.profiles.active=prod", "app.jar"] \ No newline at end of file From eee4674c0e499f292b5bc3cf4ac930658fe7c0d6 Mon Sep 17 00:00:00 2001 From: shawn9272 Date: Mon, 30 Mar 2026 15:14:33 +0900 Subject: [PATCH 11/11] =?UTF-8?q?chore=20-=20spring=20boot=20=EB=A9=94?= =?UTF-8?q?=EB=AA=A8=EB=A6=AC=20=EC=A0=9C=ED=95=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose-prod.yml | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/docker-compose-prod.yml b/docker-compose-prod.yml index c90bc01..8821331 100644 --- a/docker-compose-prod.yml +++ b/docker-compose-prod.yml @@ -24,6 +24,7 @@ services: - KAKAO_LOGIN_REDIRECT_URI=${KAKAO_LOGIN_REDIRECT_URI} - KAKAO_LOCAL_BASE_URL=https://dapi.kakao.com - KAKAO_REST_API_KEY=${KAKAO_REST_API_KEY} + - JAVA_TOOL_OPTIONS=-Xms256m -Xmx256m depends_on: mysql-db: condition: service_healthy @@ -42,6 +43,11 @@ services: networks: - cuk-compasser-net + deploy: + resources: + limits: + memory: 400M + redis: image: redis:latest container_name: redis @@ -68,11 +74,18 @@ services: networks: - cuk-compasser-net healthcheck: - test: ["CMD-SHELL", "mysqladmin ping -h localhost -u $$MYSQL_USER -p$$MYSQL_PASSWORD"] + test: [ "CMD-SHELL", "mysqladmin ping -h localhost -u $$MYSQL_USER -p$$MYSQL_PASSWORD" ] interval: 10s timeout: 5s retries: 5 start_period: 30s + command: + - --innodb_buffer_pool_size=128M # 데이터를 램에 올려두는 캐시 공간을 128MB로 제한 + - --max_connections=50 # 동시 접속자(커넥션) 수 50개로 제한 + deploy: + resources: + limits: + memory: 300M # 컨테이너 전체 메모리 상한선 prometheus: image: prom/prometheus:latest