diff --git a/doorip-api/src/main/java/org/doorip/user/api/UserApiController.java b/doorip-api/src/main/java/org/doorip/user/api/UserApiController.java index 52244e3..f0154bc 100644 --- a/doorip-api/src/main/java/org/doorip/user/api/UserApiController.java +++ b/doorip-api/src/main/java/org/doorip/user/api/UserApiController.java @@ -5,6 +5,7 @@ import org.doorip.common.ApiResponse; import org.doorip.common.ApiResponseUtil; import org.doorip.message.SuccessMessage; +import org.doorip.user.dto.request.UserReissueRequest; import org.doorip.user.dto.request.UserSignInRequest; import org.doorip.user.dto.request.UserSignUpRequest; import org.doorip.user.dto.response.UserResponse; @@ -13,6 +14,8 @@ import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; +import static org.doorip.common.Constants.AUTHORIZATION; + @RequiredArgsConstructor @RequestMapping("/api/users") @Controller @@ -20,14 +23,14 @@ public class UserApiController { private final UserService userService; @PostMapping("/signin") - public ResponseEntity> signIn(@RequestHeader("Authorization") final String token, + public ResponseEntity> signIn(@RequestHeader(AUTHORIZATION) final String token, @RequestBody final UserSignInRequest request) { final UserResponse response = userService.signIn(token, request); return ApiResponseUtil.success(SuccessMessage.OK, response); } @PostMapping("/signup") - public ResponseEntity> signUp(@RequestHeader("Authorization") final String token, + public ResponseEntity> signUp(@RequestHeader(AUTHORIZATION) final String token, @RequestBody final UserSignUpRequest request) { final UserResponse response = userService.signUp(token, request); return ApiResponseUtil.success(SuccessMessage.CREATED, response); @@ -44,4 +47,11 @@ public ResponseEntity> withdraw(@UserId final Long userId) { userService.withdraw(userId); return ApiResponseUtil.success(SuccessMessage.OK); } + + @PostMapping("/reissue") + public ResponseEntity> reissue(@RequestHeader(AUTHORIZATION) final String refreshtoken, + @RequestBody final UserReissueRequest request) { + UserResponse response = userService.reissue(refreshtoken, request); + return ApiResponseUtil.success(SuccessMessage.OK, response); + } } diff --git a/doorip-api/src/main/java/org/doorip/user/dto/request/UserReissueRequest.java b/doorip-api/src/main/java/org/doorip/user/dto/request/UserReissueRequest.java new file mode 100644 index 0000000..793e159 --- /dev/null +++ b/doorip-api/src/main/java/org/doorip/user/dto/request/UserReissueRequest.java @@ -0,0 +1,6 @@ +package org.doorip.user.dto.request; + +public record UserReissueRequest( + Long userId +) { +} diff --git a/doorip-api/src/main/java/org/doorip/user/service/UserService.java b/doorip-api/src/main/java/org/doorip/user/service/UserService.java index 24c85f0..0fcbee8 100644 --- a/doorip-api/src/main/java/org/doorip/user/service/UserService.java +++ b/doorip-api/src/main/java/org/doorip/user/service/UserService.java @@ -2,14 +2,17 @@ import lombok.RequiredArgsConstructor; import org.doorip.auth.jwt.JwtProvider; +import org.doorip.auth.jwt.JwtValidator; import org.doorip.auth.jwt.Token; import org.doorip.exception.EntityNotFoundException; +import org.doorip.exception.UnauthorizedException; import org.doorip.message.ErrorMessage; import org.doorip.openfeign.apple.AppleOAuthProvider; import org.doorip.openfeign.kakao.KakaoOAuthProvider; import org.doorip.user.domain.Platform; import org.doorip.user.domain.RefreshToken; import org.doorip.user.domain.User; +import org.doorip.user.dto.request.UserReissueRequest; import org.doorip.user.dto.request.UserSignInRequest; import org.doorip.user.dto.request.UserSignUpRequest; import org.doorip.user.dto.response.UserResponse; @@ -29,6 +32,7 @@ public class UserService { private final UserRepository userRepository; private final RefreshTokenRepository refreshTokenRepository; private final JwtProvider jwtProvider; + private final JwtValidator jwtValidator; private final AppleOAuthProvider appleOAuthProvider; private final KakaoOAuthProvider kakaoOAuthProvider; @@ -61,6 +65,17 @@ public void withdraw(Long userId) { userRepository.deleteById(userId); } + @Transactional(noRollbackFor = UnauthorizedException.class) + public UserResponse reissue(String refreshToken, UserReissueRequest request) { + Long userId = request.userId(); + validateRefreshToken(refreshToken, userId); + User findUser = getUser(userId); + Token issueToken = jwtProvider.issueToken(userId); + updateRefreshToken(issueToken.refreshToken(), findUser); + + return UserResponse.of(issueToken); + } + private String getPlatformId(String token, Platform platform) { if (platform == APPLE) { return appleOAuthProvider.getApplePlatformId(token); @@ -92,4 +107,30 @@ private void deleteRefreshToken(User user) { user.updateRefreshToken(null); refreshTokenRepository.deleteById(user.getId()); } + + private void validateRefreshToken(String refreshToken, Long userId) { + try { + jwtValidator.validateRefreshToken(refreshToken); + String storedRefreshToken = getRefreshToken(userId); + jwtValidator.equalsRefreshToken(refreshToken, storedRefreshToken); + } catch (UnauthorizedException e) { + signOut(userId); + throw e; + } + } + + private String getRefreshToken(Long userId) { + try { + return getRefreshTokenFromRedis(userId); + } catch (EntityNotFoundException e) { + User findUser = getUser(userId); + return findUser.getRefreshToken(); + } + } + + private String getRefreshTokenFromRedis(Long userId) { + RefreshToken storedRefreshToken = refreshTokenRepository.findById(userId) + .orElseThrow(() -> new EntityNotFoundException(ErrorMessage.REFRESH_TOKEN_NOT_FOUND)); + return storedRefreshToken.getRefreshToken(); + } } diff --git a/doorip-common/src/main/java/org/doorip/message/ErrorMessage.java b/doorip-common/src/main/java/org/doorip/message/ErrorMessage.java index 9391bdb..a5eebeb 100644 --- a/doorip-common/src/main/java/org/doorip/message/ErrorMessage.java +++ b/doorip-common/src/main/java/org/doorip/message/ErrorMessage.java @@ -42,6 +42,7 @@ public enum ErrorMessage { */ ENTITY_NOT_FOUND(HttpStatus.NOT_FOUND, "e4040", "대상을 찾을 수 없습니다."), USER_NOT_FOUND(HttpStatus.NOT_FOUND, "e4041", "존재하지 않는 회원입니다."), + REFRESH_TOKEN_NOT_FOUND(HttpStatus.NOT_FOUND, "e4042", "리프레쉬 토큰을 찾을 수 없습니다."), /** * 405 Method Not Allowed