From e7d074b0e5b16c0af7999f8e5c9079c833613158 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=EC=A7=80=EC=9C=A4?= Date: Sun, 10 May 2026 12:42:06 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=ED=86=A0=ED=81=B0=20=EB=A7=8C=EB=A3=8C?= =?UTF-8?q?=20=EC=9D=91=EB=8B=B5=EA=B3=BC=20=EC=A0=95=EC=82=B0=20=EB=A9=A4?= =?UTF-8?q?=EB=B2=84=20=EC=88=98=20=EC=A1=B0=ED=9A=8C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infrastructure/security/JwtFilter.java | 36 ++++++++++- .../moddo/common/config/SecurityConfig.java | 4 +- .../SettlementQueryRepositoryImpl.java | 14 ++-- .../domain/auth/service/JwtFilterTest.java | 64 +++++++++++++++++++ 4 files changed, 108 insertions(+), 10 deletions(-) create mode 100644 src/test/java/com/dnd/moddo/domain/auth/service/JwtFilterTest.java diff --git a/src/main/java/com/dnd/moddo/auth/infrastructure/security/JwtFilter.java b/src/main/java/com/dnd/moddo/auth/infrastructure/security/JwtFilter.java index f7bb9c72..cc7252ac 100644 --- a/src/main/java/com/dnd/moddo/auth/infrastructure/security/JwtFilter.java +++ b/src/main/java/com/dnd/moddo/auth/infrastructure/security/JwtFilter.java @@ -2,11 +2,19 @@ import java.io.IOException; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; +import com.dnd.moddo.common.exception.ErrorResponse; +import com.dnd.moddo.common.exception.ModdoException; +import com.fasterxml.jackson.databind.ObjectMapper; + +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.JwtException; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; @@ -19,6 +27,7 @@ public class JwtFilter extends OncePerRequestFilter { private final JwtAuth jwtAuth; private final JwtUtil jwtUtil; + private final ObjectMapper objectMapper; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) @@ -26,14 +35,35 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse String token = jwtUtil.resolveToken(request); - if (token != null) { - Authentication authentication = jwtAuth.getAuthentication(token, JwtConstants.ACCESS_KEY.message); - SecurityContextHolder.getContext().setAuthentication(authentication); + try { + if (token != null) { + Authentication authentication = jwtAuth.getAuthentication(token, JwtConstants.ACCESS_KEY.message); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + } catch (ExpiredJwtException e) { + SecurityContextHolder.clearContext(); + writeErrorResponse(response, HttpStatus.UNAUTHORIZED, "토큰이 만료되었습니다."); + return; + } catch (JwtException e) { + SecurityContextHolder.clearContext(); + writeErrorResponse(response, HttpStatus.UNAUTHORIZED, "토큰이 유효하지 않습니다."); + return; + } catch (ModdoException e) { + SecurityContextHolder.clearContext(); + writeErrorResponse(response, e.getStatus(), e.getMessage()); + return; } filterChain.doFilter(request, response); } + private void writeErrorResponse(HttpServletResponse response, HttpStatus status, String message) throws IOException { + response.setStatus(status.value()); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding("UTF-8"); + objectMapper.writeValue(response.getWriter(), new ErrorResponse(status.value(), message)); + } + @Override protected boolean shouldNotFilter(HttpServletRequest request) { return request.getRequestURI().startsWith("/api/v1/user/reissue/token"); diff --git a/src/main/java/com/dnd/moddo/common/config/SecurityConfig.java b/src/main/java/com/dnd/moddo/common/config/SecurityConfig.java index bcf98703..ad185a7c 100644 --- a/src/main/java/com/dnd/moddo/common/config/SecurityConfig.java +++ b/src/main/java/com/dnd/moddo/common/config/SecurityConfig.java @@ -15,6 +15,7 @@ import com.dnd.moddo.auth.infrastructure.security.JwtAuth; import com.dnd.moddo.auth.infrastructure.security.JwtFilter; import com.dnd.moddo.auth.infrastructure.security.JwtUtil; +import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; @@ -25,6 +26,7 @@ public class SecurityConfig { private final JwtUtil jwtUtil; private final JwtAuth jwtAuth; + private final ObjectMapper objectMapper; @Bean public BCryptPasswordEncoder passwordEncoder() { @@ -43,7 +45,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .requestMatchers(CorsUtils::isPreFlightRequest).permitAll() .anyRequest().permitAll() ) - .addFilterBefore(new JwtFilter(jwtAuth, jwtUtil), UsernamePasswordAuthenticationFilter.class); + .addFilterBefore(new JwtFilter(jwtAuth, jwtUtil, objectMapper), UsernamePasswordAuthenticationFilter.class); return http.build(); } diff --git a/src/main/java/com/dnd/moddo/event/infrastructure/SettlementQueryRepositoryImpl.java b/src/main/java/com/dnd/moddo/event/infrastructure/SettlementQueryRepositoryImpl.java index 15226fae..6a2662b9 100644 --- a/src/main/java/com/dnd/moddo/event/infrastructure/SettlementQueryRepositoryImpl.java +++ b/src/main/java/com/dnd/moddo/event/infrastructure/SettlementQueryRepositoryImpl.java @@ -37,11 +37,12 @@ public List findByUserAndStatus( int limit ) { QSettlement settlement = QSettlement.settlement; - QMember member = QMember.member; + QMember myMember = new QMember("myMember"); + QMember settlementMember = new QMember("settlementMember"); QExpense expense = QExpense.expense; BooleanExpression userCondition = - member.user.id.eq(userId); + myMember.user.id.eq(userId); BooleanExpression statusCondition = null; @@ -56,13 +57,13 @@ public List findByUserAndStatus( ? userCondition.and(statusCondition) : userCondition; - NumberExpression memberCount = member.id.count(); + NumberExpression memberCount = settlementMember.id.count(); NumberExpression completedCount = Expressions.numberTemplate( Long.class, "sum(case when {0} = true then 1 else 0 end)", - member.isPaid + settlementMember.isPaid ); JPQLQuery totalAmount = @@ -86,8 +87,9 @@ public List findByUserAndStatus( settlement.createdAt, settlement.completedAt )) - .from(member) - .join(member.settlement, settlement) + .from(myMember) + .join(myMember.settlement, settlement) + .join(settlementMember).on(settlementMember.settlement.id.eq(settlement.id)) .where(finalCondition) .groupBy( settlement.id, diff --git a/src/test/java/com/dnd/moddo/domain/auth/service/JwtFilterTest.java b/src/test/java/com/dnd/moddo/domain/auth/service/JwtFilterTest.java new file mode 100644 index 00000000..6a33b723 --- /dev/null +++ b/src/test/java/com/dnd/moddo/domain/auth/service/JwtFilterTest.java @@ -0,0 +1,64 @@ +package com.dnd.moddo.domain.auth.service; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.BDDMockito.*; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.core.context.SecurityContextHolder; + +import com.dnd.moddo.auth.infrastructure.security.JwtAuth; +import com.dnd.moddo.auth.infrastructure.security.JwtFilter; +import com.dnd.moddo.auth.infrastructure.security.JwtUtil; +import com.fasterxml.jackson.databind.ObjectMapper; + +import io.jsonwebtoken.ExpiredJwtException; +import jakarta.servlet.FilterChain; + +@ExtendWith(MockitoExtension.class) +class JwtFilterTest { + + @Mock + JwtAuth jwtAuth; + + @Mock + JwtUtil jwtUtil; + + @Mock + FilterChain filterChain; + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @AfterEach + void tearDown() { + SecurityContextHolder.clearContext(); + } + + @Test + void givenExpiredToken_thenReturnTokenExpiredResponse() throws Exception { + // given + String token = "expired-token"; + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + JwtFilter jwtFilter = new JwtFilter(jwtAuth, jwtUtil, objectMapper); + + given(jwtUtil.resolveToken(any(MockHttpServletRequest.class))).willReturn(token); + given(jwtAuth.getAuthentication(token, "access_token")) + .willThrow(new ExpiredJwtException(null, null, "expired")); + + // when + jwtFilter.doFilter(request, response, filterChain); + + // then + assertThat(response.getStatus()).isEqualTo(401); + assertThat(response.getContentType()).startsWith("application/json"); + assertThat(response.getContentAsString()).contains("\"message\":\"토큰이 만료되었습니다.\""); + then(filterChain).should(never()).doFilter(request, response); + } +}