Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -93,9 +94,13 @@ public ResponseEntity<SignInResponse> signUp(

@PostMapping("/sign-out")
public ResponseEntity<Void> 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();
Comment on lines 95 to 104
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JwtAuthetication 에서 credential 로 토큰 자체를 저장하게 해서
컨트롤러에서 인자로 쉽게 가져올 수 있었습니다~

}

Expand All @@ -109,9 +114,13 @@ public ResponseEntity<Void> quit(

@PostMapping("/reissue")
public ResponseEntity<ReissueResponse> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface AuthorizedUser {
boolean required() default true;
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
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;
import org.springframework.web.context.request.NativeWebRequest;
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 {
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.example.solidconnection.custom.security.authentication;

// todo: 사용되지 않음, 다른 PR에서 삭제하고 더 효율적인 구조를 고민해봐야 함
public class ExpiredTokenAuthentication extends JwtAuthentication {
Comment on lines +3 to 4
Copy link
Collaborator Author

@nayonsoso nayonsoso Feb 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이건 좀 고민이 필요할 것 같아서.. 😭 일단 fix PR만 먼저 올립니다!


public ExpiredTokenAuthentication(String token) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import static com.example.solidconnection.util.JwtUtils.parseSubjectIgnoringExpiration;

// todo: 사용되지 않음, 다른 PR에서 삭제하고 더 효율적인 구조를 고민해봐야 함
@Component
@RequiredArgsConstructor
public class ExpiredTokenAuthenticationProvider implements AuthenticationProvider {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public class UniversityController {

@GetMapping("/recommend")
public ResponseEntity<UniversityRecommendsResponse> getUniversityRecommends(
@AuthorizedUser SiteUser siteUser
@AuthorizedUser(required = false) SiteUser siteUser
) {
if (siteUser == null) {
return ResponseEntity.ok(universityRecommendService.getGeneralRecommends());
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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 테스트")
Expand All @@ -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(
"[email protected]",
"nickname",
"profileImageUrl",
Expand All @@ -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);
}
}