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); } } 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/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 { 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()); 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); } }