From 9b8e47aed365d2d0822ccf756d00826c0b9060a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EC=A4=80=ED=98=B8?= Date: Wed, 10 Jan 2024 11:29:24 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20logout=20=EA=B5=AC=ED=98=84=20(#154)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../koreatech/koin/domain/auth/UserAuth.java | 14 +++++ .../auth/resolver/UserArgumentResolver.java | 48 +++++++++++++++++ .../user/controller/UserController.java | 20 ++++--- .../user/repository/UserTokenRepository.java | 6 +-- .../koin/domain/user/service/UserService.java | 21 ++++---- .../koin/global/config/WebConfig.java | 3 ++ .../koin/acceptance/AuthApiTest.java | 53 +++++++++++++++++++ 7 files changed, 146 insertions(+), 19 deletions(-) create mode 100644 src/main/java/in/koreatech/koin/domain/auth/UserAuth.java create mode 100644 src/main/java/in/koreatech/koin/domain/auth/resolver/UserArgumentResolver.java diff --git a/src/main/java/in/koreatech/koin/domain/auth/UserAuth.java b/src/main/java/in/koreatech/koin/domain/auth/UserAuth.java new file mode 100644 index 000000000..ec28859f6 --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/auth/UserAuth.java @@ -0,0 +1,14 @@ +package in.koreatech.koin.domain.auth; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Target({PARAMETER, FIELD}) +@Retention(RUNTIME) +public @interface UserAuth { + +} diff --git a/src/main/java/in/koreatech/koin/domain/auth/resolver/UserArgumentResolver.java b/src/main/java/in/koreatech/koin/domain/auth/resolver/UserArgumentResolver.java new file mode 100644 index 000000000..d44e59c78 --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/auth/resolver/UserArgumentResolver.java @@ -0,0 +1,48 @@ +package in.koreatech.koin.domain.auth.resolver; + +import in.koreatech.koin.domain.auth.JwtProvider; +import in.koreatech.koin.domain.auth.UserAuth; +import in.koreatech.koin.domain.auth.exception.AuthException; +import in.koreatech.koin.domain.user.exception.UserNotFoundException; +import in.koreatech.koin.domain.user.repository.UserRepository; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.core.MethodParameter; +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; + +@Component +@RequiredArgsConstructor +public class UserArgumentResolver implements HandlerMethodArgumentResolver { + + private static final String AUTHORIZATION = "Authorization"; + + private final JwtProvider jwtProvider; + private final UserRepository userRepository; + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(UserAuth.class); + } + + @Override + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { + + HttpServletRequest nativeRequest = webRequest.getNativeRequest(HttpServletRequest.class); + if (nativeRequest == null) { + throw new AuthException("요청 값이 비어있습니다."); + } + + String authorizationHeader = nativeRequest.getHeader(AUTHORIZATION); + if (authorizationHeader == null) { + throw new AuthException("인증 헤더값이 비어있습니다. authorizationHeader: " + nativeRequest); + } + Long userId = jwtProvider.getUserId(authorizationHeader); + return userRepository.findById(userId) + .orElseThrow(() -> UserNotFoundException.withDetail("authorizationHeader: " + authorizationHeader)); + } +} diff --git a/src/main/java/in/koreatech/koin/domain/user/controller/UserController.java b/src/main/java/in/koreatech/koin/domain/user/controller/UserController.java index d8cd202af..a98dac507 100644 --- a/src/main/java/in/koreatech/koin/domain/user/controller/UserController.java +++ b/src/main/java/in/koreatech/koin/domain/user/controller/UserController.java @@ -1,17 +1,17 @@ package in.koreatech.koin.domain.user.controller; -import java.net.URI; - -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RestController; - +import in.koreatech.koin.domain.auth.UserAuth; import in.koreatech.koin.domain.user.dto.UserLoginRequest; import in.koreatech.koin.domain.user.dto.UserLoginResponse; +import in.koreatech.koin.domain.user.model.User; import in.koreatech.koin.domain.user.service.UserService; import jakarta.validation.Valid; +import java.net.URI; import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; @RestController @RequiredArgsConstructor @@ -25,4 +25,10 @@ public ResponseEntity login(@RequestBody @Valid UserLoginRequ return ResponseEntity.created(URI.create("/")) .body(response); } + + @PostMapping("/user/logout") + public ResponseEntity logout(@UserAuth User user) { + userService.logout(user); + return ResponseEntity.ok().build(); + } } diff --git a/src/main/java/in/koreatech/koin/domain/user/repository/UserTokenRepository.java b/src/main/java/in/koreatech/koin/domain/user/repository/UserTokenRepository.java index 562674d81..1af8fdd11 100644 --- a/src/main/java/in/koreatech/koin/domain/user/repository/UserTokenRepository.java +++ b/src/main/java/in/koreatech/koin/domain/user/repository/UserTokenRepository.java @@ -1,14 +1,14 @@ package in.koreatech.koin.domain.user.repository; +import in.koreatech.koin.domain.user.model.UserToken; import java.util.Optional; - import org.springframework.data.repository.Repository; -import in.koreatech.koin.domain.user.model.UserToken; - public interface UserTokenRepository extends Repository { UserToken save(UserToken userToken); Optional findById(Long userId); + + void deleteById(Long id); } diff --git a/src/main/java/in/koreatech/koin/domain/user/service/UserService.java b/src/main/java/in/koreatech/koin/domain/user/service/UserService.java index 1722a93a6..d5a6e427c 100644 --- a/src/main/java/in/koreatech/koin/domain/user/service/UserService.java +++ b/src/main/java/in/koreatech/koin/domain/user/service/UserService.java @@ -1,20 +1,18 @@ package in.koreatech.koin.domain.user.service; -import in.koreatech.koin.domain.user.exception.UserNotFoundException; -import java.time.LocalDateTime; -import java.util.UUID; - -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - import in.koreatech.koin.domain.auth.JwtProvider; -import in.koreatech.koin.domain.user.model.User; -import in.koreatech.koin.domain.user.model.UserToken; import in.koreatech.koin.domain.user.dto.UserLoginRequest; import in.koreatech.koin.domain.user.dto.UserLoginResponse; +import in.koreatech.koin.domain.user.exception.UserNotFoundException; +import in.koreatech.koin.domain.user.model.User; +import in.koreatech.koin.domain.user.model.UserToken; import in.koreatech.koin.domain.user.repository.UserRepository; import in.koreatech.koin.domain.user.repository.UserTokenRepository; +import java.time.LocalDateTime; +import java.util.UUID; import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor @@ -42,4 +40,9 @@ public UserLoginResponse login(UserLoginRequest request) { return UserLoginResponse.of(accessToken, savedToken.getRefreshToken(), saved.getUserType().getValue()); } + + @Transactional + public void logout(User user) { + userTokenRepository.deleteById(user.getId()); + } } diff --git a/src/main/java/in/koreatech/koin/global/config/WebConfig.java b/src/main/java/in/koreatech/koin/global/config/WebConfig.java index fc3c6f95e..9fee48067 100644 --- a/src/main/java/in/koreatech/koin/global/config/WebConfig.java +++ b/src/main/java/in/koreatech/koin/global/config/WebConfig.java @@ -1,6 +1,7 @@ package in.koreatech.koin.global.config; import in.koreatech.koin.domain.auth.resolver.StudentArgumentResolver; +import in.koreatech.koin.domain.auth.resolver.UserArgumentResolver; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Configuration; @@ -11,10 +12,12 @@ @RequiredArgsConstructor public class WebConfig implements WebMvcConfigurer { + private final UserArgumentResolver userArgumentResolver; private final StudentArgumentResolver studentArgumentResolver; @Override public void addArgumentResolvers(final List resolvers) { + resolvers.add(userArgumentResolver); resolvers.add(studentArgumentResolver); } } diff --git a/src/test/java/in/koreatech/koin/acceptance/AuthApiTest.java b/src/test/java/in/koreatech/koin/acceptance/AuthApiTest.java index c3f4073fb..e4befb0f1 100644 --- a/src/test/java/in/koreatech/koin/acceptance/AuthApiTest.java +++ b/src/test/java/in/koreatech/koin/acceptance/AuthApiTest.java @@ -12,6 +12,8 @@ import io.restassured.http.ContentType; import io.restassured.response.ExtractableResponse; import io.restassured.response.Response; +import java.util.Optional; +import org.assertj.core.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -73,4 +75,55 @@ void userLoginSuccess() { } ); } + + @Test + @DisplayName("사용자가 로그인 이후 로그아웃을 수행한다") + void userLogoutSuccess() { + User user = User.builder() + .password("1234") + .nickname("주노") + .name("최준호") + .phoneNumber("010-1234-5678") + .userType(UserType.USER) + .email("test@koreatech.ac.kr") + .isAuthed(true) + .isDeleted(false) + .build(); + + userRepository.save(user); + + ExtractableResponse response = RestAssured + .given() + .log().all() + .body(""" + { + "email": "test@koreatech.ac.kr", + "password": "1234" + } + """) + .contentType(ContentType.JSON) + .when() + .log().all() + .post("/user/login") + .then() + .log().all() + .statusCode(HttpStatus.CREATED.value()) + .extract(); + + RestAssured + .given() + .log().all() + .header("Authorization", "BEARER " + response.jsonPath().getString("token")) + .when() + .log().all() + .post("/user/logout") + .then() + .log().all() + .statusCode(HttpStatus.OK.value()) + .extract(); + + Optional token = tokenRepository.findById(user.getId()); + + Assertions.assertThat(token).isEmpty(); + } }