From 5da9918c5d1adb14156468d3b282a7e6d5f16b0f Mon Sep 17 00:00:00 2001 From: FaberJoo Date: Thu, 16 Nov 2023 20:27:36 +0900 Subject: [PATCH 1/5] =?UTF-8?q?refactor:=20=EB=A1=9C=EA=B9=85=20=ED=95=84?= =?UTF-8?q?=ED=84=B0=20=EC=88=98=EC=A0=95=20#104?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../security/filter/HTTPLoggingFilter.java | 88 +++++++++++++++++++ .../security/filter/ReqLoggingFilter.java | 37 -------- 2 files changed, 88 insertions(+), 37 deletions(-) create mode 100644 src/main/java/io/oduck/api/global/security/filter/HTTPLoggingFilter.java delete mode 100644 src/main/java/io/oduck/api/global/security/filter/ReqLoggingFilter.java diff --git a/src/main/java/io/oduck/api/global/security/filter/HTTPLoggingFilter.java b/src/main/java/io/oduck/api/global/security/filter/HTTPLoggingFilter.java new file mode 100644 index 00000000..acd27bf3 --- /dev/null +++ b/src/main/java/io/oduck/api/global/security/filter/HTTPLoggingFilter.java @@ -0,0 +1,88 @@ +package io.oduck.api.global.security.filter; + +import static io.oduck.api.global.utils.HttpHeaderUtils.getClientIP; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.util.ContentCachingRequestWrapper; +import org.springframework.web.util.ContentCachingResponseWrapper; + +@Slf4j +@Component +public class HTTPLoggingFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + + if (isAsyncDispatch(request)) { + filterChain.doFilter(request, response); + } else { + doFilterWrapped(new ContentCachingRequestWrapper(request), + new ContentCachingResponseWrapper(response), filterChain); + } + } + + protected void doFilterWrapped(ContentCachingRequestWrapper request, + ContentCachingResponseWrapper response, + FilterChain filterChain) throws IOException, ServletException { + try { + filterChain.doFilter(request, response); + logRequest(request); + } finally { + logResponse(response); + response.copyBodyToResponse(); + } + } + + private static void logRequest(ContentCachingRequestWrapper request) throws IOException { + String queryString = request.getQueryString(); + log.info("Request : \n {} uri=[{}]\n content-type[{}]\n client-ip[{}]\n user-agent[{}]", request.getMethod(), + queryString == null ? request.getRequestURI() : request.getRequestURI() + queryString, + request.getContentType(), getClientIP(request), request.getHeader("User-Agent")); + logPayload("Request", request.getContentType(), request.getContentAsByteArray()); + } + + private static void logResponse(ContentCachingResponseWrapper response) throws IOException { + logPayload("Response", response.getContentType(), response.getContentAsByteArray()); + } + + private static void logPayload(String prefix, String contentType, byte[] rowData) + throws IOException { + boolean visible = isVisible( + MediaType.valueOf(contentType == null ? "application/json" : contentType)); + + if (visible) { + if (rowData.length > 0) { + String contentString = new String(rowData); + log.info("{}\n Payload: {}", prefix, contentString); + } + } else { + log.info("{} Payload: Binary Content", prefix); + } + } + + private static boolean isVisible(MediaType mediaType) { + final List VISIBLE_TYPES = Arrays.asList( + MediaType.valueOf("text/*"), + MediaType.APPLICATION_FORM_URLENCODED, + MediaType.APPLICATION_JSON, + MediaType.APPLICATION_XML, + MediaType.valueOf("application/*+json"), + MediaType.valueOf("application/*+xml"), + MediaType.MULTIPART_FORM_DATA + ); + + return VISIBLE_TYPES.stream() + .anyMatch(visibleType -> visibleType.includes(mediaType)); + } +} diff --git a/src/main/java/io/oduck/api/global/security/filter/ReqLoggingFilter.java b/src/main/java/io/oduck/api/global/security/filter/ReqLoggingFilter.java deleted file mode 100644 index a9ad2357..00000000 --- a/src/main/java/io/oduck/api/global/security/filter/ReqLoggingFilter.java +++ /dev/null @@ -1,37 +0,0 @@ -package io.oduck.api.global.security.filter; - -import static io.oduck.api.global.utils.HttpHeaderUtils.getClientIP; - -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import java.io.IOException; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; -import org.springframework.web.filter.OncePerRequestFilter; -import org.springframework.web.util.ContentCachingRequestWrapper; -import org.springframework.web.util.ContentCachingResponseWrapper; - -@Slf4j -@Component -public class ReqLoggingFilter extends OncePerRequestFilter { - - @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, - FilterChain filterChain) throws ServletException, IOException { - - if (!request.getRequestURI().startsWith("/api/v1/actuator/prometheus")) { - log.info("Request: {} {} {} {}", - request.getMethod(), - request.getRequestURI(), - getClientIP(request), - request.getHeader("User-Agent") - ); - } - - filterChain.doFilter(request, response); - } -} From 71e75e8896c106c749a2addb8eb857cc5cfef056 Mon Sep 17 00:00:00 2001 From: FaberJoo Date: Thu, 16 Nov 2023 20:29:06 +0900 Subject: [PATCH 2/5] =?UTF-8?q?refactor:=20=EC=9D=B8=EA=B0=80=20=EB=B0=8F?= =?UTF-8?q?=20=ED=95=84=ED=84=B0=20=EC=B2=B4=EC=9D=B8=EC=97=90=20=EB=A1=9C?= =?UTF-8?q?=EA=B9=85=20=ED=95=84=ED=84=B0=20=EC=B6=94=EA=B0=80=20#110?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../security/config/SecurityConfig.java | 45 +++++++++++-------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/src/main/java/io/oduck/api/global/security/config/SecurityConfig.java b/src/main/java/io/oduck/api/global/security/config/SecurityConfig.java index e40f442f..75c1281c 100644 --- a/src/main/java/io/oduck/api/global/security/config/SecurityConfig.java +++ b/src/main/java/io/oduck/api/global/security/config/SecurityConfig.java @@ -2,9 +2,9 @@ import static org.springframework.security.config.Customizer.withDefaults; -import com.fasterxml.jackson.databind.ObjectMapper; import io.oduck.api.domain.member.entity.Role; //import io.oduck.api.global.security.filter.LocalAuthenticationFilter; +import io.oduck.api.global.security.filter.HTTPLoggingFilter; import io.oduck.api.global.security.handler.ForbiddenHandler; import io.oduck.api.global.security.handler.LoginFailureHandler; import io.oduck.api.global.security.handler.LoginSuccessHandler; @@ -24,6 +24,7 @@ import org.springframework.security.crypto.factory.PasswordEncoderFactories; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.logout.LogoutFilter; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; //import org.springframework.security.web.context.HttpSessionSecurityContextRepository; //import org.springframework.security.web.context.SecurityContextRepository; @@ -34,13 +35,14 @@ @Configuration public class SecurityConfig { -// private final ObjectMapper objectMapper; + // private final ObjectMapper objectMapper; private final LogoutHandler logoutHandler; private final SocialLoginService socialLoginService; private final LoginSuccessHandler loginSuccessHandler; private final LoginFailureHandler loginFailureHandler; private final ForbiddenHandler forbiddenHandler; private final UnauthorizedHandler unauthorizedHandler; + private final HTTPLoggingFilter HTTPLoggingFilter; @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { @@ -75,15 +77,17 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { // 인가 설정 http .authorizeHttpRequests((authorizeRequests) -> - authorizeRequests - // .requestMatchers("docs/index.html").hasAuthority(Role.ADMIN.name()) - .requestMatchers("/auth/status").hasAnyAuthority(Role.MEMBER.name(), Role.ADMIN.name()) - .requestMatchers(HttpMethod.PUT, "/members/**").hasAnyAuthority(Role.MEMBER.name(), Role.ADMIN.name()) - .requestMatchers(HttpMethod.PATCH, "/members/**").hasAnyAuthority(Role.MEMBER.name(), Role.ADMIN.name()) - .requestMatchers(HttpMethod.DELETE, "/members/**").hasAnyAuthority(Role.MEMBER.name(), Role.ADMIN.name()) - .requestMatchers( "/bookmarks/**").hasAnyAuthority(Role.MEMBER.name(), Role.ADMIN.name()) + authorizeRequests + // .requestMatchers("docs/index.html").hasAuthority(Role.ADMIN.name()) + .requestMatchers("/auth/status").hasAnyAuthority(Role.WITHDRAWAL.name(), Role.MEMBER.name(), Role.ADMIN.name()) + .requestMatchers(HttpMethod.PUT, "/members/**").hasAnyAuthority(Role.MEMBER.name(), Role.ADMIN.name()) + .requestMatchers(HttpMethod.PATCH, "/members/**").hasAnyAuthority(Role.MEMBER.name(), Role.ADMIN.name()) + .requestMatchers(HttpMethod.DELETE, "/members/**").hasAnyAuthority(Role.MEMBER.name(), Role.ADMIN.name()) + .requestMatchers(HttpMethod.POST, "/bookmarks/**").hasAnyAuthority(Role.MEMBER.name(), Role.ADMIN.name()) + .requestMatchers(HttpMethod.POST, "/ratings/**").hasAnyAuthority(Role.MEMBER.name(), Role.ADMIN.name()) + .requestMatchers(HttpMethod.PATCH, "/ratings/**").hasAnyAuthority(Role.MEMBER.name(), Role.ADMIN.name()) // .requestMatchers("/oduckdmin/**").hasAuthority(Role.ADMIN.name()) - .anyRequest().permitAll() + .anyRequest().permitAll() ); // 로그아웃 설정 @@ -104,12 +108,12 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .oauth2Login((oauth2Login) -> oauth2Login - .userInfoEndpoint((userInfoEndpoint) -> - userInfoEndpoint - .userService(socialLoginService) - ) - .successHandler(loginSuccessHandler) - .failureHandler(loginFailureHandler) + .userInfoEndpoint((userInfoEndpoint) -> + userInfoEndpoint + .userService(socialLoginService) + ) + .successHandler(loginSuccessHandler) + .failureHandler(loginFailureHandler) ); // 예외 처리 설정 @@ -121,6 +125,9 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .accessDeniedHandler(forbiddenHandler) ); + http + .addFilterBefore(HTTPLoggingFilter, LogoutFilter.class); + return http.build(); } @@ -142,10 +149,10 @@ public AuthenticationManager authenticationManager( // // Custom Configurer : CustomFilterConfigurer 는 직접 구현한 필터인 JwtAuthenticationFilter 를 등록하는 역할 // public class CustomFilterConfigurer extends AbstractHttpConfigurer { - // AbstractHttpConfigurer 를 상속하여 구현 - // AbstractHttpConfigurer 을 지정 + // AbstractHttpConfigurer 를 상속하여 구현 + // AbstractHttpConfigurer 을 지정 - // configure() 메서드를 오버라이드해서 Configuration 을 커스터마이징 + // configure() 메서드를 오버라이드해서 Configuration 을 커스터마이징 // @Override // public void configure(HttpSecurity builder) throws Exception { // From f5aced9e72699daaa9d09c6f88ba4ba8920a06af Mon Sep 17 00:00:00 2001 From: FaberJoo Date: Thu, 16 Nov 2023 20:49:03 +0900 Subject: [PATCH 3/5] =?UTF-8?q?refactor:=20=EC=95=A0=EB=8B=88=EB=A9=94=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20db=20lock=20=EC=BF=BC=EB=A6=AC=20=EB=A9=94?= =?UTF-8?q?=EC=86=8C=EB=93=9C=20=EC=82=AD=EC=A0=9C=20=EC=95=A0=EB=8B=88=20?= =?UTF-8?q?=EC=A1=B0=EA=B1=B4=20=EC=B6=94=EA=B0=80=20#111?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../io/oduck/api/domain/anime/repository/AnimeRepository.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/io/oduck/api/domain/anime/repository/AnimeRepository.java b/src/main/java/io/oduck/api/domain/anime/repository/AnimeRepository.java index bcc7ae75..e7bba382 100644 --- a/src/main/java/io/oduck/api/domain/anime/repository/AnimeRepository.java +++ b/src/main/java/io/oduck/api/domain/anime/repository/AnimeRepository.java @@ -14,11 +14,11 @@ @Repository public interface AnimeRepository extends JpaRepository, AnimeRepositoryCustom { - @Query("select a from Anime a where a.id = :id") + @Query("select a from Anime a where a.id = :id and a.deletedAt is null") @Lock(LockModeType.PESSIMISTIC_WRITE) @QueryHints({@QueryHint(name = "javax.persistence.lock.timeout", value ="3000")}) Optional findByIdForUpdate(@Param("id")Long id); - + @Query("select a from Anime a where a.id = :id and a.deletedAt = null and a.isReleased = :isReleased") Optional findAnimeByConditions(@Param("id") Long id, @Param("isReleased") boolean isReleased); From a835f3bdcedbce817db5bb676c5dd33fe0f35f75 Mon Sep 17 00:00:00 2001 From: FaberJoo Date: Thu, 16 Nov 2023 20:53:23 +0900 Subject: [PATCH 4/5] =?UTF-8?q?refactor:=20=EB=B3=84=EC=A0=90=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=EC=8B=9C=20=EC=95=A0=EB=8B=88=EB=A9=94=20=ED=86=A0?= =?UTF-8?q?=ED=83=88=20=ED=8F=89=EC=A0=90=20=EC=88=98=EC=A0=95=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=B6=94=EA=B0=80=20#111?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../starRating/service/StarRatingServiceImpl.java | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/main/java/io/oduck/api/domain/starRating/service/StarRatingServiceImpl.java b/src/main/java/io/oduck/api/domain/starRating/service/StarRatingServiceImpl.java index 71314ec9..7823f04f 100644 --- a/src/main/java/io/oduck/api/domain/starRating/service/StarRatingServiceImpl.java +++ b/src/main/java/io/oduck/api/domain/starRating/service/StarRatingServiceImpl.java @@ -33,7 +33,7 @@ public boolean createScore(Long memberId, Long animeId, int score) { Member member = memberRepository.findById(memberId) .orElseThrow(() -> new NotFoundException("Member")); - Anime anime = animeRepository.findById(animeId) + Anime anime = animeRepository.findByIdForUpdate(animeId) .orElseThrow(() -> new NotFoundException("Anime")); anime.increaseStarRatingScore(score); @@ -60,15 +60,24 @@ public RatedRes checkRated(Long memberId, Long animeId) { } @Override + @Transactional public boolean updateScore(Long memberId, Long animeId, int score) { StarRating foundStarRating = findByMemberIdAndAnimeId(memberId, animeId) .orElseThrow(() -> new NotFoundException("StarRating")); - if (foundStarRating.getScore() == score) { + int prevScore = foundStarRating.getScore(); + + Anime anime = animeRepository.findByIdForUpdate(animeId) + .orElseThrow(() -> new NotFoundException("Anime")); + + if (prevScore == score) { return false; } + anime.decreaseStarRatingScore(prevScore); + foundStarRating.updateScore(score); + anime.increaseStarRatingScore(score); starRatingRepository.save(foundStarRating); return true; From 47197fcf3668b4a154c231939f9535c85429a8c0 Mon Sep 17 00:00:00 2001 From: FaberJoo Date: Thu, 16 Nov 2023 21:01:10 +0900 Subject: [PATCH 5/5] =?UTF-8?q?test:=20=EB=B3=84=EC=A0=90=20=EC=84=9C?= =?UTF-8?q?=EB=B9=84=EC=8A=A4=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20db=20lock=20?= =?UTF-8?q?=EC=BF=BC=EB=A6=AC=20=EB=A9=94=EC=86=8C=EB=93=9C=20=EB=B0=98?= =?UTF-8?q?=EC=98=81=20#111?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/unit/starRating/service/StarRatingServiceTest.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/test/java/io/oduck/api/unit/starRating/service/StarRatingServiceTest.java b/src/test/java/io/oduck/api/unit/starRating/service/StarRatingServiceTest.java index 2d3e40d7..ac0d412e 100644 --- a/src/test/java/io/oduck/api/unit/starRating/service/StarRatingServiceTest.java +++ b/src/test/java/io/oduck/api/unit/starRating/service/StarRatingServiceTest.java @@ -63,7 +63,7 @@ void createScore() { .willReturn(Optional.empty()); given(memberRepository.findById(memberId)) .willReturn(Optional.ofNullable(member)); - given(animeRepository.findById(animeId)) + given(animeRepository.findByIdForUpdate(animeId)) .willReturn(Optional.ofNullable(anime)); // when @@ -173,6 +173,8 @@ void updateScore() { given(starRatingRepository.findByMemberIdAndAnimeId(memberId, animeId)) .willReturn(Optional.ofNullable(starRating)); + given(animeRepository.findByIdForUpdate(animeId)) + .willReturn(Optional.ofNullable(anime)); // when boolean result = starRatingService.updateScore(memberId, animeId, 5); @@ -194,6 +196,8 @@ void updateScoreIfNotExist() { given(starRatingRepository.findByMemberIdAndAnimeId(memberId, animeId)) .willReturn(Optional.ofNullable(starRating)); + given(animeRepository.findByIdForUpdate(animeId)) + .willReturn(Optional.ofNullable(anime)); // when boolean result = starRatingService.updateScore(memberId, animeId, score);