From 2765da834e158ee0cde4dbf4ecabec0daf35c838 Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Sat, 28 Dec 2024 22:55:20 +0900 Subject: [PATCH 01/75] =?UTF-8?q?feat:=20cors=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/tnt/global/config/CorsProperties.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 src/main/java/com/tnt/global/config/CorsProperties.java diff --git a/src/main/java/com/tnt/global/config/CorsProperties.java b/src/main/java/com/tnt/global/config/CorsProperties.java new file mode 100644 index 00000000..16922059 --- /dev/null +++ b/src/main/java/com/tnt/global/config/CorsProperties.java @@ -0,0 +1,18 @@ +package com.tnt.global.config; + +import java.util.List; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +@Configuration +@ConfigurationProperties(prefix = "cors") +public class CorsProperties { + + private List allowedOrigins; +} From bbd6f6dd763cedeb9ef8c54ec9a5f9115e781e82 Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Sat, 28 Dec 2024 22:56:27 +0900 Subject: [PATCH 02/75] =?UTF-8?q?style:=20naver=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=BB=A8=EB=B2=A4=EC=85=98=20=EC=BB=A4=EC=8A=A4=ED=85=80=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/naver-intellij-formatter-custom.xml | 75 ++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 config/naver-intellij-formatter-custom.xml diff --git a/config/naver-intellij-formatter-custom.xml b/config/naver-intellij-formatter-custom.xml new file mode 100644 index 00000000..ee944e44 --- /dev/null +++ b/config/naver-intellij-formatter-custom.xml @@ -0,0 +1,75 @@ + + + From 397688b699f0aa265e008eed005aa42e23d2931b Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Sat, 28 Dec 2024 22:56:46 +0900 Subject: [PATCH 03/75] =?UTF-8?q?feat:=20redis=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/tnt/global/config/RedisConfig.java | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 src/main/java/com/tnt/global/config/RedisConfig.java diff --git a/src/main/java/com/tnt/global/config/RedisConfig.java b/src/main/java/com/tnt/global/config/RedisConfig.java new file mode 100644 index 00000000..02d6b820 --- /dev/null +++ b/src/main/java/com/tnt/global/config/RedisConfig.java @@ -0,0 +1,35 @@ +package com.tnt.global.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.repository.configuration.EnableRedisRepositories; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +@EnableRedisRepositories +public class RedisConfig { + + @Value("${spring.data.redis.host}") + private String redisHost; + + @Value("${spring.data.redis.port}") + private int redisPort; + + @Bean + public RedisConnectionFactory redisConnectionFactory() { + return new LettuceConnectionFactory(redisHost, redisPort); + } + + @Bean + public RedisTemplate redisTemplate() { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(redisConnectionFactory()); + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setValueSerializer(new StringRedisSerializer()); + return redisTemplate; + } +} From 348aa143b131206f0c3a26836274c7782c4dd8d5 Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Sat, 28 Dec 2024 22:56:55 +0900 Subject: [PATCH 04/75] =?UTF-8?q?feat:=20webClient=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/tnt/global/config/WebClientConfig.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 src/main/java/com/tnt/global/config/WebClientConfig.java diff --git a/src/main/java/com/tnt/global/config/WebClientConfig.java b/src/main/java/com/tnt/global/config/WebClientConfig.java new file mode 100644 index 00000000..f5a86aeb --- /dev/null +++ b/src/main/java/com/tnt/global/config/WebClientConfig.java @@ -0,0 +1,15 @@ +package com.tnt.global.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.function.client.WebClient; + +@Configuration +public class WebClientConfig { + + @Bean + public WebClient webClient() { + return WebClient.builder() + .build(); + } +} From 4881e5638fe8b11f6e05e24334be5979add681b2 Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Sat, 28 Dec 2024 22:57:06 +0900 Subject: [PATCH 05/75] =?UTF-8?q?feat:=20cors=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/tnt/global/config/WebConfig.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 src/main/java/com/tnt/global/config/WebConfig.java diff --git a/src/main/java/com/tnt/global/config/WebConfig.java b/src/main/java/com/tnt/global/config/WebConfig.java new file mode 100644 index 00000000..7b2bba0f --- /dev/null +++ b/src/main/java/com/tnt/global/config/WebConfig.java @@ -0,0 +1,24 @@ +package com.tnt.global.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import lombok.RequiredArgsConstructor; + +@Configuration +@RequiredArgsConstructor +public class WebConfig implements WebMvcConfigurer { + + public static final String ALLOWED_METHOD_NAMES = "GET,HEAD,POST,PUT,DELETE,TRACE,OPTIONS,PATCH"; + + private final CorsProperties corsProperties; + + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOrigins(corsProperties.getAllowedOrigins().toArray(new String[0])) + .allowedMethods(ALLOWED_METHOD_NAMES.split(",")) + .allowCredentials(true); + } +} From 7df0f63d4a609117625d74a5b7c16286ea5d601f Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Sun, 5 Jan 2025 00:34:08 +0900 Subject: [PATCH 06/75] =?UTF-8?q?[TNT-76]=20feat:=20=EC=84=B8=EC=85=98=20?= =?UTF-8?q?=ED=95=84=ED=84=B0=20=EA=B5=AC=ED=98=84=20=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/SessionAuthenticationFilter.java | 97 +++++++++++++++++++ .../com/tnt/global/auth/SessionService.java | 13 +++ .../com/tnt/global/config/SecurityConfig.java | 10 ++ 3 files changed, 120 insertions(+) create mode 100644 src/main/java/com/tnt/global/auth/SessionAuthenticationFilter.java create mode 100644 src/main/java/com/tnt/global/auth/SessionService.java diff --git a/src/main/java/com/tnt/global/auth/SessionAuthenticationFilter.java b/src/main/java/com/tnt/global/auth/SessionAuthenticationFilter.java new file mode 100644 index 00000000..11d34042 --- /dev/null +++ b/src/main/java/com/tnt/global/auth/SessionAuthenticationFilter.java @@ -0,0 +1,97 @@ +package com.tnt.global.auth; + +import java.io.IOException; +import java.util.List; + +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; +import org.springframework.security.core.authority.mapping.NullAuthoritiesMapper; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.util.AntPathMatcher; +import org.springframework.web.filter.OncePerRequestFilter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@RequiredArgsConstructor +public class SessionAuthenticationFilter extends OncePerRequestFilter { + + private final GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper(); + private final SessionService sessionService; + private final AntPathMatcher pathMatcher = new AntPathMatcher(); + private final List allowedUris; + + @Override + protected void doFilterInternal( + HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain + ) throws ServletException, IOException { + String requestUri = request.getRequestURI(); + String queryString = request.getQueryString(); + log.info("들어온 요청 - URI: {}, Query: {}, Method: {}", + requestUri, + queryString != null ? queryString : "쿼리 스트링 없음", + request.getMethod()); + + if (isAllowedUri(requestUri)) { + log.info("{} 허용 URI. 세션 유효성 검사 스킵.", requestUri); + filterChain.doFilter(request, response); + return; + } + + String currentMemberSession = sessionService.extractCurrentMemberSession(request).orElse(null); + + log.info("사용자 세션 추출 - MemberSession: {}", currentMemberSession != null ? "존재" : "존재하지 않음"); + + checkMemberSessionAndAuthentication(request, response, filterChain); + } + + private boolean isAllowedUri(String requestUri) { + boolean allowed = false; + for (String pattern : allowedUris) { + if (pathMatcher.match(pattern, requestUri)) { + allowed = true; + break; + } + } + log.info("URI {} is {}allowed", requestUri, allowed ? "" : "not "); + return allowed; + } + + public void checkMemberSessionAndAuthentication( + HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain + ) throws ServletException, IOException { + log.info("checkMemberSessionAndAuthentication() 호출"); + sessionService.extractCurrentMemberSession(request) + .filter(sessionService::validateCurrentMemberSession) + .flatMap(sessionService::extractMemberId) + .flatMap(memberId -> memberRepository.findByMemberIdAndMemberDelete(memberId, null)) + .ifPresent(this::saveAuthentication); + + filterChain.doFilter(request, response); + } + + public void saveAuthentication(Member currentMember) { + String password = currentMember.getEmail(); + + UserDetails userDetailsUser = org.springframework.security.core.userdetails.User.builder() + .username(currentMember.getMemberId()) + .password(password) + .build(); + + Authentication authentication = new UsernamePasswordAuthenticationToken(userDetailsUser, null, + authoritiesMapper.mapAuthorities(userDetailsUser.getAuthorities())); + + SecurityContextHolder.getContext().setAuthentication(authentication); + } +} diff --git a/src/main/java/com/tnt/global/auth/SessionService.java b/src/main/java/com/tnt/global/auth/SessionService.java new file mode 100644 index 00000000..121f9b49 --- /dev/null +++ b/src/main/java/com/tnt/global/auth/SessionService.java @@ -0,0 +1,13 @@ +package com.tnt.global.auth; + +import org.springframework.stereotype.Service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Service +@RequiredArgsConstructor +public class SessionService { + +} diff --git a/src/main/java/com/tnt/global/config/SecurityConfig.java b/src/main/java/com/tnt/global/config/SecurityConfig.java index e19291c5..9cdb2454 100644 --- a/src/main/java/com/tnt/global/config/SecurityConfig.java +++ b/src/main/java/com/tnt/global/config/SecurityConfig.java @@ -1,5 +1,7 @@ package com.tnt.global.config; +import java.util.Arrays; + import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.Customizer; @@ -11,6 +13,8 @@ import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.SecurityFilterChain; +import com.tnt.global.auth.SessionAuthenticationFilter; + import lombok.RequiredArgsConstructor; @Configuration @@ -44,4 +48,10 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { return http.build(); } + + @Bean + public SessionAuthenticationFilter sessionAuthenticationFilter() { + return new SessionAuthenticationFilter(jwtService, redisService, Arrays.asList(ALLOWED_URIS)); + } + } From 4a22cd0ae9c891889cf8efa610c4083b5e792783 Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Sun, 5 Jan 2025 22:59:57 +0900 Subject: [PATCH 07/75] =?UTF-8?q?[TNT-28]=20feat:=20cors=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/tnt/global/config/CorsProperties.java | 18 -------------- .../java/com/tnt/global/config/WebConfig.java | 24 ------------------- 2 files changed, 42 deletions(-) delete mode 100644 src/main/java/com/tnt/global/config/CorsProperties.java delete mode 100644 src/main/java/com/tnt/global/config/WebConfig.java diff --git a/src/main/java/com/tnt/global/config/CorsProperties.java b/src/main/java/com/tnt/global/config/CorsProperties.java deleted file mode 100644 index 16922059..00000000 --- a/src/main/java/com/tnt/global/config/CorsProperties.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.tnt.global.config; - -import java.util.List; - -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.context.annotation.Configuration; - -import lombok.Getter; -import lombok.Setter; - -@Setter -@Getter -@Configuration -@ConfigurationProperties(prefix = "cors") -public class CorsProperties { - - private List allowedOrigins; -} diff --git a/src/main/java/com/tnt/global/config/WebConfig.java b/src/main/java/com/tnt/global/config/WebConfig.java deleted file mode 100644 index 7b2bba0f..00000000 --- a/src/main/java/com/tnt/global/config/WebConfig.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.tnt.global.config; - -import org.springframework.context.annotation.Configuration; -import org.springframework.web.servlet.config.annotation.CorsRegistry; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; - -import lombok.RequiredArgsConstructor; - -@Configuration -@RequiredArgsConstructor -public class WebConfig implements WebMvcConfigurer { - - public static final String ALLOWED_METHOD_NAMES = "GET,HEAD,POST,PUT,DELETE,TRACE,OPTIONS,PATCH"; - - private final CorsProperties corsProperties; - - @Override - public void addCorsMappings(CorsRegistry registry) { - registry.addMapping("/**") - .allowedOrigins(corsProperties.getAllowedOrigins().toArray(new String[0])) - .allowedMethods(ALLOWED_METHOD_NAMES.split(",")) - .allowCredentials(true); - } -} From 287a9663dbe7743e8df41af00cb42e1cf5df2dec Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Sun, 5 Jan 2025 23:00:22 +0900 Subject: [PATCH 08/75] =?UTF-8?q?[TNT-28]=20feat:=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=9C=84=EC=B9=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/tnt/global/auth/SessionService.java | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 src/main/java/com/tnt/global/auth/SessionService.java diff --git a/src/main/java/com/tnt/global/auth/SessionService.java b/src/main/java/com/tnt/global/auth/SessionService.java deleted file mode 100644 index 121f9b49..00000000 --- a/src/main/java/com/tnt/global/auth/SessionService.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.tnt.global.auth; - -import org.springframework.stereotype.Service; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -@Slf4j -@Service -@RequiredArgsConstructor -public class SessionService { - -} From a98cdbcf0aaad80e11310711e2dab6b3a9f46c68 Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Sun, 5 Jan 2025 23:00:43 +0900 Subject: [PATCH 09/75] =?UTF-8?q?[TNT-28]=20feat:=20=EC=BB=A4=EC=8A=A4?= =?UTF-8?q?=ED=85=80=20=EC=84=B8=EC=85=98=20=EA=B0=9D=EC=B2=B4=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/tnt/domain/auth/SessionInfo.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 src/main/java/com/tnt/domain/auth/SessionInfo.java diff --git a/src/main/java/com/tnt/domain/auth/SessionInfo.java b/src/main/java/com/tnt/domain/auth/SessionInfo.java new file mode 100644 index 00000000..81b3aa80 --- /dev/null +++ b/src/main/java/com/tnt/domain/auth/SessionInfo.java @@ -0,0 +1,16 @@ +package com.tnt.domain.auth; + +import java.time.LocalDateTime; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class SessionInfo { + + private String status; // login/logout + private LocalDateTime lastAccessTime; + private String userAgent; + private String clientIp; +} From a5090c1eaa74abcf96457067e9866df23b37c851 Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Sun, 5 Jan 2025 23:01:07 +0900 Subject: [PATCH 10/75] =?UTF-8?q?[TNT-28]=20feat:=20BaseTimeEntity=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/tnt/global/entity/BaseTimeEntity.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 src/main/java/com/tnt/global/entity/BaseTimeEntity.java diff --git a/src/main/java/com/tnt/global/entity/BaseTimeEntity.java b/src/main/java/com/tnt/global/entity/BaseTimeEntity.java new file mode 100644 index 00000000..b09468cb --- /dev/null +++ b/src/main/java/com/tnt/global/entity/BaseTimeEntity.java @@ -0,0 +1,26 @@ +package com.tnt.global.entity; + +import java.time.LocalDateTime; + +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import jakarta.persistence.Column; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; +import lombok.Getter; + +@Getter +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +public abstract class BaseTimeEntity { + + @CreatedDate + @Column(name = "created_at", updatable = false, columnDefinition = "TIMESTAMP") + private LocalDateTime createdAt; + + @LastModifiedDate + @Column(name = "updated_at", columnDefinition = "TIMESTAMP") + private LocalDateTime updatedAt; +} From 3ec4f7ae74880bdbceeaea0d522751a68e4cf1a9 Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Sun, 5 Jan 2025 23:01:27 +0900 Subject: [PATCH 11/75] =?UTF-8?q?[TNT-28]=20chore:=20TSID=20=EB=9D=BC?= =?UTF-8?q?=EC=9D=B4=EB=B8=8C=EB=9F=AC=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 9f5e7137..bf4a301d 100644 --- a/build.gradle +++ b/build.gradle @@ -126,7 +126,10 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-aop' implementation 'org.springframework.boot:spring-boot-starter-webflux' - // 애플 로그인을 위한 라이브러리 + // TSID + implementation 'com.github.f4b6a3:tsid-creator:5.2.6' + + // 애플 로그인 관련 라이브러리 implementation 'com.auth0:jwks-rsa:0.22.1' implementation 'org.json:json:20231013' implementation 'org.bouncycastle:bcprov-jdk18on:1.79' From ddcebf1d7658bd07a98188f641fd75d19ec91958 Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Sun, 5 Jan 2025 23:04:29 +0900 Subject: [PATCH 12/75] =?UTF-8?q?[TNT-28]=20refactor:=20value=EC=97=90=20j?= =?UTF-8?q?son=20=EA=B5=AC=EC=A1=B0=EB=A1=9C=20=EC=A0=80=EC=9E=A5=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/tnt/global/config/RedisConfig.java | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/tnt/global/config/RedisConfig.java b/src/main/java/com/tnt/global/config/RedisConfig.java index 02d6b820..2b951f54 100644 --- a/src/main/java/com/tnt/global/config/RedisConfig.java +++ b/src/main/java/com/tnt/global/config/RedisConfig.java @@ -7,8 +7,11 @@ import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.repository.configuration.EnableRedisRepositories; +import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; +import com.tnt.domain.auth.SessionInfo; + @Configuration @EnableRedisRepositories public class RedisConfig { @@ -25,11 +28,15 @@ public RedisConnectionFactory redisConnectionFactory() { } @Bean - public RedisTemplate redisTemplate() { - RedisTemplate redisTemplate = new RedisTemplate<>(); + public RedisTemplate redisTemplate() { + RedisTemplate redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(redisConnectionFactory()); redisTemplate.setKeySerializer(new StringRedisSerializer()); - redisTemplate.setValueSerializer(new StringRedisSerializer()); + + Jackson2JsonRedisSerializer jsonRedisSerializer = + new Jackson2JsonRedisSerializer<>(SessionInfo.class); + redisTemplate.setValueSerializer(jsonRedisSerializer); + return redisTemplate; } } From 88df7903adebe3fbcb6458eb487e549f7c70edab Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Sun, 5 Jan 2025 23:07:27 +0900 Subject: [PATCH 13/75] =?UTF-8?q?[TNT-28]=20feat:=20findByIdAndDeletedAt?= =?UTF-8?q?=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/member/repository/MemberRepository.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 src/main/java/com/tnt/domain/member/repository/MemberRepository.java diff --git a/src/main/java/com/tnt/domain/member/repository/MemberRepository.java b/src/main/java/com/tnt/domain/member/repository/MemberRepository.java new file mode 100644 index 00000000..eb710dfa --- /dev/null +++ b/src/main/java/com/tnt/domain/member/repository/MemberRepository.java @@ -0,0 +1,13 @@ +package com.tnt.domain.member.repository; + +import java.time.LocalDateTime; +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.tnt.domain.member.Member; + +public interface MemberRepository extends JpaRepository { + + Optional findByIdAndDeletedAt(Long id, LocalDateTime deletedAt); +} From 436f4844beee98e281212233edc71b5627cdab43 Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Sun, 5 Jan 2025 23:07:53 +0900 Subject: [PATCH 14/75] =?UTF-8?q?[TNT-28]=20feat:=20Member=20entity=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/tnt/domain/member/Member.java | 60 +++++++++++++++++++ .../com/tnt/domain/member/SocialType.java | 7 +++ 2 files changed, 67 insertions(+) create mode 100644 src/main/java/com/tnt/domain/member/Member.java create mode 100644 src/main/java/com/tnt/domain/member/SocialType.java diff --git a/src/main/java/com/tnt/domain/member/Member.java b/src/main/java/com/tnt/domain/member/Member.java new file mode 100644 index 00000000..ceca3301 --- /dev/null +++ b/src/main/java/com/tnt/domain/member/Member.java @@ -0,0 +1,60 @@ +package com.tnt.domain.member; + +import java.time.LocalDateTime; + +import com.tnt.global.entity.BaseTimeEntity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@Table(name = "member") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Member extends BaseTimeEntity { + + @Id + @Column(name = "id", nullable = false, unique = true) + private Long id; + + @Column(name = "social_id", nullable = false, unique = true) + private String socialId; + + @Column(name = "email", nullable = false, length = 100) + private String email; + + @Column(name = "name", nullable = false, length = 50) + private String name; + + @Column(name = "age", nullable = false) + private String age; + + @Column(name = "profile", nullable = false) + private String profile; + + @Column(name = "deleted_at") + private LocalDateTime deletedAt; + + @Enumerated(EnumType.STRING) + @Column(name = "social_type", nullable = false) + private SocialType socialType; + + @Builder + public Member(Long id, String socialId, String email, String name, String age, SocialType socialType) { + this.id = id; + this.socialId = socialId; + this.email = email; + this.name = name; + this.age = age; + this.profile = ""; + this.socialType = socialType; + } +} diff --git a/src/main/java/com/tnt/domain/member/SocialType.java b/src/main/java/com/tnt/domain/member/SocialType.java new file mode 100644 index 00000000..fd181eea --- /dev/null +++ b/src/main/java/com/tnt/domain/member/SocialType.java @@ -0,0 +1,7 @@ +package com.tnt.domain.member; + +public enum SocialType { + KAKAO, + GOOGLE, + APPLE +} From f60cdda4eeb7039037cfd33421d5007ee099ad3e Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Sun, 5 Jan 2025 23:09:21 +0900 Subject: [PATCH 15/75] =?UTF-8?q?[TNT-28]=20refactor:=20=EC=A0=91=EA=B7=BC?= =?UTF-8?q?=20=EC=A0=9C=EC=96=B4=EC=9E=90,=20=EB=B3=80=EC=88=98=EB=AA=85?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/SessionAuthenticationFilter.java | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/tnt/global/auth/SessionAuthenticationFilter.java b/src/main/java/com/tnt/global/auth/SessionAuthenticationFilter.java index 11d34042..4e7a34eb 100644 --- a/src/main/java/com/tnt/global/auth/SessionAuthenticationFilter.java +++ b/src/main/java/com/tnt/global/auth/SessionAuthenticationFilter.java @@ -12,6 +12,10 @@ import org.springframework.util.AntPathMatcher; import org.springframework.web.filter.OncePerRequestFilter; +import com.tnt.application.auth.SessionService; +import com.tnt.domain.member.Member; +import com.tnt.domain.member.repository.MemberRepository; + import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; @@ -24,9 +28,10 @@ public class SessionAuthenticationFilter extends OncePerRequestFilter { private final GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper(); - private final SessionService sessionService; private final AntPathMatcher pathMatcher = new AntPathMatcher(); private final List allowedUris; + private final SessionService sessionService; + private final MemberRepository memberRepository; @Override protected void doFilterInternal( @@ -40,16 +45,13 @@ protected void doFilterInternal( requestUri, queryString != null ? queryString : "쿼리 스트링 없음", request.getMethod()); - if (isAllowedUri(requestUri)) { log.info("{} 허용 URI. 세션 유효성 검사 스킵.", requestUri); filterChain.doFilter(request, response); return; } - - String currentMemberSession = sessionService.extractCurrentMemberSession(request).orElse(null); - - log.info("사용자 세션 추출 - MemberSession: {}", currentMemberSession != null ? "존재" : "존재하지 않음"); + String memberSession = sessionService.extractMemberSession(request).orElse(null); + log.info("사용자 세션 추출 - MemberSession: {}", memberSession != null ? "존재" : "존재하지 않음"); checkMemberSessionAndAuthentication(request, response, filterChain); } @@ -63,29 +65,30 @@ private boolean isAllowedUri(String requestUri) { } } log.info("URI {} is {}allowed", requestUri, allowed ? "" : "not "); + return allowed; } - public void checkMemberSessionAndAuthentication( + private void checkMemberSessionAndAuthentication( HttpServletRequest request, HttpServletResponse response, FilterChain filterChain ) throws ServletException, IOException { log.info("checkMemberSessionAndAuthentication() 호출"); - sessionService.extractCurrentMemberSession(request) - .filter(sessionService::validateCurrentMemberSession) + sessionService.extractMemberSession(request) + .filter(sessionService::validateMemberSession) .flatMap(sessionService::extractMemberId) - .flatMap(memberId -> memberRepository.findByMemberIdAndMemberDelete(memberId, null)) + .flatMap(memberId -> memberRepository.findByIdAndDeletedAt(memberId, null)) .ifPresent(this::saveAuthentication); filterChain.doFilter(request, response); } - public void saveAuthentication(Member currentMember) { - String password = currentMember.getEmail(); + private void saveAuthentication(Member currentMember) { + String password = currentMember.getEmail(); // SecurityContext password UserDetails userDetailsUser = org.springframework.security.core.userdetails.User.builder() - .username(currentMember.getMemberId()) + .username(String.valueOf(currentMember.getId())) .password(password) .build(); From 2dd0d6a47e9551d5c0237e351942c78fd517c895 Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Sun, 5 Jan 2025 23:11:09 +0900 Subject: [PATCH 16/75] =?UTF-8?q?[TNT-28]=20feat:=20=EC=84=B8=EC=85=98=20?= =?UTF-8?q?=ED=95=84=ED=84=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/tnt/global/config/SecurityConfig.java | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/tnt/global/config/SecurityConfig.java b/src/main/java/com/tnt/global/config/SecurityConfig.java index 9cdb2454..f27f7255 100644 --- a/src/main/java/com/tnt/global/config/SecurityConfig.java +++ b/src/main/java/com/tnt/global/config/SecurityConfig.java @@ -12,7 +12,10 @@ import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.logout.LogoutFilter; +import com.tnt.application.auth.SessionService; +import com.tnt.domain.member.repository.MemberRepository; import com.tnt.global.auth.SessionAuthenticationFilter; import lombok.RequiredArgsConstructor; @@ -29,6 +32,8 @@ public class SecurityConfig { "/index.html", "/api/oauth2/**" }; + private final SessionService sessionService; + private final MemberRepository memberRepository; @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { @@ -44,14 +49,14 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { ) .authorizeHttpRequests(request -> request.requestMatchers(ALLOWED_URIS).permitAll() - .anyRequest().authenticated()); + .anyRequest().authenticated()) + .addFilterAfter(sessionAuthenticationFilter(), LogoutFilter.class); return http.build(); } @Bean public SessionAuthenticationFilter sessionAuthenticationFilter() { - return new SessionAuthenticationFilter(jwtService, redisService, Arrays.asList(ALLOWED_URIS)); + return new SessionAuthenticationFilter(Arrays.asList(ALLOWED_URIS), sessionService, memberRepository); } - } From 8523fdbedf9c85f9a890c470e8d335c9979a3025 Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Sun, 5 Jan 2025 23:11:56 +0900 Subject: [PATCH 17/75] =?UTF-8?q?[TNT-28]=20feat:=20=EC=84=B8=EC=85=98=20?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tnt/application/auth/SessionService.java | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 src/main/java/com/tnt/application/auth/SessionService.java diff --git a/src/main/java/com/tnt/application/auth/SessionService.java b/src/main/java/com/tnt/application/auth/SessionService.java new file mode 100644 index 00000000..bea81a27 --- /dev/null +++ b/src/main/java/com/tnt/application/auth/SessionService.java @@ -0,0 +1,79 @@ +package com.tnt.application.auth; + +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; + +import com.tnt.domain.auth.SessionInfo; + +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Service +@RequiredArgsConstructor +public class SessionService { + + private static final String SESSION_COOKIE_NAME = "SESSION"; + private static final String LOGIN_STATUS = "login"; + private static final long SESSION_DURATION = 2 * 24 * 60 * 60L; // 48시간 + private final RedisTemplate redisTemplate; + + public Optional extractMemberSession(HttpServletRequest request) { + Cookie[] cookies = request.getCookies(); + if (cookies == null) { + log.info("쿠키가 존재하지 않습니다."); + return Optional.empty(); + } + + return Arrays.stream(cookies) + .filter(cookie -> SESSION_COOKIE_NAME.equals(cookie.getName())) + .map(Cookie::getValue) + .findFirst(); + } + + public boolean validateMemberSession(String sessionId) { + SessionInfo sessionInfo = redisTemplate.opsForValue().get(sessionId); + boolean isValid = LOGIN_STATUS.equals(sessionInfo.getStatus()); + + log.info("세션 검증 결과 - SessionId: {}, Valid: {}", sessionId, isValid); + return isValid; + } + + public Optional extractMemberId(String sessionId) { + try { + return Optional.of(Long.parseLong(sessionId.split(":")[0])); + } catch (Exception e) { + log.error("세션 ID에서 회원 ID를 추출하는데 실패했습니다. sessionId: {}", sessionId, e); + return Optional.empty(); + } + } + + // 로그인 시 세션 생성을 위한 메서드 + public void createSession(String memberId, HttpServletRequest request) { + SessionInfo sessionInfo = SessionInfo.builder() + .status(LOGIN_STATUS) + .lastAccessTime(LocalDateTime.now()) + .userAgent(request.getHeader("User-Agent")) + .clientIp(request.getRemoteAddr()) + .build(); + + redisTemplate.opsForValue().set( + memberId, + sessionInfo, + SESSION_DURATION, + TimeUnit.SECONDS + ); + } + + // 로그아웃 시 세션 삭제를 위한 메서드 + public void removeSession(String sessionId) { + redisTemplate.delete(sessionId); + } +} From 2ac8730fc62f5ffe7c0410b8ba71ee3315e01855 Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Mon, 6 Jan 2025 00:15:09 +0900 Subject: [PATCH 18/75] =?UTF-8?q?[TNT-28]=20chore:=20TSID=20=EB=9D=BC?= =?UTF-8?q?=EC=9D=B4=EB=B8=8C=EB=9F=AC=EB=A6=AC=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index bf4a301d..eaacab70 100644 --- a/build.gradle +++ b/build.gradle @@ -127,7 +127,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-webflux' // TSID - implementation 'com.github.f4b6a3:tsid-creator:5.2.6' + implementation 'io.hypersistence:hypersistence-utils-hibernate-63:3.9.0' // 애플 로그인 관련 라이브러리 implementation 'com.auth0:jwks-rsa:0.22.1' From 7e22fabb6e1afd6e7301679616ee50c23d8f14d8 Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Mon, 6 Jan 2025 00:15:31 +0900 Subject: [PATCH 19/75] =?UTF-8?q?[TNT-28]=20chore:=20gradle=20=EB=B2=84?= =?UTF-8?q?=EC=A0=84=208.5=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 --- gradle/wrapper/gradle-wrapper.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 3fa8f862..1af9e093 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME From fff07b4b93c73f5940471a3bdf747f655b8a388e Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Mon, 6 Jan 2025 00:16:53 +0900 Subject: [PATCH 20/75] =?UTF-8?q?[TNT-28]=20refactor:=20tsid=20=EC=96=B4?= =?UTF-8?q?=EB=85=B8=ED=85=8C=EC=9D=B4=EC=85=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/tnt/domain/member/Member.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/tnt/domain/member/Member.java b/src/main/java/com/tnt/domain/member/Member.java index ceca3301..76f1d81e 100644 --- a/src/main/java/com/tnt/domain/member/Member.java +++ b/src/main/java/com/tnt/domain/member/Member.java @@ -4,6 +4,7 @@ import com.tnt.global.entity.BaseTimeEntity; +import io.hypersistence.utils.hibernate.id.Tsid; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; @@ -22,6 +23,7 @@ public class Member extends BaseTimeEntity { @Id + @Tsid @Column(name = "id", nullable = false, unique = true) private Long id; @@ -48,8 +50,7 @@ public class Member extends BaseTimeEntity { private SocialType socialType; @Builder - public Member(Long id, String socialId, String email, String name, String age, SocialType socialType) { - this.id = id; + public Member(String socialId, String email, String name, String age, SocialType socialType) { this.socialId = socialId; this.email = email; this.name = name; From 38ee34f16fa2b1464fa924b015d808a88496a541 Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Mon, 6 Jan 2025 00:17:05 +0900 Subject: [PATCH 21/75] =?UTF-8?q?[TNT-28]=20refactor:=20repository=20?= =?UTF-8?q?=EC=96=B4=EB=85=B8=ED=85=8C=EC=9D=B4=EC=85=98=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/tnt/domain/member/repository/MemberRepository.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/com/tnt/domain/member/repository/MemberRepository.java b/src/main/java/com/tnt/domain/member/repository/MemberRepository.java index eb710dfa..260ea131 100644 --- a/src/main/java/com/tnt/domain/member/repository/MemberRepository.java +++ b/src/main/java/com/tnt/domain/member/repository/MemberRepository.java @@ -4,9 +4,11 @@ import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; import com.tnt.domain.member.Member; +@Repository public interface MemberRepository extends JpaRepository { Optional findByIdAndDeletedAt(Long id, LocalDateTime deletedAt); From f71c5934348236d1297a5fbedaf7f93cd8d5bd56 Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Mon, 6 Jan 2025 00:41:26 +0900 Subject: [PATCH 22/75] =?UTF-8?q?[TNT-28]=20refactor:=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=EC=9E=90=20=ED=8C=8C=EB=9D=BC=EB=AF=B8=ED=84=B0=EC=97=90=20id?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/tnt/domain/member/Member.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/tnt/domain/member/Member.java b/src/main/java/com/tnt/domain/member/Member.java index 76f1d81e..12db7363 100644 --- a/src/main/java/com/tnt/domain/member/Member.java +++ b/src/main/java/com/tnt/domain/member/Member.java @@ -50,7 +50,8 @@ public class Member extends BaseTimeEntity { private SocialType socialType; @Builder - public Member(String socialId, String email, String name, String age, SocialType socialType) { + public Member(Long id, String socialId, String email, String name, String age, SocialType socialType) { + this.id = id; this.socialId = socialId; this.email = email; this.name = name; From de025d6e42cb04f5605c9309beb224d432da6acf Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Mon, 6 Jan 2025 01:16:32 +0900 Subject: [PATCH 23/75] =?UTF-8?q?[TNT-28]=20feat:=20unauthorized=20?= =?UTF-8?q?=EC=98=88=EC=99=B8=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tnt/global/error/exception/UnauthorizedException.java | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 src/main/java/com/tnt/global/error/exception/UnauthorizedException.java diff --git a/src/main/java/com/tnt/global/error/exception/UnauthorizedException.java b/src/main/java/com/tnt/global/error/exception/UnauthorizedException.java new file mode 100644 index 00000000..6cdf6ff0 --- /dev/null +++ b/src/main/java/com/tnt/global/error/exception/UnauthorizedException.java @@ -0,0 +1,8 @@ +package com.tnt.global.error.exception; + +public class UnauthorizedException extends TnTException { + + public UnauthorizedException(String message) { + super(message); + } +} From b3f7b7a016abe009bfe3c060bd5ff328e9afd5e5 Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Mon, 6 Jan 2025 01:17:23 +0900 Subject: [PATCH 24/75] =?UTF-8?q?[TNT-28]=20chore:=20error=20=EB=94=94?= =?UTF-8?q?=EB=A0=89=ED=86=A0=EB=A6=AC=20=EC=BB=A4=EB=B2=84=EB=A6=AC?= =?UTF-8?q?=EC=A7=80=20=EA=B2=80=EC=A6=9D=20=EB=B0=B0=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index ea4bbaaa..f73265d8 100644 --- a/build.gradle +++ b/build.gradle @@ -106,7 +106,7 @@ jacocoTestCoverageVerification { excludes = [ '*.*Application', '*.*Config', - '*.*.*GlobalExceptionHandler' + '*.error.*' ] } } From 1ac7bde7b88f8c0902325f34b6482708a2886d4e Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Mon, 6 Jan 2025 01:22:29 +0900 Subject: [PATCH 25/75] =?UTF-8?q?[TNT-28]=20refactor:=20401=20=EC=98=88?= =?UTF-8?q?=EC=99=B8=20=EC=B6=94=EA=B0=80,=20=EC=A0=91=EA=B7=BC=20?= =?UTF-8?q?=EC=A0=9C=EC=96=B4=EC=9E=90=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../error/handler/GlobalExceptionHandler.java | 35 +++++++++++-------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/tnt/global/error/handler/GlobalExceptionHandler.java b/src/main/java/com/tnt/global/error/handler/GlobalExceptionHandler.java index 5306692e..06900c0b 100644 --- a/src/main/java/com/tnt/global/error/handler/GlobalExceptionHandler.java +++ b/src/main/java/com/tnt/global/error/handler/GlobalExceptionHandler.java @@ -14,6 +14,8 @@ import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; import org.springframework.web.multipart.MaxUploadSizeExceededException; +import com.tnt.global.error.exception.TnTException; +import com.tnt.global.error.exception.UnauthorizedException; import com.tnt.global.error.model.ErrorResponse; import jakarta.validation.ConstraintViolationException; @@ -33,7 +35,7 @@ public class GlobalExceptionHandler { // 필수 파라미터 예외 @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(MissingServletRequestParameterException.class) - public ErrorResponse handleMissingServletRequestParameter( + protected ErrorResponse handleMissingServletRequestParameter( MissingServletRequestParameterException exception) { log.warn("Required request parameter is missing: {}", exception.getParameterName()); String errorMessage = String.format("필수 파라미터 '%s'가 누락되었습니다.", exception.getParameterName()); @@ -44,25 +46,30 @@ public ErrorResponse handleMissingServletRequestParameter( // 파라미터 타입 예외 @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(MethodArgumentTypeMismatchException.class) - public ErrorResponse handleMethodArgumentTypeMismatch( + protected ErrorResponse handleMethodArgumentTypeMismatch( MethodArgumentTypeMismatchException exception) { + exception.getRequiredType(); log.warn("Type mismatch for parameter: {}. Required type: {}", exception.getName(), - exception.getRequiredType() != null ? exception.getRequiredType().getSimpleName() : "unknown"); + exception.getRequiredType().getSimpleName()); String errorMessage; - if (exception.getRequiredType() != null) { - errorMessage = String.format("파라미터 '%s'의 형식이 올바르지 않습니다. 예상 타입: %s", - exception.getName(), exception.getRequiredType().getSimpleName()); - } else { - errorMessage = String.format("파라미터 '%s'의 형식이 올바르지 않습니다.", exception.getName()); - } + exception.getRequiredType(); + errorMessage = String.format("파라미터 '%s'의 형식이 올바르지 않습니다. 예상 타입: %s", + exception.getName(), exception.getRequiredType().getSimpleName()); return new ErrorResponse(errorMessage); } + // 401 Unauthorized 예외 + @ResponseStatus(HttpStatus.UNAUTHORIZED) + @ExceptionHandler(UnauthorizedException.class) + protected ErrorResponse handleUnauthorizedException(TnTException exception) { + return new ErrorResponse(exception.getMessage()); + } + // @Validated 있는 클래스에서 @RequestParam, @PathVariable 등에 적용된 제약 조건 예외 @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(ConstraintViolationException.class) - public ErrorResponse handleConstraintViolationException(ConstraintViolationException exception) { + protected ErrorResponse handleConstraintViolationException(ConstraintViolationException exception) { log.warn("Constraint violation: {}", exception.getMessage()); List errors = exception.getConstraintViolations() @@ -78,7 +85,7 @@ public ErrorResponse handleConstraintViolationException(ConstraintViolationExcep // @Valid, @Validated 있는 곳에서 주로 @RequestBody dto 필드에 적용된 검증 어노테이션 유효성 검사 실패 예외 @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(MethodArgumentNotValidException.class) - public ErrorResponse handleMethodArgumentNotValidException( + protected ErrorResponse handleMethodArgumentNotValidException( MethodArgumentNotValidException exception) { log.warn(exception.getBindingResult().getAllErrors().getFirst().getDefaultMessage()); @@ -91,7 +98,7 @@ public ErrorResponse handleMethodArgumentNotValidException( HttpMessageNotReadableException.class, DateTimeException.class }) - public ErrorResponse handleDateTimeParseException(DateTimeException exception) { + protected ErrorResponse handleDateTimeParseException(DateTimeException exception) { log.warn(exception.getMessage()); return new ErrorResponse("DateTime 형식이 잘못되었습니다. 서버 관리자에게 문의해 주세요."); @@ -102,7 +109,7 @@ public ErrorResponse handleDateTimeParseException(DateTimeException exception) { @ExceptionHandler(value = { MaxUploadSizeExceededException.class }) - public ErrorResponse handleCustomBadRequestException(RuntimeException exception) { + protected ErrorResponse handleCustomBadRequestException(RuntimeException exception) { log.warn(exception.getMessage()); return new ErrorResponse(exception.getMessage()); @@ -111,7 +118,7 @@ public ErrorResponse handleCustomBadRequestException(RuntimeException exception) // 기타 500 예외 @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) @ExceptionHandler(RuntimeException.class) - public ErrorResponse handleRuntimeException(RuntimeException exception) { + protected ErrorResponse handleRuntimeException(RuntimeException exception) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < ERROR_KEY_LENGTH; i++) { From 0c56e0efba4dd45043e196d14b2896aeed708250 Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Mon, 6 Jan 2025 02:01:26 +0900 Subject: [PATCH 26/75] =?UTF-8?q?[TNT-28]=20refactor:=20=EC=83=81=ED=83=9C?= =?UTF-8?q?=20=ED=95=84=EB=93=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/tnt/domain/auth/SessionInfo.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/com/tnt/domain/auth/SessionInfo.java b/src/main/java/com/tnt/domain/auth/SessionInfo.java index 81b3aa80..629c1b31 100644 --- a/src/main/java/com/tnt/domain/auth/SessionInfo.java +++ b/src/main/java/com/tnt/domain/auth/SessionInfo.java @@ -9,7 +9,6 @@ @Builder public class SessionInfo { - private String status; // login/logout private LocalDateTime lastAccessTime; private String userAgent; private String clientIp; From 8144fe50e8135fdfa7fc9d5e96993ddbc019a26d Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Mon, 6 Jan 2025 02:02:00 +0900 Subject: [PATCH 27/75] =?UTF-8?q?[TNT-28]=20refactor:=20=EC=84=B8=EC=85=98?= =?UTF-8?q?=20=EA=B2=80=EC=A6=9D=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tnt/application/auth/SessionService.java | 49 ++++++++++++------- 1 file changed, 31 insertions(+), 18 deletions(-) diff --git a/src/main/java/com/tnt/application/auth/SessionService.java b/src/main/java/com/tnt/application/auth/SessionService.java index bea81a27..5c553f9f 100644 --- a/src/main/java/com/tnt/application/auth/SessionService.java +++ b/src/main/java/com/tnt/application/auth/SessionService.java @@ -9,6 +9,7 @@ import org.springframework.stereotype.Service; import com.tnt.domain.auth.SessionInfo; +import com.tnt.global.error.exception.UnauthorizedException; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; @@ -20,45 +21,57 @@ @RequiredArgsConstructor public class SessionService { + static final long SESSION_DURATION = 2L * 24 * 60 * 60; // 48시간 private static final String SESSION_COOKIE_NAME = "SESSION"; - private static final String LOGIN_STATUS = "login"; - private static final long SESSION_DURATION = 2 * 24 * 60 * 60L; // 48시간 private final RedisTemplate redisTemplate; public Optional extractMemberSession(HttpServletRequest request) { Cookie[] cookies = request.getCookies(); if (cookies == null) { log.info("쿠키가 존재하지 않습니다."); - return Optional.empty(); + throw new UnauthorizedException("세션 쿠키가 존재하지 않습니다."); } return Arrays.stream(cookies) .filter(cookie -> SESSION_COOKIE_NAME.equals(cookie.getName())) .map(Cookie::getValue) - .findFirst(); + .findFirst() + .or(() -> { + throw new UnauthorizedException("세션 쿠키가 존재하지 않습니다."); + }); } - public boolean validateMemberSession(String sessionId) { - SessionInfo sessionInfo = redisTemplate.opsForValue().get(sessionId); - boolean isValid = LOGIN_STATUS.equals(sessionInfo.getStatus()); + public void validateMemberSession(String sessionId) { + // 1. 세션 존재 여부 확인 + if (Boolean.FALSE.equals(redisTemplate.hasKey(sessionId))) { + log.info("세션이 존재하지 않음 - SessionId: {}", sessionId); + throw new UnauthorizedException("세션 스토리지에 세션이 존재하지 않습니다."); + } - log.info("세션 검증 결과 - SessionId: {}, Valid: {}", sessionId, isValid); - return isValid; - } + SessionInfo sessionInfo = redisTemplate.opsForValue().get(sessionId); - public Optional extractMemberId(String sessionId) { - try { - return Optional.of(Long.parseLong(sessionId.split(":")[0])); - } catch (Exception e) { - log.error("세션 ID에서 회원 ID를 추출하는데 실패했습니다. sessionId: {}", sessionId, e); - return Optional.empty(); + // 2. 세션 유효성 확인 + LocalDateTime lastAccessTime = sessionInfo.getLastAccessTime(); + if (lastAccessTime.isBefore(LocalDateTime.now().minusDays(2))) { // 48시간 지났는지 체크 + log.info("세션이 만료됨 - SessionId: {}, LastAccessTime: {}", sessionId, lastAccessTime); + redisTemplate.delete(sessionId); // 만료된 세션 삭제 + throw new UnauthorizedException("세션이 만료되었습니다."); } + + // 3. 세션 갱신 (마지막 접근 시간 업데이트) + sessionInfo = SessionInfo.builder() + .lastAccessTime(LocalDateTime.now()) + .userAgent(sessionInfo.getUserAgent()) + .clientIp(sessionInfo.getClientIp()) + .build(); + + redisTemplate.opsForValue().set(sessionId, sessionInfo, SESSION_DURATION, TimeUnit.SECONDS); + log.info("세션 유효성 검증 완료 및 갱신 - SessionId: {}", sessionId); } // 로그인 시 세션 생성을 위한 메서드 public void createSession(String memberId, HttpServletRequest request) { SessionInfo sessionInfo = SessionInfo.builder() - .status(LOGIN_STATUS) .lastAccessTime(LocalDateTime.now()) .userAgent(request.getHeader("User-Agent")) .clientIp(request.getRemoteAddr()) @@ -72,7 +85,7 @@ public void createSession(String memberId, HttpServletRequest request) { ); } - // 로그아웃 시 세션 삭제를 위한 메서드 + // 로그아웃 시 세션 삭제 public void removeSession(String sessionId) { redisTemplate.delete(sessionId); } From 446702cd327e2d7da37ad5182cd2cb786c9fe433 Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Mon, 6 Jan 2025 02:03:09 +0900 Subject: [PATCH 28/75] =?UTF-8?q?[TNT-28]=20refactor:=20=EC=9D=B8=EC=A6=9D?= =?UTF-8?q?=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95,=20401=20=EC=98=88?= =?UTF-8?q?=EC=99=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/SessionAuthenticationFilter.java | 67 ++++++++++++++----- 1 file changed, 49 insertions(+), 18 deletions(-) diff --git a/src/main/java/com/tnt/global/auth/SessionAuthenticationFilter.java b/src/main/java/com/tnt/global/auth/SessionAuthenticationFilter.java index 4e7a34eb..d1c24b0e 100644 --- a/src/main/java/com/tnt/global/auth/SessionAuthenticationFilter.java +++ b/src/main/java/com/tnt/global/auth/SessionAuthenticationFilter.java @@ -1,20 +1,24 @@ package com.tnt.global.auth; import java.io.IOException; +import java.time.LocalDateTime; import java.util.List; +import java.util.Map; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; import org.springframework.security.core.authority.mapping.NullAuthoritiesMapper; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.util.AntPathMatcher; import org.springframework.web.filter.OncePerRequestFilter; +import com.fasterxml.jackson.databind.ObjectMapper; import com.tnt.application.auth.SessionService; -import com.tnt.domain.member.Member; import com.tnt.domain.member.repository.MemberRepository; +import com.tnt.global.error.exception.UnauthorizedException; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; @@ -45,15 +49,18 @@ protected void doFilterInternal( requestUri, queryString != null ? queryString : "쿼리 스트링 없음", request.getMethod()); + if (isAllowedUri(requestUri)) { log.info("{} 허용 URI. 세션 유효성 검사 스킵.", requestUri); filterChain.doFilter(request, response); return; } - String memberSession = sessionService.extractMemberSession(request).orElse(null); - log.info("사용자 세션 추출 - MemberSession: {}", memberSession != null ? "존재" : "존재하지 않음"); - checkMemberSessionAndAuthentication(request, response, filterChain); + try { + checkSessionAndAuthentication(request, response, filterChain); + } catch (UnauthorizedException e) { + handleUnauthorizedException(response, e); + } } private boolean isAllowedUri(String requestUri) { @@ -69,32 +76,56 @@ private boolean isAllowedUri(String requestUri) { return allowed; } - private void checkMemberSessionAndAuthentication( + private void checkSessionAndAuthentication( HttpServletRequest request, HttpServletResponse response, FilterChain filterChain ) throws ServletException, IOException { - log.info("checkMemberSessionAndAuthentication() 호출"); - sessionService.extractMemberSession(request) - .filter(sessionService::validateMemberSession) - .flatMap(sessionService::extractMemberId) - .flatMap(memberId -> memberRepository.findByIdAndDeletedAt(memberId, null)) - .ifPresent(this::saveAuthentication); + log.info("세션 검증 시작"); + + String sessionId = sessionService.extractMemberSession(request) + .orElseThrow(() -> new UnauthorizedException("세션이 존재하지 않습니다.")); + + sessionService.validateMemberSession(sessionId); + Long memberId = Long.parseLong(sessionId); + + saveAuthentication(memberId); filterChain.doFilter(request, response); } - private void saveAuthentication(Member currentMember) { - String password = currentMember.getEmail(); // SecurityContext password + private void handleUnauthorizedException( + HttpServletResponse response, + UnauthorizedException exception + ) throws IOException { + log.error("인증 실패: {}", exception.getMessage()); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setContentType("application/json;charset=UTF-8"); + + Map errorResponse = Map.of( + "message", exception.getMessage(), + "status", HttpServletResponse.SC_UNAUTHORIZED, + "timestamp", LocalDateTime.now().toString() + ); + + new ObjectMapper().writeValueAsString(errorResponse); + response.getWriter().write(new ObjectMapper().writeValueAsString(errorResponse)); + } - UserDetails userDetailsUser = org.springframework.security.core.userdetails.User.builder() - .username(String.valueOf(currentMember.getId())) - .password(password) + private void saveAuthentication(Long memberId) { + UserDetails userDetails = User.builder() + .username(String.valueOf(memberId)) + .password("") + .roles("USER") .build(); - Authentication authentication = new UsernamePasswordAuthenticationToken(userDetailsUser, null, - authoritiesMapper.mapAuthorities(userDetailsUser.getAuthorities())); + Authentication authentication = new UsernamePasswordAuthenticationToken( + userDetails, + null, + authoritiesMapper.mapAuthorities(userDetails.getAuthorities()) + ); SecurityContextHolder.getContext().setAuthentication(authentication); + log.info("시큐리티 컨텍스트에 인증 정보 저장 완료 - MemberId: {}", memberId); } } From 4356b0b765b9acdec193054541ae84b88395d033 Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Mon, 6 Jan 2025 02:07:30 +0900 Subject: [PATCH 29/75] =?UTF-8?q?[TNT-28]=20feat:=20SessionServiceTest=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/auth/SessionServiceTest.java | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 src/test/java/com/tnt/application/auth/SessionServiceTest.java diff --git a/src/test/java/com/tnt/application/auth/SessionServiceTest.java b/src/test/java/com/tnt/application/auth/SessionServiceTest.java new file mode 100644 index 00000000..b8d5c9e1 --- /dev/null +++ b/src/test/java/com/tnt/application/auth/SessionServiceTest.java @@ -0,0 +1,102 @@ +package com.tnt.application.auth; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.BDDMockito.*; + +import java.time.LocalDateTime; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ValueOperations; + +import com.tnt.domain.auth.SessionInfo; +import com.tnt.global.error.exception.UnauthorizedException; + +import jakarta.servlet.http.HttpServletRequest; + +@ExtendWith(MockitoExtension.class) +class SessionServiceTest { + + @InjectMocks + private SessionService sessionService; + + @Mock + private RedisTemplate redisTemplate; + + @Mock + private ValueOperations valueOperations; + + @Mock + private HttpServletRequest request; + + @Test + @DisplayName("요청에 세션 쿠키가 없으면 예외 발생") + void request_does_not_have_session_cookie_error() { + // given + given(request.getCookies()).willReturn(null); + + // when + assertThatThrownBy(() -> sessionService.extractMemberSession(request)) + .isInstanceOf(UnauthorizedException.class) + .hasMessage("세션 쿠키가 존재하지 않습니다."); + } + + @Test + @DisplayName("세션 스토리지에 세션 존재하지 않으면 예외 발생") + void session_does_not_exist_in_storage_error() { + // given + String sessionId = "test-session-id"; + given(redisTemplate.hasKey(sessionId)).willReturn(false); + + // when & then + assertThatThrownBy(() -> sessionService.validateMemberSession(sessionId)) + .isInstanceOf(UnauthorizedException.class) + .hasMessage("세션 스토리지에 세션이 존재하지 않습니다."); + } + + @Test + @DisplayName("세션이 만료되면 예외 발생") + void session_expires_error() { + // given + String sessionId = "test-session-id"; + SessionInfo sessionInfo = SessionInfo.builder() + .lastAccessTime(LocalDateTime.now().minusDays(3)) // 48시간 초과 + .build(); + + given(redisTemplate.hasKey(sessionId)).willReturn(true); + given(redisTemplate.opsForValue()).willReturn(valueOperations); + given(valueOperations.get(sessionId)).willReturn(sessionInfo); + + // when & then + assertThatThrownBy(() -> sessionService.validateMemberSession(sessionId)) + .isInstanceOf(UnauthorizedException.class) + .hasMessage("세션이 만료되었습니다."); + + verify(redisTemplate).delete(sessionId); + } + + @Test + @DisplayName("세션 생성 성공") + void create_session_success() { + // given + given(redisTemplate.opsForValue()).willReturn(valueOperations); + String memberId = "12345"; + + // when + sessionService.createSession(memberId, request); + + // then + verify(valueOperations).set( + eq(memberId), + any(SessionInfo.class), + eq(2L * 24 * 60 * 60), + eq(TimeUnit.SECONDS) + ); + } +} From aace3c2eac4a8624d90e6cae397bc4e073ff1690 Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Mon, 6 Jan 2025 02:09:18 +0900 Subject: [PATCH 30/75] =?UTF-8?q?[TNT-28]=20feat:=20MemberTest=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/tnt/domain/member/MemberTest.java | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 src/test/java/com/tnt/domain/member/MemberTest.java diff --git a/src/test/java/com/tnt/domain/member/MemberTest.java b/src/test/java/com/tnt/domain/member/MemberTest.java new file mode 100644 index 00000000..9f28d4e9 --- /dev/null +++ b/src/test/java/com/tnt/domain/member/MemberTest.java @@ -0,0 +1,89 @@ +package com.tnt.domain.member; + +import static org.assertj.core.api.Assertions.*; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.IntStream; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +import io.hypersistence.tsid.TSID; + +@ExtendWith(MockitoExtension.class) +class MemberTest { + + @Nested + @DisplayName("회원 생성") + class CreateMember { + + @Test + @DisplayName("회원 생성 성공") + void create_member_success() { + // when + Member member = Member.builder() + .id(TSID.fast().toLong()) // TSID 직접 생성 + .socialId("12345") + .email("test@example.com") + .name("홍길동") + .age("20") + .socialType(SocialType.KAKAO) + .build(); + + // then + assertThat(member.getId()).isNotNull(); + assertThat(String.valueOf(member.getId())).hasSize(18); + } + + @Test + @DisplayName("tsid 중복 검증 성공") + void verify_tsid_duplication_success() { + // when + List ids = IntStream.range(0, 100) + .mapToObj(i -> Member.builder() + .id(TSID.fast().toLong()) + .socialId("social" + i) + .email("test" + i + "@example.com") + .name("사용자" + i) + .age(String.valueOf(20 + (i % 20))) + .socialType(SocialType.KAKAO) + .build()) + .map(Member::getId) + .toList(); + + // then + assertThat(ids).doesNotHaveDuplicates(); + + // ID가 시간순으로 증가하는지 검사 + List sortedIds = new ArrayList<>(ids); + Collections.sort(sortedIds); + assertThat(ids).isEqualTo(sortedIds); + } + + @Test + @DisplayName("tsid의 타임스탬프가 현재 시간과 일치하는지 검증 성공") + void verify_tsid_timestamp_success() { + // when + Member member = Member.builder() + .id(TSID.fast().toLong()) // TSID 직접 생성 + .socialId("12345") + .email("test@example.com") + .name("홍길동") + .age("20") + .socialType(SocialType.KAKAO) + .build(); + TSID tsid = TSID.from(member.getId()); + Instant timestamp = tsid.getInstant(); + + // then + assertThat(timestamp).isCloseTo(Instant.now(), within(1, ChronoUnit.SECONDS)); + } + } +} From f0ac237005fe171185d6135f98b5fd47b082afa1 Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Mon, 6 Jan 2025 02:09:34 +0900 Subject: [PATCH 31/75] =?UTF-8?q?[TNT-28]=20feat:=20MemberIntegrationTest?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/member/MemberIntegrationTest.java | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 src/test/java/com/tnt/domain/member/MemberIntegrationTest.java diff --git a/src/test/java/com/tnt/domain/member/MemberIntegrationTest.java b/src/test/java/com/tnt/domain/member/MemberIntegrationTest.java new file mode 100644 index 00000000..578657c1 --- /dev/null +++ b/src/test/java/com/tnt/domain/member/MemberIntegrationTest.java @@ -0,0 +1,46 @@ +package com.tnt.domain.member; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; + +import com.tnt.domain.member.repository.MemberRepository; + +@SpringBootTest +@Transactional // 롤백 비활성화 +class MemberIntegrationTest { + + @Autowired + private MemberRepository memberRepository; + + @Test + @DisplayName("회원 DB에 저장 시 tsid 자동 생성 성공") + void save_member_to_db_success() { + // given + Member member = Member.builder() + .socialId("12345") + .email("test@example.com") + .name("홍길동") + .age("20") + .socialType(SocialType.KAKAO) + .build(); + + // when + Member savedMember = memberRepository.save(member); + memberRepository.flush(); + + // then + Member foundMember = memberRepository.findById(savedMember.getId()) + .orElseThrow(); + + assertThat(foundMember.getId()).isNotNull(); + assertThat(String.valueOf(foundMember.getId())).hasSize(18); + assertThat(foundMember.getId()).isEqualTo(savedMember.getId()); + assertThat(foundMember.getSocialId()).isEqualTo("12345"); + assertThat(foundMember.getEmail()).isEqualTo("test@example.com"); + } +} From 078662590e49e481b9231d7ce7ba6de5e4b33400 Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Mon, 6 Jan 2025 02:14:10 +0900 Subject: [PATCH 32/75] =?UTF-8?q?[TNT-28]=20refactor:=20memberRepository?= =?UTF-8?q?=20=EC=B0=B8=EC=A1=B0=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/tnt/global/auth/SessionAuthenticationFilter.java | 2 -- src/main/java/com/tnt/global/config/SecurityConfig.java | 4 +--- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/main/java/com/tnt/global/auth/SessionAuthenticationFilter.java b/src/main/java/com/tnt/global/auth/SessionAuthenticationFilter.java index d1c24b0e..47b32ac0 100644 --- a/src/main/java/com/tnt/global/auth/SessionAuthenticationFilter.java +++ b/src/main/java/com/tnt/global/auth/SessionAuthenticationFilter.java @@ -17,7 +17,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.tnt.application.auth.SessionService; -import com.tnt.domain.member.repository.MemberRepository; import com.tnt.global.error.exception.UnauthorizedException; import jakarta.servlet.FilterChain; @@ -35,7 +34,6 @@ public class SessionAuthenticationFilter extends OncePerRequestFilter { private final AntPathMatcher pathMatcher = new AntPathMatcher(); private final List allowedUris; private final SessionService sessionService; - private final MemberRepository memberRepository; @Override protected void doFilterInternal( diff --git a/src/main/java/com/tnt/global/config/SecurityConfig.java b/src/main/java/com/tnt/global/config/SecurityConfig.java index 0592c048..5eb8a705 100644 --- a/src/main/java/com/tnt/global/config/SecurityConfig.java +++ b/src/main/java/com/tnt/global/config/SecurityConfig.java @@ -15,7 +15,6 @@ import org.springframework.security.web.authentication.logout.LogoutFilter; import com.tnt.application.auth.SessionService; -import com.tnt.domain.member.repository.MemberRepository; import com.tnt.global.auth.SessionAuthenticationFilter; import lombok.RequiredArgsConstructor; @@ -36,7 +35,6 @@ public class SecurityConfig { "/api/oauth2/**" }; private final SessionService sessionService; - private final MemberRepository memberRepository; @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { @@ -60,6 +58,6 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { @Bean public SessionAuthenticationFilter sessionAuthenticationFilter() { - return new SessionAuthenticationFilter(Arrays.asList(ALLOWED_URIS), sessionService, memberRepository); + return new SessionAuthenticationFilter(Arrays.asList(ALLOWED_URIS), sessionService); } } From 11e8c6630dd33862122dc76ffe7bd2c74550e338 Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Mon, 6 Jan 2025 08:31:39 +0900 Subject: [PATCH 33/75] =?UTF-8?q?[TNT-28]=20refactor:=20=EA=B3=B5=EB=B0=B1?= =?UTF-8?q?=20=EC=A0=9C=EA=B1=B0,=20=EC=9D=91=EB=8B=B5=20=EA=B5=AC?= =?UTF-8?q?=EC=A1=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/SessionAuthenticationFilter.java | 23 +++---------------- 1 file changed, 3 insertions(+), 20 deletions(-) diff --git a/src/main/java/com/tnt/global/auth/SessionAuthenticationFilter.java b/src/main/java/com/tnt/global/auth/SessionAuthenticationFilter.java index 47b32ac0..ae91b21e 100644 --- a/src/main/java/com/tnt/global/auth/SessionAuthenticationFilter.java +++ b/src/main/java/com/tnt/global/auth/SessionAuthenticationFilter.java @@ -1,9 +1,7 @@ package com.tnt.global.auth; import java.io.IOException; -import java.time.LocalDateTime; import java.util.List; -import java.util.Map; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; @@ -15,7 +13,6 @@ import org.springframework.util.AntPathMatcher; import org.springframework.web.filter.OncePerRequestFilter; -import com.fasterxml.jackson.databind.ObjectMapper; import com.tnt.application.auth.SessionService; import com.tnt.global.error.exception.UnauthorizedException; @@ -79,16 +76,9 @@ private void checkSessionAndAuthentication( HttpServletResponse response, FilterChain filterChain ) throws ServletException, IOException { - log.info("세션 검증 시작"); - - String sessionId = sessionService.extractMemberSession(request) - .orElseThrow(() -> new UnauthorizedException("세션이 존재하지 않습니다.")); - + String sessionId = sessionService.extractMemberSession(request); sessionService.validateMemberSession(sessionId); - - Long memberId = Long.parseLong(sessionId); - - saveAuthentication(memberId); + saveAuthentication(Long.parseLong(sessionId)); filterChain.doFilter(request, response); } @@ -100,14 +90,7 @@ private void handleUnauthorizedException( response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setContentType("application/json;charset=UTF-8"); - Map errorResponse = Map.of( - "message", exception.getMessage(), - "status", HttpServletResponse.SC_UNAUTHORIZED, - "timestamp", LocalDateTime.now().toString() - ); - - new ObjectMapper().writeValueAsString(errorResponse); - response.getWriter().write(new ObjectMapper().writeValueAsString(errorResponse)); + response.getWriter().write(exception.getMessage()); } private void saveAuthentication(Long memberId) { From 531186d042d40e64862fcbcc0b84eb28491b18f3 Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Mon, 6 Jan 2025 08:32:01 +0900 Subject: [PATCH 34/75] =?UTF-8?q?[TNT-28]=20refactor:=20Optional=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tnt/application/auth/SessionService.java | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/tnt/application/auth/SessionService.java b/src/main/java/com/tnt/application/auth/SessionService.java index 5c553f9f..219088e4 100644 --- a/src/main/java/com/tnt/application/auth/SessionService.java +++ b/src/main/java/com/tnt/application/auth/SessionService.java @@ -2,7 +2,6 @@ import java.time.LocalDateTime; import java.util.Arrays; -import java.util.Optional; import java.util.concurrent.TimeUnit; import org.springframework.data.redis.core.RedisTemplate; @@ -25,7 +24,7 @@ public class SessionService { private static final String SESSION_COOKIE_NAME = "SESSION"; private final RedisTemplate redisTemplate; - public Optional extractMemberSession(HttpServletRequest request) { + public String extractMemberSession(HttpServletRequest request) { Cookie[] cookies = request.getCookies(); if (cookies == null) { log.info("쿠키가 존재하지 않습니다."); @@ -34,15 +33,13 @@ public Optional extractMemberSession(HttpServletRequest request) { return Arrays.stream(cookies) .filter(cookie -> SESSION_COOKIE_NAME.equals(cookie.getName())) - .map(Cookie::getValue) .findFirst() - .or(() -> { - throw new UnauthorizedException("세션 쿠키가 존재하지 않습니다."); - }); + .map(Cookie::getValue) + .orElseThrow(() -> new UnauthorizedException("세션 쿠키가 존재하지 않습니다.")); } public void validateMemberSession(String sessionId) { - // 1. 세션 존재 여부 확인 + // 세션 존재 여부 확인 if (Boolean.FALSE.equals(redisTemplate.hasKey(sessionId))) { log.info("세션이 존재하지 않음 - SessionId: {}", sessionId); throw new UnauthorizedException("세션 스토리지에 세션이 존재하지 않습니다."); @@ -50,15 +47,15 @@ public void validateMemberSession(String sessionId) { SessionInfo sessionInfo = redisTemplate.opsForValue().get(sessionId); - // 2. 세션 유효성 확인 + // 세션 유효성 확인 LocalDateTime lastAccessTime = sessionInfo.getLastAccessTime(); - if (lastAccessTime.isBefore(LocalDateTime.now().minusDays(2))) { // 48시간 지났는지 체크 + if (lastAccessTime.isBefore(LocalDateTime.now().minusDays(2))) { log.info("세션이 만료됨 - SessionId: {}, LastAccessTime: {}", sessionId, lastAccessTime); - redisTemplate.delete(sessionId); // 만료된 세션 삭제 + redisTemplate.delete(sessionId); throw new UnauthorizedException("세션이 만료되었습니다."); } - // 3. 세션 갱신 (마지막 접근 시간 업데이트) + // 세션 갱신 sessionInfo = SessionInfo.builder() .lastAccessTime(LocalDateTime.now()) .userAgent(sessionInfo.getUserAgent()) From e16edc5c53adef5015c67f9a0b153d7686b2963e Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Mon, 6 Jan 2025 08:32:31 +0900 Subject: [PATCH 35/75] =?UTF-8?q?[TNT-28]=20feat:=20=EC=84=B8=EC=85=98=20?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=20=ED=95=84=ED=84=B0=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/SessionAuthenticationFilterTest.java | 144 ++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 src/test/java/com/tnt/global/auth/SessionAuthenticationFilterTest.java diff --git a/src/test/java/com/tnt/global/auth/SessionAuthenticationFilterTest.java b/src/test/java/com/tnt/global/auth/SessionAuthenticationFilterTest.java new file mode 100644 index 00000000..2c4a1c43 --- /dev/null +++ b/src/test/java/com/tnt/global/auth/SessionAuthenticationFilterTest.java @@ -0,0 +1,144 @@ +package com.tnt.global.auth; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.BDDMockito.*; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +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.security.core.context.SecurityContextHolder; + +import com.tnt.application.auth.SessionService; +import com.tnt.global.error.exception.UnauthorizedException; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +@ExtendWith(MockitoExtension.class) +class SessionAuthenticationFilterTest { + + @Mock + private SessionService sessionService; + + @Mock + private HttpServletRequest request; + + @Mock + private HttpServletResponse response; + + @Mock + private FilterChain filterChain; + + private SessionAuthenticationFilter sessionAuthenticationFilter; + + @BeforeEach + void setUp() { + List allowedUris = Arrays.asList("/api/auth/**", "/api/health"); + sessionAuthenticationFilter = new SessionAuthenticationFilter( + allowedUris, + sessionService + ); + } + + @Test + @DisplayName("허용된 URI 세션 검증 스킵 성공") + void allowed_uri_skip_session_verification_success() throws ServletException, IOException { + // given + given(request.getRequestURI()).willReturn("/api/auth/login"); + given(request.getQueryString()).willReturn(null); + + // when + sessionAuthenticationFilter.doFilterInternal(request, response, filterChain); + + // then + verify(sessionService, never()).extractMemberSession(any()); + verify(filterChain).doFilter(request, response); + } + + @Test + @DisplayName("요청에 세션이 없는 경우 예외 발생") + void do_not_exist_session_in_request_error() throws ServletException, IOException { + // given + given(request.getRequestURI()).willReturn("/api/members/me"); + given(sessionService.extractMemberSession(request)) + .willThrow(new UnauthorizedException("세션 쿠키가 존재하지 않습니다.")); + + StringWriter stringWriter = new StringWriter(); + given(response.getWriter()).willReturn(new PrintWriter(stringWriter)); + + // when + sessionAuthenticationFilter.doFilterInternal(request, response, filterChain); + + // then + verify(response).setStatus(HttpServletResponse.SC_UNAUTHORIZED); + verify(response).setContentType("application/json;charset=UTF-8"); + verify(filterChain, never()).doFilter(request, response); + assertThat(stringWriter.toString()).contains("세션 쿠키가 존재하지 않습니다."); + } + + @Test + @DisplayName("세션이 스토리지에 존재하지 않는 경우 예외 발생") + void do_not_exist_session_in_storage_error() throws ServletException, IOException { + // given + String sessionId = "12345"; + given(request.getRequestURI()).willReturn("/api/members/me"); + given(sessionService.extractMemberSession(request)).willReturn(sessionId); + willThrow(new UnauthorizedException("세션 스토리지에 세션이 존재하지 않습니다.")) + .given(sessionService) + .validateMemberSession(sessionId); + + StringWriter stringWriter = new StringWriter(); + given(response.getWriter()).willReturn(new PrintWriter(stringWriter)); + + // when + sessionAuthenticationFilter.doFilterInternal(request, response, filterChain); + + // then + verify(response).setStatus(HttpServletResponse.SC_UNAUTHORIZED); + verify(response).setContentType("application/json;charset=UTF-8"); + verify(filterChain, never()).doFilter(request, response); + assertThat(stringWriter.toString()).contains("세션 스토리지에 세션이 존재하지 않습니다."); + } + + @Test + @DisplayName("유효한 세션이 아닐 경우 예외 발생") + void do_not_validate_session_error() throws ServletException, IOException { + // given + String sessionId = "12345"; + given(request.getRequestURI()).willReturn("/api/members/me"); + given(sessionService.extractMemberSession(request)).willReturn(sessionId); + willThrow(new UnauthorizedException("세션이 만료되었습니다.")) + .given(sessionService) + .validateMemberSession(sessionId); + + StringWriter stringWriter = new StringWriter(); + given(response.getWriter()).willReturn(new PrintWriter(stringWriter)); + + // when + sessionAuthenticationFilter.doFilterInternal(request, response, filterChain); + + // then + verify(response).setStatus(HttpServletResponse.SC_UNAUTHORIZED); + verify(response).setContentType("application/json;charset=UTF-8"); + verify(filterChain, never()).doFilter(request, response); + assertThat(stringWriter.toString()).contains("세션이 만료되었습니다."); + } + + @AfterEach + void tearDown() { + SecurityContextHolder.clearContext(); + } +} From 7266c0317262fdb826ca2d05ba4840037a53879c Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Mon, 6 Jan 2025 08:55:17 +0900 Subject: [PATCH 36/75] =?UTF-8?q?[TNT-28]=20refactor:=20=EC=BF=A0=ED=82=A4?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=ED=97=A4=EB=8D=94=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=EC=9C=BC=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 --- .../tnt/application/auth/SessionService.java | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/tnt/application/auth/SessionService.java b/src/main/java/com/tnt/application/auth/SessionService.java index 219088e4..9cfcf3cb 100644 --- a/src/main/java/com/tnt/application/auth/SessionService.java +++ b/src/main/java/com/tnt/application/auth/SessionService.java @@ -1,7 +1,6 @@ package com.tnt.application.auth; import java.time.LocalDateTime; -import java.util.Arrays; import java.util.concurrent.TimeUnit; import org.springframework.data.redis.core.RedisTemplate; @@ -10,7 +9,6 @@ import com.tnt.domain.auth.SessionInfo; import com.tnt.global.error.exception.UnauthorizedException; -import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -21,21 +19,18 @@ public class SessionService { static final long SESSION_DURATION = 2L * 24 * 60 * 60; // 48시간 - private static final String SESSION_COOKIE_NAME = "SESSION"; + private static final String AUTHORIZATION_HEADER = "Authorization"; + private static final String BEARER_PREFIX = "Bearer "; private final RedisTemplate redisTemplate; public String extractMemberSession(HttpServletRequest request) { - Cookie[] cookies = request.getCookies(); - if (cookies == null) { - log.info("쿠키가 존재하지 않습니다."); - throw new UnauthorizedException("세션 쿠키가 존재하지 않습니다."); + String authHeader = request.getHeader(AUTHORIZATION_HEADER); + if (authHeader == null || !authHeader.startsWith(BEARER_PREFIX)) { + log.info("Authorization 헤더가 존재하지 않거나 올바르지 않은 형식입니다."); + throw new UnauthorizedException("인증 세션이 존재하지 않습니다."); } - return Arrays.stream(cookies) - .filter(cookie -> SESSION_COOKIE_NAME.equals(cookie.getName())) - .findFirst() - .map(Cookie::getValue) - .orElseThrow(() -> new UnauthorizedException("세션 쿠키가 존재하지 않습니다.")); + return authHeader.substring(BEARER_PREFIX.length()); } public void validateMemberSession(String sessionId) { From d83e7f812ccfca69ae86b633f48e7df545a2b801 Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Mon, 6 Jan 2025 08:56:13 +0900 Subject: [PATCH 37/75] =?UTF-8?q?[TNT-28]=20refactor:=20=EC=BF=A0=ED=82=A4?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=ED=97=A4=EB=8D=94=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=EC=9C=BC=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 --- .../application/auth/SessionServiceTest.java | 77 +++++++++++++++++-- .../auth/SessionAuthenticationFilterTest.java | 30 ++++++-- 2 files changed, 96 insertions(+), 11 deletions(-) diff --git a/src/test/java/com/tnt/application/auth/SessionServiceTest.java b/src/test/java/com/tnt/application/auth/SessionServiceTest.java index b8d5c9e1..a4bb382e 100644 --- a/src/test/java/com/tnt/application/auth/SessionServiceTest.java +++ b/src/test/java/com/tnt/application/auth/SessionServiceTest.java @@ -1,5 +1,6 @@ package com.tnt.application.auth; +import static com.tnt.application.auth.SessionService.*; import static org.assertj.core.api.Assertions.*; import static org.mockito.BDDMockito.*; @@ -36,15 +37,41 @@ class SessionServiceTest { private HttpServletRequest request; @Test - @DisplayName("요청에 세션 쿠키가 없으면 예외 발생") - void request_does_not_have_session_cookie_error() { + @DisplayName("요청 헤더에 세션이 없으면 예외 발생") + void no_authorization_header_error() { // given - given(request.getCookies()).willReturn(null); + given(request.getHeader("Authorization")).willReturn(null); - // when + // when & then + assertThatThrownBy(() -> sessionService.extractMemberSession(request)) + .isInstanceOf(UnauthorizedException.class) + .hasMessage("인증 세션이 존재하지 않습니다."); + } + + @Test + @DisplayName("Authorization 헤더가 Bearer로 시작하지 않으면 예외 발생") + void invalid_authorization_header_format_error() { + // given + given(request.getHeader("Authorization")).willReturn("Invalid 12345"); + + // when & then assertThatThrownBy(() -> sessionService.extractMemberSession(request)) .isInstanceOf(UnauthorizedException.class) - .hasMessage("세션 쿠키가 존재하지 않습니다."); + .hasMessage("인증 세션이 존재하지 않습니다."); + } + + @Test + @DisplayName("Authorization 헤더에서 세션 ID 추출 성공") + void extract_session_id_success() { + // given + String sessionId = "test-session-id"; + given(request.getHeader("Authorization")).willReturn("Bearer " + sessionId); + + // when + String extractedSessionId = sessionService.extractMemberSession(request); + + // then + assertThat(extractedSessionId).isEqualTo(sessionId); } @Test @@ -81,6 +108,33 @@ void session_expires_error() { verify(redisTemplate).delete(sessionId); } + @Test + @DisplayName("세션 유효성 검증 및 갱신 성공") + void validate_and_refresh_session_success() { + // given + String sessionId = "test-session-id"; + SessionInfo sessionInfo = SessionInfo.builder() + .lastAccessTime(LocalDateTime.now().minusHours(1)) + .userAgent("Mozilla") + .clientIp("127.0.0.1") + .build(); + + given(redisTemplate.hasKey(sessionId)).willReturn(true); + given(redisTemplate.opsForValue()).willReturn(valueOperations); + given(valueOperations.get(sessionId)).willReturn(sessionInfo); + + // when + sessionService.validateMemberSession(sessionId); + + // then + verify(valueOperations).set( + eq(sessionId), + any(SessionInfo.class), + eq(SESSION_DURATION), + eq(TimeUnit.SECONDS) + ); + } + @Test @DisplayName("세션 생성 성공") void create_session_success() { @@ -99,4 +153,17 @@ void create_session_success() { eq(TimeUnit.SECONDS) ); } + + @Test + @DisplayName("세션 삭제 성공") + void remove_session_success() { + // given + String sessionId = "12345"; + + // when + sessionService.removeSession(sessionId); + + // then + verify(redisTemplate).delete(sessionId); + } } diff --git a/src/test/java/com/tnt/global/auth/SessionAuthenticationFilterTest.java b/src/test/java/com/tnt/global/auth/SessionAuthenticationFilterTest.java index 2c4a1c43..8e5f3a47 100644 --- a/src/test/java/com/tnt/global/auth/SessionAuthenticationFilterTest.java +++ b/src/test/java/com/tnt/global/auth/SessionAuthenticationFilterTest.java @@ -69,12 +69,12 @@ void allowed_uri_skip_session_verification_success() throws ServletException, IO } @Test - @DisplayName("요청에 세션이 없는 경우 예외 발생") - void do_not_exist_session_in_request_error() throws ServletException, IOException { + @DisplayName("Authorization 헤더가 없는 경우 예외 발생") + void missing_authorization_header_error() throws ServletException, IOException { // given given(request.getRequestURI()).willReturn("/api/members/me"); given(sessionService.extractMemberSession(request)) - .willThrow(new UnauthorizedException("세션 쿠키가 존재하지 않습니다.")); + .willThrow(new UnauthorizedException("인증 세션이 존재하지 않습니다.")); StringWriter stringWriter = new StringWriter(); given(response.getWriter()).willReturn(new PrintWriter(stringWriter)); @@ -86,12 +86,12 @@ void do_not_exist_session_in_request_error() throws ServletException, IOExceptio verify(response).setStatus(HttpServletResponse.SC_UNAUTHORIZED); verify(response).setContentType("application/json;charset=UTF-8"); verify(filterChain, never()).doFilter(request, response); - assertThat(stringWriter.toString()).contains("세션 쿠키가 존재하지 않습니다."); + assertThat(stringWriter.toString()).contains("인증 세션이 존재하지 않습니다."); } @Test @DisplayName("세션이 스토리지에 존재하지 않는 경우 예외 발생") - void do_not_exist_session_in_storage_error() throws ServletException, IOException { + void session_not_exist_in_storage_error() throws ServletException, IOException { // given String sessionId = "12345"; given(request.getRequestURI()).willReturn("/api/members/me"); @@ -115,7 +115,7 @@ void do_not_exist_session_in_storage_error() throws ServletException, IOExceptio @Test @DisplayName("유효한 세션이 아닐 경우 예외 발생") - void do_not_validate_session_error() throws ServletException, IOException { + void expired_session_error() throws ServletException, IOException { // given String sessionId = "12345"; given(request.getRequestURI()).willReturn("/api/members/me"); @@ -137,6 +137,24 @@ void do_not_validate_session_error() throws ServletException, IOException { assertThat(stringWriter.toString()).contains("세션이 만료되었습니다."); } + @Test + @DisplayName("유효한 세션으로 인증 성공") + void authenticate_with_valid_session_success() throws ServletException, IOException { + // given + String sessionId = "12345"; + given(request.getRequestURI()).willReturn("/api/members/me"); + given(sessionService.extractMemberSession(request)).willReturn(sessionId); + willDoNothing().given(sessionService).validateMemberSession(sessionId); + + // when + sessionAuthenticationFilter.doFilterInternal(request, response, filterChain); + + // then + verify(filterChain).doFilter(request, response); + verify(sessionService).extractMemberSession(request); + verify(sessionService).validateMemberSession(sessionId); + } + @AfterEach void tearDown() { SecurityContextHolder.clearContext(); From aa9bca448882d0691aad1cf917a9ba0127394b97 Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Mon, 6 Jan 2025 09:00:45 +0900 Subject: [PATCH 38/75] =?UTF-8?q?[TNT-28]=20refactor:=20=EB=B6=88=ED=95=84?= =?UTF-8?q?=EC=9A=94=ED=95=9C=20=EC=A3=BC=EC=84=9D=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/tnt/application/auth/SessionService.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/com/tnt/application/auth/SessionService.java b/src/main/java/com/tnt/application/auth/SessionService.java index 9cfcf3cb..98af0007 100644 --- a/src/main/java/com/tnt/application/auth/SessionService.java +++ b/src/main/java/com/tnt/application/auth/SessionService.java @@ -61,7 +61,6 @@ public void validateMemberSession(String sessionId) { log.info("세션 유효성 검증 완료 및 갱신 - SessionId: {}", sessionId); } - // 로그인 시 세션 생성을 위한 메서드 public void createSession(String memberId, HttpServletRequest request) { SessionInfo sessionInfo = SessionInfo.builder() .lastAccessTime(LocalDateTime.now()) @@ -77,7 +76,6 @@ public void createSession(String memberId, HttpServletRequest request) { ); } - // 로그아웃 시 세션 삭제 public void removeSession(String sessionId) { redisTemplate.delete(sessionId); } From 4bba1c134a97d6692688f4c00a9221e017a72501 Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Mon, 6 Jan 2025 14:04:34 +0900 Subject: [PATCH 39/75] =?UTF-8?q?[TNT-28]=20chore:=20mysql=20db=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2eb0d9de..5d0d368a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,7 +47,7 @@ jobs: - name: MySQL Docker container for tests run: | - sudo docker run -d -p 3306:3306 --env MYSQL_DATABASE={} --env MYSQL_ROOT_PASSWORD=root mysql:latest + sudo docker run -d -p 3306:3306 --env MYSQL_DATABASE=tnt_dev --env MYSQL_ROOT_PASSWORD=root mysql:latest - name: Build env: From c4b227cf9687defbad80eca1c2347f261dea492f Mon Sep 17 00:00:00 2001 From: Manje Cho Date: Mon, 6 Jan 2025 14:08:26 +0900 Subject: [PATCH 40/75] =?UTF-8?q?[TNT-28]=20chore:=20mysql=20db=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20app2=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 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5d0d368a..4889ca73 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,7 +47,7 @@ jobs: - name: MySQL Docker container for tests run: | - sudo docker run -d -p 3306:3306 --env MYSQL_DATABASE=tnt_dev --env MYSQL_ROOT_PASSWORD=root mysql:latest + sudo docker run -d -p 3306:3306 --env MYSQL_DATABASE=app2 --env MYSQL_ROOT_PASSWORD=root mysql:latest - name: Build env: From 1a170ed993411505aebd2c8ed908faead767622a Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Mon, 6 Jan 2025 14:35:02 +0900 Subject: [PATCH 41/75] =?UTF-8?q?[TNT-28]=20chore:=20=EB=B9=8C=EB=93=9C=20?= =?UTF-8?q?=EC=8B=9C=20profile=20dev=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4889ca73..71d144c3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,10 +47,11 @@ jobs: - name: MySQL Docker container for tests run: | - sudo docker run -d -p 3306:3306 --env MYSQL_DATABASE=app2 --env MYSQL_ROOT_PASSWORD=root mysql:latest + sudo docker run -d -p 3306:3306 --env MYSQL_DATABASE=tnt_dev --env MYSQL_ROOT_PASSWORD=root mysql:latest - name: Build env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + SPRING_PROFILES_ACTIVE: dev run: ./gradlew build sonar --info --stacktrace From be6a35387c23689e86feb8e4fcadffc26545a87a Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Mon, 6 Jan 2025 14:42:25 +0900 Subject: [PATCH 42/75] =?UTF-8?q?[TNT-28]=20chore:=20=EB=B9=8C=EB=93=9C=20?= =?UTF-8?q?=EC=8B=9C=20profile=20dev=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 71d144c3..031e892a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,5 +53,4 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - SPRING_PROFILES_ACTIVE: dev - run: ./gradlew build sonar --info --stacktrace + run: ./gradlew build sonar -Dspring.profiles.active=dev --info --stacktrace From b517e658acdb39dd21a508ae13b5fefae5855680 Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Mon, 6 Jan 2025 14:48:34 +0900 Subject: [PATCH 43/75] =?UTF-8?q?[TNT-28]=20chore:=20=EB=B9=8C=EB=93=9C=20?= =?UTF-8?q?=EC=8B=9C=20profile=20dev=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 031e892a..5d0d368a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,4 +53,4 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - run: ./gradlew build sonar -Dspring.profiles.active=dev --info --stacktrace + run: ./gradlew build sonar --info --stacktrace From 7a7dd5d57048fcaf8db04e5c4b60202ce3aaaa0a Mon Sep 17 00:00:00 2001 From: Manje Cho Date: Mon, 6 Jan 2025 14:55:01 +0900 Subject: [PATCH 44/75] =?UTF-8?q?[TNT-28]=20chore:=20mysql=20db=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20tnt=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 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5d0d368a..0e0d1499 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,7 +47,7 @@ jobs: - name: MySQL Docker container for tests run: | - sudo docker run -d -p 3306:3306 --env MYSQL_DATABASE=tnt_dev --env MYSQL_ROOT_PASSWORD=root mysql:latest + sudo docker run -d -p 3306:3306 --env MYSQL_DATABASE=tnt --env MYSQL_ROOT_PASSWORD=root mysql:latest - name: Build env: From 91c6d1241cd0c5738bae2136eb1d9cbb9f7b5167 Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Mon, 6 Jan 2025 15:01:00 +0900 Subject: [PATCH 45/75] Update submodule to latest version --- src/main/resources/config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/config b/src/main/resources/config index fe70b352..29c658c0 160000 --- a/src/main/resources/config +++ b/src/main/resources/config @@ -1 +1 @@ -Subproject commit fe70b35293d629b1a6c1c40ca181d093944d19b6 +Subproject commit 29c658c0b17ff45638b1397c640c33a8cdb68f6b From 71ce9965d093474ca475736f8efdb8c53fceff7f Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Mon, 6 Jan 2025 15:09:26 +0900 Subject: [PATCH 46/75] =?UTF-8?q?[TNT-28]=20chore:=20mysql=20db=20tnt=5Fde?= =?UTF-8?q?v=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 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0e0d1499..5d0d368a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,7 +47,7 @@ jobs: - name: MySQL Docker container for tests run: | - sudo docker run -d -p 3306:3306 --env MYSQL_DATABASE=tnt --env MYSQL_ROOT_PASSWORD=root mysql:latest + sudo docker run -d -p 3306:3306 --env MYSQL_DATABASE=tnt_dev --env MYSQL_ROOT_PASSWORD=root mysql:latest - name: Build env: From f80c5bd2bcca4ae44ce8bb9824e6b2a50799d237 Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Mon, 6 Jan 2025 15:09:50 +0900 Subject: [PATCH 47/75] Update submodule to latest version --- src/main/resources/config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/config b/src/main/resources/config index 29c658c0..4d295461 160000 --- a/src/main/resources/config +++ b/src/main/resources/config @@ -1 +1 @@ -Subproject commit 29c658c0b17ff45638b1397c640c33a8cdb68f6b +Subproject commit 4d295461beffa736bf5835aa18bd41743a372eaa From 6b2b7a068998434648996dccfafdb4c13a06a98d Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Tue, 7 Jan 2025 10:06:06 +0900 Subject: [PATCH 48/75] =?UTF-8?q?[TNT-28]=20refactor:=20=EC=9C=A0=ED=9A=A8?= =?UTF-8?q?=EC=84=B1=20=EA=B2=80=EC=82=AC=20=EB=A1=9C=EC=A7=81=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 --- .../java/com/tnt/application/auth/SessionService.java | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/main/java/com/tnt/application/auth/SessionService.java b/src/main/java/com/tnt/application/auth/SessionService.java index 98af0007..7b6d599b 100644 --- a/src/main/java/com/tnt/application/auth/SessionService.java +++ b/src/main/java/com/tnt/application/auth/SessionService.java @@ -42,14 +42,6 @@ public void validateMemberSession(String sessionId) { SessionInfo sessionInfo = redisTemplate.opsForValue().get(sessionId); - // 세션 유효성 확인 - LocalDateTime lastAccessTime = sessionInfo.getLastAccessTime(); - if (lastAccessTime.isBefore(LocalDateTime.now().minusDays(2))) { - log.info("세션이 만료됨 - SessionId: {}, LastAccessTime: {}", sessionId, lastAccessTime); - redisTemplate.delete(sessionId); - throw new UnauthorizedException("세션이 만료되었습니다."); - } - // 세션 갱신 sessionInfo = SessionInfo.builder() .lastAccessTime(LocalDateTime.now()) From 5a33c7e302e491958249fb9040125822b92e026f Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Tue, 7 Jan 2025 10:07:03 +0900 Subject: [PATCH 49/75] =?UTF-8?q?[TNT-28]=20refactor:=20prefix=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/tnt/application/auth/SessionService.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/tnt/application/auth/SessionService.java b/src/main/java/com/tnt/application/auth/SessionService.java index 7b6d599b..6897f7b6 100644 --- a/src/main/java/com/tnt/application/auth/SessionService.java +++ b/src/main/java/com/tnt/application/auth/SessionService.java @@ -20,17 +20,17 @@ public class SessionService { static final long SESSION_DURATION = 2L * 24 * 60 * 60; // 48시간 private static final String AUTHORIZATION_HEADER = "Authorization"; - private static final String BEARER_PREFIX = "Bearer "; + private static final String SESSION_ID_PREFIX = "SESSION-ID "; private final RedisTemplate redisTemplate; public String extractMemberSession(HttpServletRequest request) { String authHeader = request.getHeader(AUTHORIZATION_HEADER); - if (authHeader == null || !authHeader.startsWith(BEARER_PREFIX)) { + if (authHeader == null || !authHeader.startsWith(SESSION_ID_PREFIX)) { log.info("Authorization 헤더가 존재하지 않거나 올바르지 않은 형식입니다."); throw new UnauthorizedException("인증 세션이 존재하지 않습니다."); } - return authHeader.substring(BEARER_PREFIX.length()); + return authHeader.substring(SESSION_ID_PREFIX.length()); } public void validateMemberSession(String sessionId) { From 6e159319c72f77ccb7e966dff6ff6620e8ea76c3 Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Tue, 7 Jan 2025 10:07:33 +0900 Subject: [PATCH 50/75] =?UTF-8?q?[TNT-28]=20refactor:=20isBlank=EB=A1=9C?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/tnt/application/auth/SessionService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/tnt/application/auth/SessionService.java b/src/main/java/com/tnt/application/auth/SessionService.java index 6897f7b6..e345c1df 100644 --- a/src/main/java/com/tnt/application/auth/SessionService.java +++ b/src/main/java/com/tnt/application/auth/SessionService.java @@ -25,7 +25,7 @@ public class SessionService { public String extractMemberSession(HttpServletRequest request) { String authHeader = request.getHeader(AUTHORIZATION_HEADER); - if (authHeader == null || !authHeader.startsWith(SESSION_ID_PREFIX)) { + if (authHeader.isBlank() || !authHeader.startsWith(SESSION_ID_PREFIX)) { log.info("Authorization 헤더가 존재하지 않거나 올바르지 않은 형식입니다."); throw new UnauthorizedException("인증 세션이 존재하지 않습니다."); } From a9efc0bacf0325b9f083dfcf61730fc91bd9f3b7 Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Tue, 7 Jan 2025 10:10:01 +0900 Subject: [PATCH 51/75] =?UTF-8?q?[TNT-28]=20refactor:=20throw=20=EB=B6=80?= =?UTF-8?q?=EB=B6=84=20=EB=A1=9C=EA=B7=B8=20error=EB=A1=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/tnt/application/auth/SessionService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/tnt/application/auth/SessionService.java b/src/main/java/com/tnt/application/auth/SessionService.java index e345c1df..5e4bd704 100644 --- a/src/main/java/com/tnt/application/auth/SessionService.java +++ b/src/main/java/com/tnt/application/auth/SessionService.java @@ -26,7 +26,7 @@ public class SessionService { public String extractMemberSession(HttpServletRequest request) { String authHeader = request.getHeader(AUTHORIZATION_HEADER); if (authHeader.isBlank() || !authHeader.startsWith(SESSION_ID_PREFIX)) { - log.info("Authorization 헤더가 존재하지 않거나 올바르지 않은 형식입니다."); + log.error("Authorization 헤더가 존재하지 않거나 올바르지 않은 형식입니다."); throw new UnauthorizedException("인증 세션이 존재하지 않습니다."); } @@ -36,7 +36,7 @@ public String extractMemberSession(HttpServletRequest request) { public void validateMemberSession(String sessionId) { // 세션 존재 여부 확인 if (Boolean.FALSE.equals(redisTemplate.hasKey(sessionId))) { - log.info("세션이 존재하지 않음 - SessionId: {}", sessionId); + log.error("세션이 존재하지 않음 - SessionId: {}", sessionId); throw new UnauthorizedException("세션 스토리지에 세션이 존재하지 않습니다."); } From ee987e0e98783d39c807e3e86f0307c95f1017f1 Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Tue, 7 Jan 2025 10:11:28 +0900 Subject: [PATCH 52/75] =?UTF-8?q?[TNT-28]=20refactor:=20=ED=8C=8C=EB=9D=BC?= =?UTF-8?q?=EB=AF=B8=ED=84=B0=20=EA=B0=84=EA=B2=A9=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/SessionAuthenticationFilter.java | 20 ++++++------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/tnt/global/auth/SessionAuthenticationFilter.java b/src/main/java/com/tnt/global/auth/SessionAuthenticationFilter.java index ae91b21e..ff6bc7f8 100644 --- a/src/main/java/com/tnt/global/auth/SessionAuthenticationFilter.java +++ b/src/main/java/com/tnt/global/auth/SessionAuthenticationFilter.java @@ -33,11 +33,8 @@ public class SessionAuthenticationFilter extends OncePerRequestFilter { private final SessionService sessionService; @Override - protected void doFilterInternal( - HttpServletRequest request, - HttpServletResponse response, - FilterChain filterChain - ) throws ServletException, IOException { + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { String requestUri = request.getRequestURI(); String queryString = request.getQueryString(); log.info("들어온 요청 - URI: {}, Query: {}, Method: {}", @@ -71,21 +68,16 @@ private boolean isAllowedUri(String requestUri) { return allowed; } - private void checkSessionAndAuthentication( - HttpServletRequest request, - HttpServletResponse response, - FilterChain filterChain - ) throws ServletException, IOException { + private void checkSessionAndAuthentication(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { String sessionId = sessionService.extractMemberSession(request); sessionService.validateMemberSession(sessionId); saveAuthentication(Long.parseLong(sessionId)); filterChain.doFilter(request, response); } - private void handleUnauthorizedException( - HttpServletResponse response, - UnauthorizedException exception - ) throws IOException { + private void handleUnauthorizedException(HttpServletResponse response, UnauthorizedException exception) throws + IOException { log.error("인증 실패: {}", exception.getMessage()); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setContentType("application/json;charset=UTF-8"); From 86e860555ba490cafc377cd0e54cf07efcd062f1 Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Tue, 7 Jan 2025 10:12:41 +0900 Subject: [PATCH 53/75] =?UTF-8?q?[TNT-28]=20refactor:=20=EC=95=88=20?= =?UTF-8?q?=EC=93=B0=EB=8A=94=20=EB=A9=94=EC=84=9C=EB=93=9C=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 --- .../com/tnt/domain/member/repository/MemberRepository.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/main/java/com/tnt/domain/member/repository/MemberRepository.java b/src/main/java/com/tnt/domain/member/repository/MemberRepository.java index 260ea131..448298a1 100644 --- a/src/main/java/com/tnt/domain/member/repository/MemberRepository.java +++ b/src/main/java/com/tnt/domain/member/repository/MemberRepository.java @@ -1,8 +1,5 @@ package com.tnt.domain.member.repository; -import java.time.LocalDateTime; -import java.util.Optional; - import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @@ -11,5 +8,4 @@ @Repository public interface MemberRepository extends JpaRepository { - Optional findByIdAndDeletedAt(Long id, LocalDateTime deletedAt); } From b8f65dca9bac74302b9b38f43781062723627601 Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Tue, 7 Jan 2025 10:21:19 +0900 Subject: [PATCH 54/75] =?UTF-8?q?[TNT-28]=20refactor:=20=EC=97=90=EB=9F=AC?= =?UTF-8?q?=20=ED=95=B8=EB=93=A4=EB=A7=81=20=EB=B0=A9=EC=8B=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/tnt/global/auth/SessionAuthenticationFilter.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/tnt/global/auth/SessionAuthenticationFilter.java b/src/main/java/com/tnt/global/auth/SessionAuthenticationFilter.java index ff6bc7f8..1ae3dd2d 100644 --- a/src/main/java/com/tnt/global/auth/SessionAuthenticationFilter.java +++ b/src/main/java/com/tnt/global/auth/SessionAuthenticationFilter.java @@ -50,8 +50,9 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse try { checkSessionAndAuthentication(request, response, filterChain); - } catch (UnauthorizedException e) { - handleUnauthorizedException(response, e); + } catch (RuntimeException e) { + log.error("인증 처리 중 에러 발생: ", e); + handleUnauthorizedException(response, new UnauthorizedException("인증 처리 중 에러가 발생했습니다.")); } } From 08dbcf8b5e32b316e6eb4238f501c993011b1c75 Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Tue, 7 Jan 2025 10:22:43 +0900 Subject: [PATCH 55/75] =?UTF-8?q?[TNT-28]=20refactor:=20=EC=97=90=EB=9F=AC?= =?UTF-8?q?=20=EB=A1=9C=EA=B7=B8=20=EC=B6=9C=EB=A0=A5=20=EB=B0=A9=EC=8B=9D?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/tnt/global/auth/SessionAuthenticationFilter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/tnt/global/auth/SessionAuthenticationFilter.java b/src/main/java/com/tnt/global/auth/SessionAuthenticationFilter.java index 1ae3dd2d..6070a9ef 100644 --- a/src/main/java/com/tnt/global/auth/SessionAuthenticationFilter.java +++ b/src/main/java/com/tnt/global/auth/SessionAuthenticationFilter.java @@ -79,7 +79,7 @@ private void checkSessionAndAuthentication(HttpServletRequest request, HttpServl private void handleUnauthorizedException(HttpServletResponse response, UnauthorizedException exception) throws IOException { - log.error("인증 실패: {}", exception.getMessage()); + log.error("인증 실패: ", exception); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setContentType("application/json;charset=UTF-8"); From 9c176e443c75a5975c9a5635ae1b78c53d0f33c1 Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Tue, 7 Jan 2025 10:38:15 +0900 Subject: [PATCH 56/75] =?UTF-8?q?[TNT-28]=20refactor:=20flush=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 --- src/test/java/com/tnt/domain/member/MemberIntegrationTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/java/com/tnt/domain/member/MemberIntegrationTest.java b/src/test/java/com/tnt/domain/member/MemberIntegrationTest.java index 578657c1..c7bfc125 100644 --- a/src/test/java/com/tnt/domain/member/MemberIntegrationTest.java +++ b/src/test/java/com/tnt/domain/member/MemberIntegrationTest.java @@ -31,7 +31,6 @@ void save_member_to_db_success() { // when Member savedMember = memberRepository.save(member); - memberRepository.flush(); // then Member foundMember = memberRepository.findById(savedMember.getId()) From 36e3566e3d64b5181d0de520a8850a0bd6153f6f Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Tue, 7 Jan 2025 10:57:46 +0900 Subject: [PATCH 57/75] =?UTF-8?q?[TNT-28]=20refactor:=20validate=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=20private=EB=A1=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95,=20=EB=A9=94=EC=84=9C=EB=93=9C=EB=AA=85=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tnt/application/auth/SessionService.java | 6 +++-- .../auth/SessionAuthenticationFilter.java | 19 ++++++--------- .../application/auth/SessionServiceTest.java | 24 ++++++++++++------- 3 files changed, 26 insertions(+), 23 deletions(-) diff --git a/src/main/java/com/tnt/application/auth/SessionService.java b/src/main/java/com/tnt/application/auth/SessionService.java index 5e4bd704..d0618741 100644 --- a/src/main/java/com/tnt/application/auth/SessionService.java +++ b/src/main/java/com/tnt/application/auth/SessionService.java @@ -23,17 +23,19 @@ public class SessionService { private static final String SESSION_ID_PREFIX = "SESSION-ID "; private final RedisTemplate redisTemplate; - public String extractMemberSession(HttpServletRequest request) { + public String authenticate(HttpServletRequest request) { String authHeader = request.getHeader(AUTHORIZATION_HEADER); + if (authHeader.isBlank() || !authHeader.startsWith(SESSION_ID_PREFIX)) { log.error("Authorization 헤더가 존재하지 않거나 올바르지 않은 형식입니다."); throw new UnauthorizedException("인증 세션이 존재하지 않습니다."); } + validateSession(authHeader.substring(SESSION_ID_PREFIX.length())); return authHeader.substring(SESSION_ID_PREFIX.length()); } - public void validateMemberSession(String sessionId) { + private void validateSession(String sessionId) { // 세션 존재 여부 확인 if (Boolean.FALSE.equals(redisTemplate.hasKey(sessionId))) { log.error("세션이 존재하지 않음 - SessionId: {}", sessionId); diff --git a/src/main/java/com/tnt/global/auth/SessionAuthenticationFilter.java b/src/main/java/com/tnt/global/auth/SessionAuthenticationFilter.java index 6070a9ef..95044780 100644 --- a/src/main/java/com/tnt/global/auth/SessionAuthenticationFilter.java +++ b/src/main/java/com/tnt/global/auth/SessionAuthenticationFilter.java @@ -37,11 +37,9 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse FilterChain filterChain) throws ServletException, IOException { String requestUri = request.getRequestURI(); String queryString = request.getQueryString(); - log.info("들어온 요청 - URI: {}, Query: {}, Method: {}", - requestUri, - queryString != null ? queryString : "쿼리 스트링 없음", - request.getMethod()); + log.info("들어온 요청 - URI: {}, Query: {}, Method: {}", requestUri, queryString != null ? queryString : "쿼리 스트링 없음", + request.getMethod()); if (isAllowedUri(requestUri)) { log.info("{} 허용 URI. 세션 유효성 검사 스킵.", requestUri); filterChain.doFilter(request, response); @@ -58,6 +56,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse private boolean isAllowedUri(String requestUri) { boolean allowed = false; + for (String pattern : allowedUris) { if (pathMatcher.match(pattern, requestUri)) { allowed = true; @@ -71,8 +70,8 @@ private boolean isAllowedUri(String requestUri) { private void checkSessionAndAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { - String sessionId = sessionService.extractMemberSession(request); - sessionService.validateMemberSession(sessionId); + String sessionId = sessionService.authenticate(request); + saveAuthentication(Long.parseLong(sessionId)); filterChain.doFilter(request, response); } @@ -82,7 +81,6 @@ private void handleUnauthorizedException(HttpServletResponse response, Unauthori log.error("인증 실패: ", exception); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setContentType("application/json;charset=UTF-8"); - response.getWriter().write(exception.getMessage()); } @@ -93,11 +91,8 @@ private void saveAuthentication(Long memberId) { .roles("USER") .build(); - Authentication authentication = new UsernamePasswordAuthenticationToken( - userDetails, - null, - authoritiesMapper.mapAuthorities(userDetails.getAuthorities()) - ); + Authentication authentication = new UsernamePasswordAuthenticationToken(userDetails, null, + authoritiesMapper.mapAuthorities(userDetails.getAuthorities())); SecurityContextHolder.getContext().setAuthentication(authentication); log.info("시큐리티 컨텍스트에 인증 정보 저장 완료 - MemberId: {}", memberId); diff --git a/src/test/java/com/tnt/application/auth/SessionServiceTest.java b/src/test/java/com/tnt/application/auth/SessionServiceTest.java index a4bb382e..a648665f 100644 --- a/src/test/java/com/tnt/application/auth/SessionServiceTest.java +++ b/src/test/java/com/tnt/application/auth/SessionServiceTest.java @@ -43,19 +43,19 @@ void no_authorization_header_error() { given(request.getHeader("Authorization")).willReturn(null); // when & then - assertThatThrownBy(() -> sessionService.extractMemberSession(request)) + assertThatThrownBy(() -> sessionService.authenticate(request)) .isInstanceOf(UnauthorizedException.class) .hasMessage("인증 세션이 존재하지 않습니다."); } @Test - @DisplayName("Authorization 헤더가 Bearer로 시작하지 않으면 예외 발생") + @DisplayName("Authorization 헤더가 SESSION-ID로 시작하지 않으면 예외 발생") void invalid_authorization_header_format_error() { // given given(request.getHeader("Authorization")).willReturn("Invalid 12345"); // when & then - assertThatThrownBy(() -> sessionService.extractMemberSession(request)) + assertThatThrownBy(() -> sessionService.authenticate(request)) .isInstanceOf(UnauthorizedException.class) .hasMessage("인증 세션이 존재하지 않습니다."); } @@ -65,10 +65,11 @@ void invalid_authorization_header_format_error() { void extract_session_id_success() { // given String sessionId = "test-session-id"; - given(request.getHeader("Authorization")).willReturn("Bearer " + sessionId); + + given(request.getHeader("Authorization")).willReturn("SESSION-ID " + sessionId); // when - String extractedSessionId = sessionService.extractMemberSession(request); + String extractedSessionId = sessionService.authenticate(request); // then assertThat(extractedSessionId).isEqualTo(sessionId); @@ -79,10 +80,12 @@ void extract_session_id_success() { void session_does_not_exist_in_storage_error() { // given String sessionId = "test-session-id"; + + given(request.getHeader("Authorization")).willReturn("SESSION-ID " + sessionId); given(redisTemplate.hasKey(sessionId)).willReturn(false); // when & then - assertThatThrownBy(() -> sessionService.validateMemberSession(sessionId)) + assertThatThrownBy(() -> sessionService.authenticate(request)) .isInstanceOf(UnauthorizedException.class) .hasMessage("세션 스토리지에 세션이 존재하지 않습니다."); } @@ -96,12 +99,13 @@ void session_expires_error() { .lastAccessTime(LocalDateTime.now().minusDays(3)) // 48시간 초과 .build(); + given(request.getHeader("Authorization")).willReturn("SESSION-ID " + sessionId); given(redisTemplate.hasKey(sessionId)).willReturn(true); given(redisTemplate.opsForValue()).willReturn(valueOperations); given(valueOperations.get(sessionId)).willReturn(sessionInfo); // when & then - assertThatThrownBy(() -> sessionService.validateMemberSession(sessionId)) + assertThatThrownBy(() -> sessionService.authenticate(request)) .isInstanceOf(UnauthorizedException.class) .hasMessage("세션이 만료되었습니다."); @@ -119,12 +123,13 @@ void validate_and_refresh_session_success() { .clientIp("127.0.0.1") .build(); + given(request.getHeader("Authorization")).willReturn("SESSION-ID " + sessionId); given(redisTemplate.hasKey(sessionId)).willReturn(true); given(redisTemplate.opsForValue()).willReturn(valueOperations); given(valueOperations.get(sessionId)).willReturn(sessionInfo); // when - sessionService.validateMemberSession(sessionId); + sessionService.authenticate(request); // then verify(valueOperations).set( @@ -139,9 +144,10 @@ void validate_and_refresh_session_success() { @DisplayName("세션 생성 성공") void create_session_success() { // given - given(redisTemplate.opsForValue()).willReturn(valueOperations); String memberId = "12345"; + given(redisTemplate.opsForValue()).willReturn(valueOperations); + // when sessionService.createSession(memberId, request); From 84959032e676c716e4b66e45b772a02348fc0c59 Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Tue, 7 Jan 2025 11:04:57 +0900 Subject: [PATCH 58/75] =?UTF-8?q?[TNT-28]=20refactor:=20=EB=B6=88=ED=95=84?= =?UTF-8?q?=EC=9A=94=20=EC=A3=BC=EC=84=9D=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test/java/com/tnt/domain/member/MemberIntegrationTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/com/tnt/domain/member/MemberIntegrationTest.java b/src/test/java/com/tnt/domain/member/MemberIntegrationTest.java index c7bfc125..15f23fe2 100644 --- a/src/test/java/com/tnt/domain/member/MemberIntegrationTest.java +++ b/src/test/java/com/tnt/domain/member/MemberIntegrationTest.java @@ -11,7 +11,7 @@ import com.tnt.domain.member.repository.MemberRepository; @SpringBootTest -@Transactional // 롤백 비활성화 +@Transactional class MemberIntegrationTest { @Autowired From 733b374ab6b4a72589fdb1f0c19a14dc04a5e9a9 Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Tue, 7 Jan 2025 11:14:26 +0900 Subject: [PATCH 59/75] =?UTF-8?q?[TNT-28]=20refactor:=20=EB=B3=80=EC=88=98?= =?UTF-8?q?=20=EC=9C=84=EC=B9=98,=20=EA=B0=9C=ED=96=89=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../error/handler/GlobalExceptionHandler.java | 44 ++++++++----------- 1 file changed, 18 insertions(+), 26 deletions(-) diff --git a/src/main/java/com/tnt/global/error/handler/GlobalExceptionHandler.java b/src/main/java/com/tnt/global/error/handler/GlobalExceptionHandler.java index 06900c0b..f39dd4d4 100644 --- a/src/main/java/com/tnt/global/error/handler/GlobalExceptionHandler.java +++ b/src/main/java/com/tnt/global/error/handler/GlobalExceptionHandler.java @@ -3,6 +3,7 @@ import java.security.SecureRandom; import java.time.DateTimeException; import java.util.List; +import java.util.Objects; import org.springframework.http.HttpStatus; import org.springframework.http.converter.HttpMessageNotReadableException; @@ -35,10 +36,9 @@ public class GlobalExceptionHandler { // 필수 파라미터 예외 @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(MissingServletRequestParameterException.class) - protected ErrorResponse handleMissingServletRequestParameter( - MissingServletRequestParameterException exception) { - log.warn("Required request parameter is missing: {}", exception.getParameterName()); + protected ErrorResponse handleMissingServletRequestParameter(MissingServletRequestParameterException exception) { String errorMessage = String.format("필수 파라미터 '%s'가 누락되었습니다.", exception.getParameterName()); + log.warn("Required request parameter is missing: {}", exception.getParameterName()); return new ErrorResponse(errorMessage); } @@ -46,26 +46,17 @@ protected ErrorResponse handleMissingServletRequestParameter( // 파라미터 타입 예외 @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(MethodArgumentTypeMismatchException.class) - protected ErrorResponse handleMethodArgumentTypeMismatch( - MethodArgumentTypeMismatchException exception) { - exception.getRequiredType(); + protected ErrorResponse handleMethodArgumentTypeMismatch(MethodArgumentTypeMismatchException exception) { + String errorMessage; log.warn("Type mismatch for parameter: {}. Required type: {}", exception.getName(), + Objects.requireNonNull(exception.getRequiredType()).getSimpleName()); + + errorMessage = String.format("파라미터 '%s'의 형식이 올바르지 않습니다. 예상 타입: %s", exception.getName(), exception.getRequiredType().getSimpleName()); - String errorMessage; - exception.getRequiredType(); - errorMessage = String.format("파라미터 '%s'의 형식이 올바르지 않습니다. 예상 타입: %s", - exception.getName(), exception.getRequiredType().getSimpleName()); return new ErrorResponse(errorMessage); } - // 401 Unauthorized 예외 - @ResponseStatus(HttpStatus.UNAUTHORIZED) - @ExceptionHandler(UnauthorizedException.class) - protected ErrorResponse handleUnauthorizedException(TnTException exception) { - return new ErrorResponse(exception.getMessage()); - } - // @Validated 있는 클래스에서 @RequestParam, @PathVariable 등에 적용된 제약 조건 예외 @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(ConstraintViolationException.class) @@ -85,8 +76,7 @@ protected ErrorResponse handleConstraintViolationException(ConstraintViolationEx // @Valid, @Validated 있는 곳에서 주로 @RequestBody dto 필드에 적용된 검증 어노테이션 유효성 검사 실패 예외 @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(MethodArgumentNotValidException.class) - protected ErrorResponse handleMethodArgumentNotValidException( - MethodArgumentNotValidException exception) { + protected ErrorResponse handleMethodArgumentNotValidException(MethodArgumentNotValidException exception) { log.warn(exception.getBindingResult().getAllErrors().getFirst().getDefaultMessage()); return new ErrorResponse(exception.getBindingResult().getAllErrors().getFirst().getDefaultMessage()); @@ -94,21 +84,23 @@ protected ErrorResponse handleMethodArgumentNotValidException( // json 파싱, 날짜/시간 형식 예외 @ResponseStatus(HttpStatus.BAD_REQUEST) - @ExceptionHandler(value = { - HttpMessageNotReadableException.class, - DateTimeException.class - }) + @ExceptionHandler(value = {HttpMessageNotReadableException.class, DateTimeException.class}) protected ErrorResponse handleDateTimeParseException(DateTimeException exception) { log.warn(exception.getMessage()); return new ErrorResponse("DateTime 형식이 잘못되었습니다. 서버 관리자에게 문의해 주세요."); } + // 401 Unauthorized 예외 + @ResponseStatus(HttpStatus.UNAUTHORIZED) + @ExceptionHandler(UnauthorizedException.class) + protected ErrorResponse handleUnauthorizedException(TnTException exception) { + return new ErrorResponse(exception.getMessage()); + } + // 커스텀 예외 @ResponseStatus(HttpStatus.BAD_REQUEST) - @ExceptionHandler(value = { - MaxUploadSizeExceededException.class - }) + @ExceptionHandler(value = {MaxUploadSizeExceededException.class}) protected ErrorResponse handleCustomBadRequestException(RuntimeException exception) { log.warn(exception.getMessage()); From 281858d23da441fc7cfb8849ed7e21a61198b0ac Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Tue, 7 Jan 2025 11:14:54 +0900 Subject: [PATCH 60/75] =?UTF-8?q?[TNT-28]=20refactor:=20doFilter=20?= =?UTF-8?q?=EC=9C=84=EC=B9=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/auth/SessionAuthenticationFilter.java | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/tnt/global/auth/SessionAuthenticationFilter.java b/src/main/java/com/tnt/global/auth/SessionAuthenticationFilter.java index 95044780..f40b6ae3 100644 --- a/src/main/java/com/tnt/global/auth/SessionAuthenticationFilter.java +++ b/src/main/java/com/tnt/global/auth/SessionAuthenticationFilter.java @@ -14,7 +14,6 @@ import org.springframework.web.filter.OncePerRequestFilter; import com.tnt.application.auth.SessionService; -import com.tnt.global.error.exception.UnauthorizedException; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; @@ -47,11 +46,13 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse } try { - checkSessionAndAuthentication(request, response, filterChain); + checkSessionAndAuthentication(request); } catch (RuntimeException e) { log.error("인증 처리 중 에러 발생: ", e); - handleUnauthorizedException(response, new UnauthorizedException("인증 처리 중 에러가 발생했습니다.")); + handleUnauthorizedException(response, e); } + + filterChain.doFilter(request, response); } private boolean isAllowedUri(String requestUri) { @@ -68,15 +69,13 @@ private boolean isAllowedUri(String requestUri) { return allowed; } - private void checkSessionAndAuthentication(HttpServletRequest request, HttpServletResponse response, - FilterChain filterChain) throws ServletException, IOException { + private void checkSessionAndAuthentication(HttpServletRequest request) { String sessionId = sessionService.authenticate(request); saveAuthentication(Long.parseLong(sessionId)); - filterChain.doFilter(request, response); } - private void handleUnauthorizedException(HttpServletResponse response, UnauthorizedException exception) throws + private void handleUnauthorizedException(HttpServletResponse response, RuntimeException exception) throws IOException { log.error("인증 실패: ", exception); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); From c88469fb9d61a1243631ccfd591f142948e676eb Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Tue, 7 Jan 2025 11:15:19 +0900 Subject: [PATCH 61/75] =?UTF-8?q?[TNT-28]=20refactor:=20=EB=A9=94=EC=84=9C?= =?UTF-8?q?=EB=93=9C=EB=AA=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/SessionAuthenticationFilterTest.java | 32 +++++++++---------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/src/test/java/com/tnt/global/auth/SessionAuthenticationFilterTest.java b/src/test/java/com/tnt/global/auth/SessionAuthenticationFilterTest.java index 8e5f3a47..31008e86 100644 --- a/src/test/java/com/tnt/global/auth/SessionAuthenticationFilterTest.java +++ b/src/test/java/com/tnt/global/auth/SessionAuthenticationFilterTest.java @@ -64,7 +64,7 @@ void allowed_uri_skip_session_verification_success() throws ServletException, IO sessionAuthenticationFilter.doFilterInternal(request, response, filterChain); // then - verify(sessionService, never()).extractMemberSession(any()); + verify(sessionService, never()).authenticate(any()); verify(filterChain).doFilter(request, response); } @@ -72,11 +72,10 @@ void allowed_uri_skip_session_verification_success() throws ServletException, IO @DisplayName("Authorization 헤더가 없는 경우 예외 발생") void missing_authorization_header_error() throws ServletException, IOException { // given - given(request.getRequestURI()).willReturn("/api/members/me"); - given(sessionService.extractMemberSession(request)) - .willThrow(new UnauthorizedException("인증 세션이 존재하지 않습니다.")); - StringWriter stringWriter = new StringWriter(); + + given(request.getRequestURI()).willReturn("/api/members/me"); + given(sessionService.authenticate(request)).willThrow(new UnauthorizedException("인증 세션이 존재하지 않습니다.")); given(response.getWriter()).willReturn(new PrintWriter(stringWriter)); // when @@ -93,14 +92,14 @@ void missing_authorization_header_error() throws ServletException, IOException { @DisplayName("세션이 스토리지에 존재하지 않는 경우 예외 발생") void session_not_exist_in_storage_error() throws ServletException, IOException { // given + StringWriter stringWriter = new StringWriter(); String sessionId = "12345"; + given(request.getRequestURI()).willReturn("/api/members/me"); - given(sessionService.extractMemberSession(request)).willReturn(sessionId); + given(sessionService.authenticate(request)).willReturn(sessionId); willThrow(new UnauthorizedException("세션 스토리지에 세션이 존재하지 않습니다.")) .given(sessionService) - .validateMemberSession(sessionId); - - StringWriter stringWriter = new StringWriter(); + .authenticate(request); given(response.getWriter()).willReturn(new PrintWriter(stringWriter)); // when @@ -117,14 +116,14 @@ void session_not_exist_in_storage_error() throws ServletException, IOException { @DisplayName("유효한 세션이 아닐 경우 예외 발생") void expired_session_error() throws ServletException, IOException { // given + StringWriter stringWriter = new StringWriter(); String sessionId = "12345"; + given(request.getRequestURI()).willReturn("/api/members/me"); - given(sessionService.extractMemberSession(request)).willReturn(sessionId); + given(sessionService.authenticate(request)).willReturn(sessionId); willThrow(new UnauthorizedException("세션이 만료되었습니다.")) .given(sessionService) - .validateMemberSession(sessionId); - - StringWriter stringWriter = new StringWriter(); + .authenticate(request); given(response.getWriter()).willReturn(new PrintWriter(stringWriter)); // when @@ -142,17 +141,16 @@ void expired_session_error() throws ServletException, IOException { void authenticate_with_valid_session_success() throws ServletException, IOException { // given String sessionId = "12345"; + given(request.getRequestURI()).willReturn("/api/members/me"); - given(sessionService.extractMemberSession(request)).willReturn(sessionId); - willDoNothing().given(sessionService).validateMemberSession(sessionId); + given(sessionService.authenticate(request)).willReturn(sessionId); // when sessionAuthenticationFilter.doFilterInternal(request, response, filterChain); // then verify(filterChain).doFilter(request, response); - verify(sessionService).extractMemberSession(request); - verify(sessionService).validateMemberSession(sessionId); + verify(sessionService).authenticate(request); } @AfterEach From f35949e48863e264eea4f1f87cfd47e771d3ed27 Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Tue, 7 Jan 2025 11:25:56 +0900 Subject: [PATCH 62/75] =?UTF-8?q?[TNT-28]=20refactor:=20requireNonNull=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/tnt/global/error/handler/GlobalExceptionHandler.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/tnt/global/error/handler/GlobalExceptionHandler.java b/src/main/java/com/tnt/global/error/handler/GlobalExceptionHandler.java index f39dd4d4..cb837f10 100644 --- a/src/main/java/com/tnt/global/error/handler/GlobalExceptionHandler.java +++ b/src/main/java/com/tnt/global/error/handler/GlobalExceptionHandler.java @@ -1,9 +1,10 @@ package com.tnt.global.error.handler; +import static java.util.Objects.*; + import java.security.SecureRandom; import java.time.DateTimeException; import java.util.List; -import java.util.Objects; import org.springframework.http.HttpStatus; import org.springframework.http.converter.HttpMessageNotReadableException; @@ -49,7 +50,7 @@ protected ErrorResponse handleMissingServletRequestParameter(MissingServletReque protected ErrorResponse handleMethodArgumentTypeMismatch(MethodArgumentTypeMismatchException exception) { String errorMessage; log.warn("Type mismatch for parameter: {}. Required type: {}", exception.getName(), - Objects.requireNonNull(exception.getRequiredType()).getSimpleName()); + requireNonNull(exception.getRequiredType()).getSimpleName()); errorMessage = String.format("파라미터 '%s'의 형식이 올바르지 않습니다. 예상 타입: %s", exception.getName(), exception.getRequiredType().getSimpleName()); From 333521762cf9a2c1081186a45169827ac72c8591 Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Tue, 7 Jan 2025 11:43:26 +0900 Subject: [PATCH 63/75] =?UTF-8?q?[TNT-28]=20refactor:=20=EC=9D=B8=EC=A6=9D?= =?UTF-8?q?=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tnt/application/auth/SessionService.java | 29 +++++-------------- 1 file changed, 7 insertions(+), 22 deletions(-) diff --git a/src/main/java/com/tnt/application/auth/SessionService.java b/src/main/java/com/tnt/application/auth/SessionService.java index d0618741..629f386c 100644 --- a/src/main/java/com/tnt/application/auth/SessionService.java +++ b/src/main/java/com/tnt/application/auth/SessionService.java @@ -1,5 +1,7 @@ package com.tnt.application.auth; +import static java.util.Objects.*; + import java.time.LocalDateTime; import java.util.concurrent.TimeUnit; @@ -25,34 +27,17 @@ public class SessionService { public String authenticate(HttpServletRequest request) { String authHeader = request.getHeader(AUTHORIZATION_HEADER); + String sessionId; if (authHeader.isBlank() || !authHeader.startsWith(SESSION_ID_PREFIX)) { log.error("Authorization 헤더가 존재하지 않거나 올바르지 않은 형식입니다."); - throw new UnauthorizedException("인증 세션이 존재하지 않습니다."); - } - validateSession(authHeader.substring(SESSION_ID_PREFIX.length())); - - return authHeader.substring(SESSION_ID_PREFIX.length()); - } - - private void validateSession(String sessionId) { - // 세션 존재 여부 확인 - if (Boolean.FALSE.equals(redisTemplate.hasKey(sessionId))) { - log.error("세션이 존재하지 않음 - SessionId: {}", sessionId); - throw new UnauthorizedException("세션 스토리지에 세션이 존재하지 않습니다."); + throw new UnauthorizedException("인가 세션이 존재하지 않습니다."); } + sessionId = authHeader.substring(SESSION_ID_PREFIX.length()); - SessionInfo sessionInfo = redisTemplate.opsForValue().get(sessionId); - - // 세션 갱신 - sessionInfo = SessionInfo.builder() - .lastAccessTime(LocalDateTime.now()) - .userAgent(sessionInfo.getUserAgent()) - .clientIp(sessionInfo.getClientIp()) - .build(); + requireNonNull(redisTemplate.opsForValue().get(sessionId), "세션 스토리지에 세션이 존재하지 않습니다."); - redisTemplate.opsForValue().set(sessionId, sessionInfo, SESSION_DURATION, TimeUnit.SECONDS); - log.info("세션 유효성 검증 완료 및 갱신 - SessionId: {}", sessionId); + return sessionId; } public void createSession(String memberId, HttpServletRequest request) { From 6f330ffbee0c6da7cb1aafa49acdf8395cb4b34d Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Tue, 7 Jan 2025 11:43:50 +0900 Subject: [PATCH 64/75] =?UTF-8?q?[TNT-28]=20refactor:=20=EB=A9=94=EC=8B=9C?= =?UTF-8?q?=EC=A7=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test/java/com/tnt/application/auth/SessionServiceTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/com/tnt/application/auth/SessionServiceTest.java b/src/test/java/com/tnt/application/auth/SessionServiceTest.java index a648665f..c8dc0d81 100644 --- a/src/test/java/com/tnt/application/auth/SessionServiceTest.java +++ b/src/test/java/com/tnt/application/auth/SessionServiceTest.java @@ -57,7 +57,7 @@ void invalid_authorization_header_format_error() { // when & then assertThatThrownBy(() -> sessionService.authenticate(request)) .isInstanceOf(UnauthorizedException.class) - .hasMessage("인증 세션이 존재하지 않습니다."); + .hasMessage("인가 세션이 존재하지 않습니다."); } @Test From 11aed26d92fac016a402ec5911ff6c12f90616c0 Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Tue, 7 Jan 2025 11:46:09 +0900 Subject: [PATCH 65/75] =?UTF-8?q?[TNT-28]=20refactor:=20=EB=A9=94=EC=8B=9C?= =?UTF-8?q?=EC=A7=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test/java/com/tnt/application/auth/SessionServiceTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/com/tnt/application/auth/SessionServiceTest.java b/src/test/java/com/tnt/application/auth/SessionServiceTest.java index c8dc0d81..95813095 100644 --- a/src/test/java/com/tnt/application/auth/SessionServiceTest.java +++ b/src/test/java/com/tnt/application/auth/SessionServiceTest.java @@ -45,7 +45,7 @@ void no_authorization_header_error() { // when & then assertThatThrownBy(() -> sessionService.authenticate(request)) .isInstanceOf(UnauthorizedException.class) - .hasMessage("인증 세션이 존재하지 않습니다."); + .hasMessage("인가 세션이 존재하지 않습니다."); } @Test From ccf892e3974f12f1b302907cc11803d40c376f90 Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Tue, 7 Jan 2025 11:47:12 +0900 Subject: [PATCH 66/75] =?UTF-8?q?[TNT-28]=20refactor:=20=EB=A9=94=EC=8B=9C?= =?UTF-8?q?=EC=A7=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/tnt/global/auth/SessionAuthenticationFilterTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/com/tnt/global/auth/SessionAuthenticationFilterTest.java b/src/test/java/com/tnt/global/auth/SessionAuthenticationFilterTest.java index 31008e86..fe6f563e 100644 --- a/src/test/java/com/tnt/global/auth/SessionAuthenticationFilterTest.java +++ b/src/test/java/com/tnt/global/auth/SessionAuthenticationFilterTest.java @@ -75,7 +75,7 @@ void missing_authorization_header_error() throws ServletException, IOException { StringWriter stringWriter = new StringWriter(); given(request.getRequestURI()).willReturn("/api/members/me"); - given(sessionService.authenticate(request)).willThrow(new UnauthorizedException("인증 세션이 존재하지 않습니다.")); + given(sessionService.authenticate(request)).willThrow(new UnauthorizedException("인가 세션이 존재하지 않습니다.")); given(response.getWriter()).willReturn(new PrintWriter(stringWriter)); // when @@ -85,7 +85,7 @@ void missing_authorization_header_error() throws ServletException, IOException { verify(response).setStatus(HttpServletResponse.SC_UNAUTHORIZED); verify(response).setContentType("application/json;charset=UTF-8"); verify(filterChain, never()).doFilter(request, response); - assertThat(stringWriter.toString()).contains("인증 세션이 존재하지 않습니다."); + assertThat(stringWriter.toString()).contains("인가 세션이 존재하지 않습니다."); } @Test From 94ee88b9eba22f20261d92aaae503b0382eeceaa Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Tue, 7 Jan 2025 11:47:31 +0900 Subject: [PATCH 67/75] =?UTF-8?q?[TNT-28]=20refactor:=20age=20int=EB=A1=9C?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/tnt/domain/member/Member.java | 4 ++-- .../java/com/tnt/domain/member/MemberIntegrationTest.java | 2 +- src/test/java/com/tnt/domain/member/MemberTest.java | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/tnt/domain/member/Member.java b/src/main/java/com/tnt/domain/member/Member.java index 12db7363..69ad650e 100644 --- a/src/main/java/com/tnt/domain/member/Member.java +++ b/src/main/java/com/tnt/domain/member/Member.java @@ -37,7 +37,7 @@ public class Member extends BaseTimeEntity { private String name; @Column(name = "age", nullable = false) - private String age; + private int age; @Column(name = "profile", nullable = false) private String profile; @@ -50,7 +50,7 @@ public class Member extends BaseTimeEntity { private SocialType socialType; @Builder - public Member(Long id, String socialId, String email, String name, String age, SocialType socialType) { + public Member(Long id, String socialId, String email, String name, int age, SocialType socialType) { this.id = id; this.socialId = socialId; this.email = email; diff --git a/src/test/java/com/tnt/domain/member/MemberIntegrationTest.java b/src/test/java/com/tnt/domain/member/MemberIntegrationTest.java index 15f23fe2..1fac11b2 100644 --- a/src/test/java/com/tnt/domain/member/MemberIntegrationTest.java +++ b/src/test/java/com/tnt/domain/member/MemberIntegrationTest.java @@ -25,7 +25,7 @@ void save_member_to_db_success() { .socialId("12345") .email("test@example.com") .name("홍길동") - .age("20") + .age(20) .socialType(SocialType.KAKAO) .build(); diff --git a/src/test/java/com/tnt/domain/member/MemberTest.java b/src/test/java/com/tnt/domain/member/MemberTest.java index 9f28d4e9..e7623430 100644 --- a/src/test/java/com/tnt/domain/member/MemberTest.java +++ b/src/test/java/com/tnt/domain/member/MemberTest.java @@ -33,7 +33,7 @@ void create_member_success() { .socialId("12345") .email("test@example.com") .name("홍길동") - .age("20") + .age(20) .socialType(SocialType.KAKAO) .build(); @@ -52,7 +52,7 @@ void verify_tsid_duplication_success() { .socialId("social" + i) .email("test" + i + "@example.com") .name("사용자" + i) - .age(String.valueOf(20 + (i % 20))) + .age(20 + (i % 20)) .socialType(SocialType.KAKAO) .build()) .map(Member::getId) @@ -76,7 +76,7 @@ void verify_tsid_timestamp_success() { .socialId("12345") .email("test@example.com") .name("홍길동") - .age("20") + .age(20) .socialType(SocialType.KAKAO) .build(); TSID tsid = TSID.from(member.getId()); From 9bf66144f16c3217ff503be3d75a6c0cf7e095ed Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Tue, 7 Jan 2025 11:53:37 +0900 Subject: [PATCH 68/75] =?UTF-8?q?[TNT-28]=20refactor:=20saveAuthentication?= =?UTF-8?q?=20=EB=A9=94=EC=84=9C=EB=93=9C=EC=9D=98=20=ED=8C=8C=EB=9D=BC?= =?UTF-8?q?=EB=AF=B8=ED=84=B0=EB=AA=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/tnt/global/auth/SessionAuthenticationFilter.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/tnt/global/auth/SessionAuthenticationFilter.java b/src/main/java/com/tnt/global/auth/SessionAuthenticationFilter.java index f40b6ae3..5d65a408 100644 --- a/src/main/java/com/tnt/global/auth/SessionAuthenticationFilter.java +++ b/src/main/java/com/tnt/global/auth/SessionAuthenticationFilter.java @@ -83,9 +83,9 @@ private void handleUnauthorizedException(HttpServletResponse response, RuntimeEx response.getWriter().write(exception.getMessage()); } - private void saveAuthentication(Long memberId) { + private void saveAuthentication(Long sessionId) { UserDetails userDetails = User.builder() - .username(String.valueOf(memberId)) + .username(String.valueOf(sessionId)) .password("") .roles("USER") .build(); @@ -94,6 +94,6 @@ private void saveAuthentication(Long memberId) { authoritiesMapper.mapAuthorities(userDetails.getAuthorities())); SecurityContextHolder.getContext().setAuthentication(authentication); - log.info("시큐리티 컨텍스트에 인증 정보 저장 완료 - MemberId: {}", memberId); + log.info("시큐리티 컨텍스트에 인증 정보 저장 완료 - SessionId: {}", sessionId); } } From b759144cc6c464cfbffde4d3d1ddcd7e5a12afd9 Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Tue, 7 Jan 2025 12:13:52 +0900 Subject: [PATCH 69/75] =?UTF-8?q?[TNT-28]=20refactor:=20isBlank=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/tnt/application/auth/SessionService.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/tnt/application/auth/SessionService.java b/src/main/java/com/tnt/application/auth/SessionService.java index 629f386c..58961d45 100644 --- a/src/main/java/com/tnt/application/auth/SessionService.java +++ b/src/main/java/com/tnt/application/auth/SessionService.java @@ -1,5 +1,6 @@ package com.tnt.application.auth; +import static io.micrometer.common.util.StringUtils.*; import static java.util.Objects.*; import java.time.LocalDateTime; @@ -29,7 +30,7 @@ public String authenticate(HttpServletRequest request) { String authHeader = request.getHeader(AUTHORIZATION_HEADER); String sessionId; - if (authHeader.isBlank() || !authHeader.startsWith(SESSION_ID_PREFIX)) { + if (isBlank(authHeader) || !authHeader.startsWith(SESSION_ID_PREFIX)) { log.error("Authorization 헤더가 존재하지 않거나 올바르지 않은 형식입니다."); throw new UnauthorizedException("인가 세션이 존재하지 않습니다."); } From 1028f01a916abaff3b0cfee0e1fef0a635414bae Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Tue, 7 Jan 2025 12:14:05 +0900 Subject: [PATCH 70/75] =?UTF-8?q?[TNT-28]=20refactor:=20=ED=95=84=EC=9A=94?= =?UTF-8?q?=EC=97=86=EB=8A=94=20=ED=85=8C=EC=8A=A4=ED=8A=B8=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 --- .../application/auth/SessionServiceTest.java | 62 +++---------------- 1 file changed, 7 insertions(+), 55 deletions(-) diff --git a/src/test/java/com/tnt/application/auth/SessionServiceTest.java b/src/test/java/com/tnt/application/auth/SessionServiceTest.java index 95813095..38fcf83b 100644 --- a/src/test/java/com/tnt/application/auth/SessionServiceTest.java +++ b/src/test/java/com/tnt/application/auth/SessionServiceTest.java @@ -1,10 +1,8 @@ package com.tnt.application.auth; -import static com.tnt.application.auth.SessionService.*; import static org.assertj.core.api.Assertions.*; import static org.mockito.BDDMockito.*; -import java.time.LocalDateTime; import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.DisplayName; @@ -40,7 +38,7 @@ class SessionServiceTest { @DisplayName("요청 헤더에 세션이 없으면 예외 발생") void no_authorization_header_error() { // given - given(request.getHeader("Authorization")).willReturn(null); + given(request.getHeader("Authorization")).willReturn(null, " "); // when & then assertThatThrownBy(() -> sessionService.authenticate(request)) @@ -65,8 +63,11 @@ void invalid_authorization_header_format_error() { void extract_session_id_success() { // given String sessionId = "test-session-id"; + SessionInfo sessionInfo = SessionInfo.builder().build(); given(request.getHeader("Authorization")).willReturn("SESSION-ID " + sessionId); + given(redisTemplate.opsForValue()).willReturn(valueOperations); + given(valueOperations.get(sessionId)).willReturn(sessionInfo); // when String extractedSessionId = sessionService.authenticate(request); @@ -82,62 +83,13 @@ void session_does_not_exist_in_storage_error() { String sessionId = "test-session-id"; given(request.getHeader("Authorization")).willReturn("SESSION-ID " + sessionId); - given(redisTemplate.hasKey(sessionId)).willReturn(false); - - // when & then - assertThatThrownBy(() -> sessionService.authenticate(request)) - .isInstanceOf(UnauthorizedException.class) - .hasMessage("세션 스토리지에 세션이 존재하지 않습니다."); - } - - @Test - @DisplayName("세션이 만료되면 예외 발생") - void session_expires_error() { - // given - String sessionId = "test-session-id"; - SessionInfo sessionInfo = SessionInfo.builder() - .lastAccessTime(LocalDateTime.now().minusDays(3)) // 48시간 초과 - .build(); - - given(request.getHeader("Authorization")).willReturn("SESSION-ID " + sessionId); - given(redisTemplate.hasKey(sessionId)).willReturn(true); given(redisTemplate.opsForValue()).willReturn(valueOperations); - given(valueOperations.get(sessionId)).willReturn(sessionInfo); + given(valueOperations.get(sessionId)).willReturn(null); // when & then assertThatThrownBy(() -> sessionService.authenticate(request)) - .isInstanceOf(UnauthorizedException.class) - .hasMessage("세션이 만료되었습니다."); - - verify(redisTemplate).delete(sessionId); - } - - @Test - @DisplayName("세션 유효성 검증 및 갱신 성공") - void validate_and_refresh_session_success() { - // given - String sessionId = "test-session-id"; - SessionInfo sessionInfo = SessionInfo.builder() - .lastAccessTime(LocalDateTime.now().minusHours(1)) - .userAgent("Mozilla") - .clientIp("127.0.0.1") - .build(); - - given(request.getHeader("Authorization")).willReturn("SESSION-ID " + sessionId); - given(redisTemplate.hasKey(sessionId)).willReturn(true); - given(redisTemplate.opsForValue()).willReturn(valueOperations); - given(valueOperations.get(sessionId)).willReturn(sessionInfo); - - // when - sessionService.authenticate(request); - - // then - verify(valueOperations).set( - eq(sessionId), - any(SessionInfo.class), - eq(SESSION_DURATION), - eq(TimeUnit.SECONDS) - ); + .isInstanceOf(NullPointerException.class) + .hasMessage("세션 스토리지에 세션이 존재하지 않습니다."); } @Test From 0e248b44a67bc604d84bcdeabab5c1a90dfc09a4 Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Tue, 7 Jan 2025 12:35:52 +0900 Subject: [PATCH 71/75] =?UTF-8?q?[TNT-28]=20refactor:=20=EC=98=88=EC=99=B8?= =?UTF-8?q?=20=EC=B2=98=EB=A6=AC=20=ED=9B=84=20return=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/tnt/global/auth/SessionAuthenticationFilter.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/tnt/global/auth/SessionAuthenticationFilter.java b/src/main/java/com/tnt/global/auth/SessionAuthenticationFilter.java index 5d65a408..f1c9d69c 100644 --- a/src/main/java/com/tnt/global/auth/SessionAuthenticationFilter.java +++ b/src/main/java/com/tnt/global/auth/SessionAuthenticationFilter.java @@ -50,6 +50,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse } catch (RuntimeException e) { log.error("인증 처리 중 에러 발생: ", e); handleUnauthorizedException(response, e); + return; } filterChain.doFilter(request, response); From 39dd95b61ae48c5bf58dc1f0141d3b034e716dd9 Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Tue, 7 Jan 2025 12:59:13 +0900 Subject: [PATCH 72/75] =?UTF-8?q?[TNT-28]=20refactor:=20=EC=9D=B8=EC=A6=9D?= =?UTF-8?q?=20=EC=A0=95=EB=B3=B4=20=EC=A0=80=EC=9E=A5,=20=EC=84=9C?= =?UTF-8?q?=EB=B8=94=EB=A6=BF=20=EC=98=88=EC=99=B8=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/SessionAuthenticationFilterTest.java | 79 ++++++++++--------- 1 file changed, 43 insertions(+), 36 deletions(-) diff --git a/src/test/java/com/tnt/global/auth/SessionAuthenticationFilterTest.java b/src/test/java/com/tnt/global/auth/SessionAuthenticationFilterTest.java index fe6f563e..e05c175a 100644 --- a/src/test/java/com/tnt/global/auth/SessionAuthenticationFilterTest.java +++ b/src/test/java/com/tnt/global/auth/SessionAuthenticationFilterTest.java @@ -15,9 +15,13 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; import com.tnt.application.auth.SessionService; import com.tnt.global.error.exception.UnauthorizedException; @@ -68,14 +72,23 @@ void allowed_uri_skip_session_verification_success() throws ServletException, IO verify(filterChain).doFilter(request, response); } - @Test - @DisplayName("Authorization 헤더가 없는 경우 예외 발생") - void missing_authorization_header_error() throws ServletException, IOException { + @ParameterizedTest + @DisplayName("세션 인증 실패 시 예외 발생") + @ValueSource(strings = { + "인가 세션이 존재하지 않습니다.", + "세션 스토리지에 세션이 존재하지 않습니다.", + "세션이 만료되었습니다." + }) + void session_authentication_failure_cases(String errorMessage) throws ServletException, IOException { // given StringWriter stringWriter = new StringWriter(); given(request.getRequestURI()).willReturn("/api/members/me"); - given(sessionService.authenticate(request)).willThrow(new UnauthorizedException("인가 세션이 존재하지 않습니다.")); + if (errorMessage.equals("세션 스토리지에 세션이 존재하지 않습니다.")) { + given(sessionService.authenticate(request)).willThrow(new RuntimeException(errorMessage)); + } else { + given(sessionService.authenticate(request)).willThrow(new UnauthorizedException(errorMessage)); + } given(response.getWriter()).willReturn(new PrintWriter(stringWriter)); // when @@ -85,72 +98,66 @@ void missing_authorization_header_error() throws ServletException, IOException { verify(response).setStatus(HttpServletResponse.SC_UNAUTHORIZED); verify(response).setContentType("application/json;charset=UTF-8"); verify(filterChain, never()).doFilter(request, response); - assertThat(stringWriter.toString()).contains("인가 세션이 존재하지 않습니다."); + assertThat(stringWriter.toString()).contains(errorMessage); } @Test - @DisplayName("세션이 스토리지에 존재하지 않는 경우 예외 발생") - void session_not_exist_in_storage_error() throws ServletException, IOException { + @DisplayName("유효한 세션으로 인증 정보 저장 성공") + void save_authentication_success() throws ServletException, IOException { // given - StringWriter stringWriter = new StringWriter(); String sessionId = "12345"; given(request.getRequestURI()).willReturn("/api/members/me"); given(sessionService.authenticate(request)).willReturn(sessionId); - willThrow(new UnauthorizedException("세션 스토리지에 세션이 존재하지 않습니다.")) - .given(sessionService) - .authenticate(request); - given(response.getWriter()).willReturn(new PrintWriter(stringWriter)); // when sessionAuthenticationFilter.doFilterInternal(request, response, filterChain); // then - verify(response).setStatus(HttpServletResponse.SC_UNAUTHORIZED); - verify(response).setContentType("application/json;charset=UTF-8"); - verify(filterChain, never()).doFilter(request, response); - assertThat(stringWriter.toString()).contains("세션 스토리지에 세션이 존재하지 않습니다."); + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + assertThat(authentication).isNotNull(); + assertThat(authentication.getPrincipal()).isInstanceOf(UserDetails.class); + UserDetails userDetails = (UserDetails)authentication.getPrincipal(); + assertThat(userDetails.getUsername()).isEqualTo(sessionId); + assertThat(userDetails.getAuthorities()) + .extracting("authority") + .contains("ROLE_USER"); } @Test - @DisplayName("유효한 세션이 아닐 경우 예외 발생") - void expired_session_error() throws ServletException, IOException { + @DisplayName("유효한 세션으로 인증 성공") + void authenticate_with_valid_session_success() throws ServletException, IOException { // given - StringWriter stringWriter = new StringWriter(); String sessionId = "12345"; given(request.getRequestURI()).willReturn("/api/members/me"); given(sessionService.authenticate(request)).willReturn(sessionId); - willThrow(new UnauthorizedException("세션이 만료되었습니다.")) - .given(sessionService) - .authenticate(request); - given(response.getWriter()).willReturn(new PrintWriter(stringWriter)); // when sessionAuthenticationFilter.doFilterInternal(request, response, filterChain); // then - verify(response).setStatus(HttpServletResponse.SC_UNAUTHORIZED); - verify(response).setContentType("application/json;charset=UTF-8"); - verify(filterChain, never()).doFilter(request, response); - assertThat(stringWriter.toString()).contains("세션이 만료되었습니다."); + verify(filterChain).doFilter(request, response); + verify(sessionService).authenticate(request); } @Test - @DisplayName("유효한 세션으로 인증 성공") - void authenticate_with_valid_session_success() throws ServletException, IOException { + @DisplayName("필터 체인 실행 중 ServletException 예외 발생") + void handle_servlet_exception_error() throws ServletException, IOException { // given String sessionId = "12345"; given(request.getRequestURI()).willReturn("/api/members/me"); given(sessionService.authenticate(request)).willReturn(sessionId); - - // when - sessionAuthenticationFilter.doFilterInternal(request, response, filterChain); - - // then - verify(filterChain).doFilter(request, response); - verify(sessionService).authenticate(request); + willThrow(new ServletException("필터 체인 에러")) + .given(filterChain) + .doFilter(any(), any()); + + // when & then + assertThatThrownBy(() -> + sessionAuthenticationFilter.doFilterInternal(request, response, filterChain)) + .isInstanceOf(ServletException.class) + .hasMessage("필터 체인 에러"); } @AfterEach From b7c75a7f44fdfa7f36903760acdd82529ca20423 Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Tue, 7 Jan 2025 13:06:45 +0900 Subject: [PATCH 73/75] =?UTF-8?q?[TNT-28]=20refactor:=20SessionValue?= =?UTF-8?q?=EB=A1=9C=20=ED=81=B4=EB=9E=98=EC=8A=A4=EB=AA=85=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tnt/application/auth/SessionService.java | 8 ++-- .../{SessionInfo.java => SessionValue.java} | 2 +- .../com/tnt/global/config/RedisConfig.java | 10 ++--- .../application/auth/SessionServiceTest.java | 41 ++++++++++++++++--- 4 files changed, 45 insertions(+), 16 deletions(-) rename src/main/java/com/tnt/domain/auth/{SessionInfo.java => SessionValue.java} (88%) diff --git a/src/main/java/com/tnt/application/auth/SessionService.java b/src/main/java/com/tnt/application/auth/SessionService.java index 58961d45..12dc86a8 100644 --- a/src/main/java/com/tnt/application/auth/SessionService.java +++ b/src/main/java/com/tnt/application/auth/SessionService.java @@ -9,7 +9,7 @@ import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; -import com.tnt.domain.auth.SessionInfo; +import com.tnt.domain.auth.SessionValue; import com.tnt.global.error.exception.UnauthorizedException; import jakarta.servlet.http.HttpServletRequest; @@ -24,7 +24,7 @@ public class SessionService { static final long SESSION_DURATION = 2L * 24 * 60 * 60; // 48시간 private static final String AUTHORIZATION_HEADER = "Authorization"; private static final String SESSION_ID_PREFIX = "SESSION-ID "; - private final RedisTemplate redisTemplate; + private final RedisTemplate redisTemplate; public String authenticate(HttpServletRequest request) { String authHeader = request.getHeader(AUTHORIZATION_HEADER); @@ -42,7 +42,7 @@ public String authenticate(HttpServletRequest request) { } public void createSession(String memberId, HttpServletRequest request) { - SessionInfo sessionInfo = SessionInfo.builder() + SessionValue sessionValue = SessionValue.builder() .lastAccessTime(LocalDateTime.now()) .userAgent(request.getHeader("User-Agent")) .clientIp(request.getRemoteAddr()) @@ -50,7 +50,7 @@ public void createSession(String memberId, HttpServletRequest request) { redisTemplate.opsForValue().set( memberId, - sessionInfo, + sessionValue, SESSION_DURATION, TimeUnit.SECONDS ); diff --git a/src/main/java/com/tnt/domain/auth/SessionInfo.java b/src/main/java/com/tnt/domain/auth/SessionValue.java similarity index 88% rename from src/main/java/com/tnt/domain/auth/SessionInfo.java rename to src/main/java/com/tnt/domain/auth/SessionValue.java index 629c1b31..ba6b4d02 100644 --- a/src/main/java/com/tnt/domain/auth/SessionInfo.java +++ b/src/main/java/com/tnt/domain/auth/SessionValue.java @@ -7,7 +7,7 @@ @Getter @Builder -public class SessionInfo { +public class SessionValue { private LocalDateTime lastAccessTime; private String userAgent; diff --git a/src/main/java/com/tnt/global/config/RedisConfig.java b/src/main/java/com/tnt/global/config/RedisConfig.java index 2b951f54..36a5d9f6 100644 --- a/src/main/java/com/tnt/global/config/RedisConfig.java +++ b/src/main/java/com/tnt/global/config/RedisConfig.java @@ -10,7 +10,7 @@ import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; -import com.tnt.domain.auth.SessionInfo; +import com.tnt.domain.auth.SessionValue; @Configuration @EnableRedisRepositories @@ -28,13 +28,13 @@ public RedisConnectionFactory redisConnectionFactory() { } @Bean - public RedisTemplate redisTemplate() { - RedisTemplate redisTemplate = new RedisTemplate<>(); + public RedisTemplate redisTemplate() { + RedisTemplate redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(redisConnectionFactory()); redisTemplate.setKeySerializer(new StringRedisSerializer()); - Jackson2JsonRedisSerializer jsonRedisSerializer = - new Jackson2JsonRedisSerializer<>(SessionInfo.class); + Jackson2JsonRedisSerializer jsonRedisSerializer = + new Jackson2JsonRedisSerializer<>(SessionValue.class); redisTemplate.setValueSerializer(jsonRedisSerializer); return redisTemplate; diff --git a/src/test/java/com/tnt/application/auth/SessionServiceTest.java b/src/test/java/com/tnt/application/auth/SessionServiceTest.java index 38fcf83b..82c4bf39 100644 --- a/src/test/java/com/tnt/application/auth/SessionServiceTest.java +++ b/src/test/java/com/tnt/application/auth/SessionServiceTest.java @@ -1,5 +1,6 @@ package com.tnt.application.auth; +import static com.tnt.application.auth.SessionService.*; import static org.assertj.core.api.Assertions.*; import static org.mockito.BDDMockito.*; @@ -14,7 +15,7 @@ import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.ValueOperations; -import com.tnt.domain.auth.SessionInfo; +import com.tnt.domain.auth.SessionValue; import com.tnt.global.error.exception.UnauthorizedException; import jakarta.servlet.http.HttpServletRequest; @@ -26,10 +27,10 @@ class SessionServiceTest { private SessionService sessionService; @Mock - private RedisTemplate redisTemplate; + private RedisTemplate redisTemplate; @Mock - private ValueOperations valueOperations; + private ValueOperations valueOperations; @Mock private HttpServletRequest request; @@ -63,11 +64,11 @@ void invalid_authorization_header_format_error() { void extract_session_id_success() { // given String sessionId = "test-session-id"; - SessionInfo sessionInfo = SessionInfo.builder().build(); + SessionValue sessionValue = SessionValue.builder().build(); given(request.getHeader("Authorization")).willReturn("SESSION-ID " + sessionId); given(redisTemplate.opsForValue()).willReturn(valueOperations); - given(valueOperations.get(sessionId)).willReturn(sessionInfo); + given(valueOperations.get(sessionId)).willReturn(sessionValue); // when String extractedSessionId = sessionService.authenticate(request); @@ -106,12 +107,40 @@ void create_session_success() { // then verify(valueOperations).set( eq(memberId), - any(SessionInfo.class), + any(SessionValue.class), eq(2L * 24 * 60 * 60), eq(TimeUnit.SECONDS) ); } + @Test + @DisplayName("세션 스토리지에 저장 성공") + void create_session_with_request_info_success() { + // given + String memberId = "12345"; + String userAgent = "Mozilla/5.0"; + String clientIp = "127.0.0.1"; + + given(request.getHeader("User-Agent")).willReturn(userAgent); + given(request.getRemoteAddr()).willReturn(clientIp); + given(redisTemplate.opsForValue()).willReturn(valueOperations); + + // when + sessionService.createSession(memberId, request); + + // then + verify(valueOperations).set( + eq(memberId), + argThat(sessionValue -> + sessionValue.getUserAgent().equals(userAgent) && + sessionValue.getClientIp().equals(clientIp) && + sessionValue.getLastAccessTime() != null + ), + eq(SESSION_DURATION), + eq(TimeUnit.SECONDS) + ); + } + @Test @DisplayName("세션 삭제 성공") void remove_session_success() { From 2590e9bda3b636b79d500e2318668782e280af99 Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Tue, 7 Jan 2025 13:11:03 +0900 Subject: [PATCH 74/75] =?UTF-8?q?[TNT-28]=20refactor:=20=EA=B0=9C=ED=96=89?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/tnt/application/auth/SessionServiceTest.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/test/java/com/tnt/application/auth/SessionServiceTest.java b/src/test/java/com/tnt/application/auth/SessionServiceTest.java index 82c4bf39..d6b5d94b 100644 --- a/src/test/java/com/tnt/application/auth/SessionServiceTest.java +++ b/src/test/java/com/tnt/application/auth/SessionServiceTest.java @@ -132,9 +132,8 @@ void create_session_with_request_info_success() { verify(valueOperations).set( eq(memberId), argThat(sessionValue -> - sessionValue.getUserAgent().equals(userAgent) && - sessionValue.getClientIp().equals(clientIp) && - sessionValue.getLastAccessTime() != null + sessionValue.getUserAgent().equals(userAgent) && sessionValue.getClientIp().equals(clientIp) + && sessionValue.getLastAccessTime() != null ), eq(SESSION_DURATION), eq(TimeUnit.SECONDS) From 33fb5aaf3f1de36e1c1d9891a3c349973d51e764 Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Tue, 7 Jan 2025 14:27:03 +0900 Subject: [PATCH 75/75] =?UTF-8?q?[TNT-28]=20refactor:=20=EB=B3=80=EC=88=98?= =?UTF-8?q?,=20=EB=A1=9C=EA=B7=B8=20=EA=B0=9C=ED=96=89=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/tnt/application/auth/SessionService.java | 5 +++-- .../global/auth/SessionAuthenticationFilter.java | 6 ++++++ .../error/handler/GlobalExceptionHandler.java | 14 +++++++------- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/tnt/application/auth/SessionService.java b/src/main/java/com/tnt/application/auth/SessionService.java index 12dc86a8..d7d5de73 100644 --- a/src/main/java/com/tnt/application/auth/SessionService.java +++ b/src/main/java/com/tnt/application/auth/SessionService.java @@ -28,13 +28,14 @@ public class SessionService { public String authenticate(HttpServletRequest request) { String authHeader = request.getHeader(AUTHORIZATION_HEADER); - String sessionId; if (isBlank(authHeader) || !authHeader.startsWith(SESSION_ID_PREFIX)) { log.error("Authorization 헤더가 존재하지 않거나 올바르지 않은 형식입니다."); + throw new UnauthorizedException("인가 세션이 존재하지 않습니다."); } - sessionId = authHeader.substring(SESSION_ID_PREFIX.length()); + + String sessionId = authHeader.substring(SESSION_ID_PREFIX.length()); requireNonNull(redisTemplate.opsForValue().get(sessionId), "세션 스토리지에 세션이 존재하지 않습니다."); diff --git a/src/main/java/com/tnt/global/auth/SessionAuthenticationFilter.java b/src/main/java/com/tnt/global/auth/SessionAuthenticationFilter.java index f1c9d69c..612165a0 100644 --- a/src/main/java/com/tnt/global/auth/SessionAuthenticationFilter.java +++ b/src/main/java/com/tnt/global/auth/SessionAuthenticationFilter.java @@ -39,8 +39,10 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse log.info("들어온 요청 - URI: {}, Query: {}, Method: {}", requestUri, queryString != null ? queryString : "쿼리 스트링 없음", request.getMethod()); + if (isAllowedUri(requestUri)) { log.info("{} 허용 URI. 세션 유효성 검사 스킵.", requestUri); + filterChain.doFilter(request, response); return; } @@ -49,6 +51,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse checkSessionAndAuthentication(request); } catch (RuntimeException e) { log.error("인증 처리 중 에러 발생: ", e); + handleUnauthorizedException(response, e); return; } @@ -65,6 +68,7 @@ private boolean isAllowedUri(String requestUri) { break; } } + log.info("URI {} is {}allowed", requestUri, allowed ? "" : "not "); return allowed; @@ -79,6 +83,7 @@ private void checkSessionAndAuthentication(HttpServletRequest request) { private void handleUnauthorizedException(HttpServletResponse response, RuntimeException exception) throws IOException { log.error("인증 실패: ", exception); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write(exception.getMessage()); @@ -95,6 +100,7 @@ private void saveAuthentication(Long sessionId) { authoritiesMapper.mapAuthorities(userDetails.getAuthorities())); SecurityContextHolder.getContext().setAuthentication(authentication); + log.info("시큐리티 컨텍스트에 인증 정보 저장 완료 - SessionId: {}", sessionId); } } diff --git a/src/main/java/com/tnt/global/error/handler/GlobalExceptionHandler.java b/src/main/java/com/tnt/global/error/handler/GlobalExceptionHandler.java index cb837f10..5873f6aa 100644 --- a/src/main/java/com/tnt/global/error/handler/GlobalExceptionHandler.java +++ b/src/main/java/com/tnt/global/error/handler/GlobalExceptionHandler.java @@ -39,6 +39,7 @@ public class GlobalExceptionHandler { @ExceptionHandler(MissingServletRequestParameterException.class) protected ErrorResponse handleMissingServletRequestParameter(MissingServletRequestParameterException exception) { String errorMessage = String.format("필수 파라미터 '%s'가 누락되었습니다.", exception.getParameterName()); + log.warn("Required request parameter is missing: {}", exception.getParameterName()); return new ErrorResponse(errorMessage); @@ -48,12 +49,11 @@ protected ErrorResponse handleMissingServletRequestParameter(MissingServletReque @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(MethodArgumentTypeMismatchException.class) protected ErrorResponse handleMethodArgumentTypeMismatch(MethodArgumentTypeMismatchException exception) { - String errorMessage; - log.warn("Type mismatch for parameter: {}. Required type: {}", exception.getName(), + String errorMessage = String.format("파라미터 '%s'의 형식이 올바르지 않습니다. 예상 타입: %s", exception.getName(), requireNonNull(exception.getRequiredType()).getSimpleName()); - errorMessage = String.format("파라미터 '%s'의 형식이 올바르지 않습니다. 예상 타입: %s", exception.getName(), - exception.getRequiredType().getSimpleName()); + log.warn("Type mismatch for parameter: {}. Required type: {}", exception.getName(), + requireNonNull(exception.getRequiredType()).getSimpleName()); return new ErrorResponse(errorMessage); } @@ -62,15 +62,14 @@ protected ErrorResponse handleMethodArgumentTypeMismatch(MethodArgumentTypeMisma @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(ConstraintViolationException.class) protected ErrorResponse handleConstraintViolationException(ConstraintViolationException exception) { - log.warn("Constraint violation: {}", exception.getMessage()); - List errors = exception.getConstraintViolations() .stream() .map(violation -> violation.getPropertyPath() + ": " + violation.getMessage()) .toList(); - String errorMessage = String.join(", ", errors); + log.warn("Constraint violation: {}", exception.getMessage()); + return new ErrorResponse("입력값이 유효하지 않습니다: " + errorMessage); } @@ -120,6 +119,7 @@ protected ErrorResponse handleRuntimeException(RuntimeException exception) { String errorKeyInfo = String.format(ERROR_KEY_FORMAT, sb); String exceptionTypeInfo = String.format(EXCEPTION_CLASS_TYPE_MESSAGE_FORMANT, exception.getClass()); + log.error("{}{}{}", exception.getMessage(), errorKeyInfo, exceptionTypeInfo); return new ErrorResponse(DEFAULT_ERROR_MESSAGE + errorKeyInfo);