From 45a80a3d712ba81760473e4c420ebcec2fbbf768 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Fri, 14 Feb 2025 12:12:31 +0900 Subject: [PATCH 1/4] =?UTF-8?q?fix:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=EC=9D=B4=20=EC=84=A0=ED=83=9D=EC=A0=81?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=ED=95=84=EC=9A=94=ED=95=9C=20=EA=B3=B3?= =?UTF-8?q?=EC=9D=84=20=EB=AA=85=EC=8B=9C=ED=95=98=EB=8F=84=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../custom/resolver/AuthorizedUser.java | 1 + .../resolver/AuthorizedUserResolver.java | 20 +++++++++++++++---- .../controller/UniversityController.java | 2 +- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/example/solidconnection/custom/resolver/AuthorizedUser.java b/src/main/java/com/example/solidconnection/custom/resolver/AuthorizedUser.java index b14d80994..fa1db7f74 100644 --- a/src/main/java/com/example/solidconnection/custom/resolver/AuthorizedUser.java +++ b/src/main/java/com/example/solidconnection/custom/resolver/AuthorizedUser.java @@ -8,4 +8,5 @@ @Target({ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) public @interface AuthorizedUser { + boolean required() default true; } diff --git a/src/main/java/com/example/solidconnection/custom/resolver/AuthorizedUserResolver.java b/src/main/java/com/example/solidconnection/custom/resolver/AuthorizedUserResolver.java index 93707b007..f4ba9fe7f 100644 --- a/src/main/java/com/example/solidconnection/custom/resolver/AuthorizedUserResolver.java +++ b/src/main/java/com/example/solidconnection/custom/resolver/AuthorizedUserResolver.java @@ -1,9 +1,11 @@ package com.example.solidconnection.custom.resolver; +import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.custom.security.userdetails.SiteUserDetails; import com.example.solidconnection.siteuser.domain.SiteUser; import lombok.RequiredArgsConstructor; import org.springframework.core.MethodParameter; +import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; import org.springframework.web.bind.support.WebDataBinderFactory; @@ -11,6 +13,8 @@ import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.ModelAndViewContainer; +import static com.example.solidconnection.custom.exception.ErrorCode.AUTHENTICATION_FAILED; + @Component @RequiredArgsConstructor public class AuthorizedUserResolver implements HandlerMethodArgumentResolver { @@ -25,11 +29,19 @@ public boolean supportsParameter(MethodParameter parameter) { public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, - WebDataBinderFactory binderFactory) throws Exception { + WebDataBinderFactory binderFactory) { + SiteUser siteUser = extractSiteUserFromAuthentication(); + if (parameter.getParameterAnnotation(AuthorizedUser.class).required() && siteUser == null) { + throw new CustomException(AUTHENTICATION_FAILED, "로그인 상태가 아닙니다."); + } + + return siteUser; + } + + private SiteUser extractSiteUserFromAuthentication() { try { - SiteUserDetails principal = (SiteUserDetails) SecurityContextHolder.getContext() - .getAuthentication() - .getPrincipal(); + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + SiteUserDetails principal = (SiteUserDetails) authentication.getPrincipal(); return principal.getSiteUser(); } catch (Exception e) { return null; diff --git a/src/main/java/com/example/solidconnection/university/controller/UniversityController.java b/src/main/java/com/example/solidconnection/university/controller/UniversityController.java index 66da87095..83d90f600 100644 --- a/src/main/java/com/example/solidconnection/university/controller/UniversityController.java +++ b/src/main/java/com/example/solidconnection/university/controller/UniversityController.java @@ -36,7 +36,7 @@ public class UniversityController { @GetMapping("/recommend") public ResponseEntity getUniversityRecommends( - @AuthorizedUser SiteUser siteUser + @AuthorizedUser(required = false) SiteUser siteUser ) { if (siteUser == null) { return ResponseEntity.ok(universityRecommendService.getGeneralRecommends()); From 566680e808bb36e0a8874ec0c43f7920cbd78ed8 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Fri, 14 Feb 2025 12:12:54 +0900 Subject: [PATCH 2/4] =?UTF-8?q?fix:=20=EB=A7=8C=EB=A3=8C=EB=90=9C=20?= =?UTF-8?q?=ED=86=A0=ED=81=B0=20=EA=B0=9D=EC=B2=B4=EA=B0=80=20=EC=95=84?= =?UTF-8?q?=EB=8B=88=EB=9D=BC=20=ED=86=A0=ED=81=B0=20=EC=9E=90=EC=B2=B4?= =?UTF-8?q?=EB=A5=BC=20=EC=82=AC=EC=9A=A9=ED=95=98=EB=8F=84=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/controller/AuthController.java | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/example/solidconnection/auth/controller/AuthController.java b/src/main/java/com/example/solidconnection/auth/controller/AuthController.java index 80520942f..9c84e8d22 100644 --- a/src/main/java/com/example/solidconnection/auth/controller/AuthController.java +++ b/src/main/java/com/example/solidconnection/auth/controller/AuthController.java @@ -16,14 +16,15 @@ import com.example.solidconnection.auth.service.oauth.AppleOAuthService; import com.example.solidconnection.auth.service.oauth.KakaoOAuthService; import com.example.solidconnection.auth.service.oauth.OAuthSignUpService; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.custom.exception.ErrorCode; import com.example.solidconnection.custom.resolver.AuthorizedUser; -import com.example.solidconnection.custom.resolver.ExpiredToken; -import com.example.solidconnection.custom.security.authentication.ExpiredTokenAuthentication; import com.example.solidconnection.siteuser.domain.AuthType; import com.example.solidconnection.siteuser.domain.SiteUser; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -93,9 +94,13 @@ public ResponseEntity signUp( @PostMapping("/sign-out") public ResponseEntity signOut( - @ExpiredToken ExpiredTokenAuthentication expiredToken + Authentication authentication ) { - authService.signOut(expiredToken.getToken()); + String token = authentication.getCredentials().toString(); + if (token == null) { + throw new CustomException(ErrorCode.AUTHENTICATION_FAILED, "토큰이 없습니다."); + } + authService.signOut(token); return ResponseEntity.ok().build(); } @@ -109,9 +114,13 @@ public ResponseEntity quit( @PostMapping("/reissue") public ResponseEntity reissueToken( - @ExpiredToken ExpiredTokenAuthentication expiredToken + Authentication authentication ) { - ReissueResponse reissueResponse = authService.reissue(expiredToken.getSubject()); + String token = authentication.getCredentials().toString(); + if (token == null) { + throw new CustomException(ErrorCode.AUTHENTICATION_FAILED, "토큰이 없습니다."); + } + ReissueResponse reissueResponse = authService.reissue(token); return ResponseEntity.ok(reissueResponse); } } From 2bb5ff275d3c28dc78871a9cc86dcdedf62aaf02 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Fri, 14 Feb 2025 12:26:09 +0900 Subject: [PATCH 3/4] =?UTF-8?q?test:=20=EC=9D=B8=EC=A6=9D=EB=90=9C=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=EC=9E=90=20=EB=A6=AC=EC=A1=B8=EB=B2=84=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EB=B3=B4=EA=B0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resolver/AuthorizedUserResolverTest.java | 66 +++++++++++++++---- 1 file changed, 55 insertions(+), 11 deletions(-) diff --git a/src/test/java/com/example/solidconnection/custom/resolver/AuthorizedUserResolverTest.java b/src/test/java/com/example/solidconnection/custom/resolver/AuthorizedUserResolverTest.java index 763fdf101..779474c27 100644 --- a/src/test/java/com/example/solidconnection/custom/resolver/AuthorizedUserResolverTest.java +++ b/src/test/java/com/example/solidconnection/custom/resolver/AuthorizedUserResolverTest.java @@ -1,6 +1,7 @@ package com.example.solidconnection.custom.resolver; +import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.custom.security.authentication.SiteUserAuthentication; import com.example.solidconnection.custom.security.userdetails.SiteUserDetails; import com.example.solidconnection.siteuser.domain.SiteUser; @@ -11,11 +12,18 @@ import com.example.solidconnection.type.Role; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.MethodParameter; +import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; +import static com.example.solidconnection.custom.exception.ErrorCode.AUTHENTICATION_FAILED; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; @TestContainerSpringBootTest @DisplayName("인증된 사용자 argument resolver 테스트") @@ -33,28 +41,58 @@ void setUp() { } @Test - void security_context_에_저장된_인증된_사용자를_반환한다() throws Exception { + void security_context_에_저장된_인증된_사용자를_반환한다() { // given - SiteUser siteUser = siteUserRepository.save(createSiteUser()); - SiteUserDetails userDetails = new SiteUserDetails(siteUser); - SiteUserAuthentication authentication = new SiteUserAuthentication("token", userDetails); + SiteUser siteUser = createAndSaveSiteUser(); + Authentication authentication = createAuthenticationWithUser(siteUser); SecurityContextHolder.getContext().setAuthentication(authentication); + MethodParameter parameter = mock(MethodParameter.class); + AuthorizedUser authorizedUser = mock(AuthorizedUser.class); + given(parameter.getParameterAnnotation(AuthorizedUser.class)).willReturn(authorizedUser); + given(authorizedUser.required()).willReturn(false); + // when - SiteUser resolveSiteUser = (SiteUser) authorizedUserResolver.resolveArgument(null, null, null, null); + SiteUser resolveSiteUser = (SiteUser) authorizedUserResolver.resolveArgument(parameter, null, null, null); // then assertThat(resolveSiteUser).isEqualTo(siteUser); } - @Test - void security_context_에_저장된_사용자가_없으면_null_을_반환한다() throws Exception { - // when, then - assertThat(authorizedUserResolver.resolveArgument(null, null, null, null)).isNull(); + @Nested + class security_context_에_저장된_사용자가_없는_경우 { + + @Test + void required_가_true_이면_예외_응답을_반환한다() { + // given + MethodParameter parameter = mock(MethodParameter.class); + AuthorizedUser authorizedUser = mock(AuthorizedUser.class); + given(parameter.getParameterAnnotation(AuthorizedUser.class)).willReturn(authorizedUser); + given(authorizedUser.required()).willReturn(true); + + // when, then + assertThatCode(() -> authorizedUserResolver.resolveArgument(parameter, null, null, null)) + .isInstanceOf(CustomException.class) + .hasMessageContaining(AUTHENTICATION_FAILED.getMessage()); + } + + @Test + void required_가_false_이면_null_을_반환한다() { + // given + MethodParameter parameter = mock(MethodParameter.class); + AuthorizedUser authorizedUser = mock(AuthorizedUser.class); + given(parameter.getParameterAnnotation(AuthorizedUser.class)).willReturn(authorizedUser); + given(authorizedUser.required()).willReturn(false); + + // when, then + assertThat( + authorizedUserResolver.resolveArgument(parameter, null, null, null) + ).isNull(); + } } - private SiteUser createSiteUser() { - return new SiteUser( + private SiteUser createAndSaveSiteUser() { + SiteUser siteUser = new SiteUser( "test@example.com", "nickname", "profileImageUrl", @@ -63,5 +101,11 @@ private SiteUser createSiteUser() { Role.MENTEE, Gender.MALE ); + return siteUserRepository.save(siteUser); + } + + private SiteUserAuthentication createAuthenticationWithUser(SiteUser siteUser) { + SiteUserDetails userDetails = new SiteUserDetails(siteUser); + return new SiteUserAuthentication("token", userDetails); } } From e346b39270be6c686235048fe98d92f7512ed0b3 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Fri, 14 Feb 2025 12:27:36 +0900 Subject: [PATCH 4/4] =?UTF-8?q?chore:=20=EC=9E=91=EC=97=85=ED=95=98?= =?UTF-8?q?=EB=A9=B0=20=EB=B0=9C=EA=B2=AC=ED=95=9C=20=EA=B0=9C=EC=84=A0?= =?UTF-8?q?=EC=9D=B4=20=ED=95=84=EC=9A=94=ED=95=9C=20=EB=B6=80=EB=B6=84=20?= =?UTF-8?q?=ED=91=9C=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/solidconnection/custom/resolver/ExpiredToken.java | 1 + .../solidconnection/custom/resolver/ExpiredTokenResolver.java | 1 + .../security/authentication/ExpiredTokenAuthentication.java | 1 + .../security/provider/ExpiredTokenAuthenticationProvider.java | 1 + 4 files changed, 4 insertions(+) diff --git a/src/main/java/com/example/solidconnection/custom/resolver/ExpiredToken.java b/src/main/java/com/example/solidconnection/custom/resolver/ExpiredToken.java index 61abff98c..5de4ad95a 100644 --- a/src/main/java/com/example/solidconnection/custom/resolver/ExpiredToken.java +++ b/src/main/java/com/example/solidconnection/custom/resolver/ExpiredToken.java @@ -5,6 +5,7 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +// todo: 사용되지 않음, 다른 PR에서 삭제하고 더 효율적인 구조를 고민해봐야 함 @Target({ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) public @interface ExpiredToken { diff --git a/src/main/java/com/example/solidconnection/custom/resolver/ExpiredTokenResolver.java b/src/main/java/com/example/solidconnection/custom/resolver/ExpiredTokenResolver.java index 691136438..7547a1d61 100644 --- a/src/main/java/com/example/solidconnection/custom/resolver/ExpiredTokenResolver.java +++ b/src/main/java/com/example/solidconnection/custom/resolver/ExpiredTokenResolver.java @@ -10,6 +10,7 @@ import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.ModelAndViewContainer; +// todo: 사용되지 않음, 다른 PR에서 삭제하고 더 효율적인 구조를 고민해봐야 함 @Component @RequiredArgsConstructor public class ExpiredTokenResolver implements HandlerMethodArgumentResolver { diff --git a/src/main/java/com/example/solidconnection/custom/security/authentication/ExpiredTokenAuthentication.java b/src/main/java/com/example/solidconnection/custom/security/authentication/ExpiredTokenAuthentication.java index 811ea6a1b..061484674 100644 --- a/src/main/java/com/example/solidconnection/custom/security/authentication/ExpiredTokenAuthentication.java +++ b/src/main/java/com/example/solidconnection/custom/security/authentication/ExpiredTokenAuthentication.java @@ -1,5 +1,6 @@ package com.example.solidconnection.custom.security.authentication; +// todo: 사용되지 않음, 다른 PR에서 삭제하고 더 효율적인 구조를 고민해봐야 함 public class ExpiredTokenAuthentication extends JwtAuthentication { public ExpiredTokenAuthentication(String token) { diff --git a/src/main/java/com/example/solidconnection/custom/security/provider/ExpiredTokenAuthenticationProvider.java b/src/main/java/com/example/solidconnection/custom/security/provider/ExpiredTokenAuthenticationProvider.java index d7461a0e6..01b065a19 100644 --- a/src/main/java/com/example/solidconnection/custom/security/provider/ExpiredTokenAuthenticationProvider.java +++ b/src/main/java/com/example/solidconnection/custom/security/provider/ExpiredTokenAuthenticationProvider.java @@ -12,6 +12,7 @@ import static com.example.solidconnection.util.JwtUtils.parseSubjectIgnoringExpiration; +// todo: 사용되지 않음, 다른 PR에서 삭제하고 더 효율적인 구조를 고민해봐야 함 @Component @RequiredArgsConstructor public class ExpiredTokenAuthenticationProvider implements AuthenticationProvider {