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 a98dac507..073efcd0d 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 @@ -3,6 +3,8 @@ 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.dto.UserTokenRefreshRequest; +import in.koreatech.koin.domain.user.dto.UserTokenRefreshResponse; import in.koreatech.koin.domain.user.model.User; import in.koreatech.koin.domain.user.service.UserService; import jakarta.validation.Valid; @@ -31,4 +33,12 @@ public ResponseEntity logout(@UserAuth User user) { userService.logout(user); return ResponseEntity.ok().build(); } + + @PostMapping("/user/refresh") + public ResponseEntity refresh( + @RequestBody @Valid UserTokenRefreshRequest request + ) { + UserTokenRefreshResponse tokenGroupResponse = userService.refresh(request); + return ResponseEntity.ok().body(tokenGroupResponse); + } } diff --git a/src/main/java/in/koreatech/koin/domain/user/dto/UserTokenRefreshRequest.java b/src/main/java/in/koreatech/koin/domain/user/dto/UserTokenRefreshRequest.java new file mode 100644 index 000000000..c2955a01d --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/user/dto/UserTokenRefreshRequest.java @@ -0,0 +1,13 @@ +package in.koreatech.koin.domain.user.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import jakarta.validation.constraints.NotNull; + +@JsonNaming(value = SnakeCaseStrategy.class) +public record UserTokenRefreshRequest( + @JsonProperty("refresh_token") @NotNull(message = "refresh_token을 입력해주세요.") String refreshToken +) { + +} diff --git a/src/main/java/in/koreatech/koin/domain/user/dto/UserTokenRefreshResponse.java b/src/main/java/in/koreatech/koin/domain/user/dto/UserTokenRefreshResponse.java new file mode 100644 index 000000000..86c7f793f --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/user/dto/UserTokenRefreshResponse.java @@ -0,0 +1,13 @@ +package in.koreatech.koin.domain.user.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public record UserTokenRefreshResponse( + @JsonProperty("token") String accessToken, + @JsonProperty("refresh_token") String refreshToken +) { + + public static UserTokenRefreshResponse of(String accessToken, String refreshToken) { + return new UserTokenRefreshResponse(accessToken, refreshToken); + } +} 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 1af8fdd11..0a3d15984 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 @@ -10,5 +10,7 @@ public interface UserTokenRepository extends Repository { Optional findById(Long userId); + Optional findByRefreshToken(String refreshToken); + 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 d5a6e427c..404114ff9 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,14 +1,18 @@ package in.koreatech.koin.domain.user.service; import in.koreatech.koin.domain.auth.JwtProvider; +import in.koreatech.koin.domain.auth.exception.AuthException; import in.koreatech.koin.domain.user.dto.UserLoginRequest; import in.koreatech.koin.domain.user.dto.UserLoginResponse; +import in.koreatech.koin.domain.user.dto.UserTokenRefreshRequest; +import in.koreatech.koin.domain.user.dto.UserTokenRefreshResponse; 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.Objects; import java.util.UUID; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -33,7 +37,7 @@ public UserLoginResponse login(UserLoginRequest request) { } String accessToken = jwtProvider.createToken(user); - String refreshToken = String.format("%s%d", UUID.randomUUID(), user.getId()); + String refreshToken = String.format("%s-%d", UUID.randomUUID(), user.getId()); UserToken savedToken = userTokenRepository.save(UserToken.create(user.getId(), refreshToken)); user.updateLastLoggedTime(LocalDateTime.now()); User saved = userRepository.save(user); @@ -45,4 +49,25 @@ public UserLoginResponse login(UserLoginRequest request) { public void logout(User user) { userTokenRepository.deleteById(user.getId()); } + + public UserTokenRefreshResponse refresh(UserTokenRefreshRequest request) { + String userId = getUserId(request.refreshToken()); + UserToken userToken = userTokenRepository.findById(Long.parseLong(userId)) + .orElseThrow(() -> new IllegalArgumentException("refresh token이 존재하지 않습니다. request: " + request)); + if (!Objects.equals(userToken.getRefreshToken(), request.refreshToken())) { + throw new IllegalArgumentException("refresh token이 일치하지 않습니다. request: " + request); + } + User user = userRepository.findById(userToken.getId()) + .orElseThrow(() -> new UserNotFoundException("사용자를 찾을 수 없습니다. refreshToken: " + userToken)); + String accessToken = jwtProvider.createToken(user); + return UserTokenRefreshResponse.of(accessToken, userToken.getRefreshToken()); + } + + private static String getUserId(String refreshToken) { + String[] split = refreshToken.split("-"); + if (split.length == 0) { + throw new AuthException("올바르지 않은 인증 토큰입니다. refreshToken: " + refreshToken); + } + return split[split.length - 1]; + } } diff --git a/src/test/java/in/koreatech/koin/acceptance/AuthApiTest.java b/src/test/java/in/koreatech/koin/acceptance/AuthApiTest.java index e4befb0f1..b3a876800 100644 --- a/src/test/java/in/koreatech/koin/acceptance/AuthApiTest.java +++ b/src/test/java/in/koreatech/koin/acceptance/AuthApiTest.java @@ -12,6 +12,7 @@ import io.restassured.http.ContentType; import io.restassured.response.ExtractableResponse; import io.restassured.response.Response; +import java.util.Map; import java.util.Optional; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.DisplayName; @@ -126,4 +127,65 @@ void userLogoutSuccess() { Assertions.assertThat(token).isEmpty(); } + + @Test + @DisplayName("사용자가 로그인 이후 refreshToken을 재발급한다") + void userRefreshToken() { + 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() + .body( + Map.of("refresh_token", response.jsonPath().getString("refresh_token")) + ) + .contentType(ContentType.JSON) + .when() + .log().all() + .post("/user/refresh") + .then() + .log().all() + .statusCode(HttpStatus.OK.value()) + .extract(); + + UserToken token = tokenRepository.findById(user.getId()).get(); + + assertSoftly( + softly -> { + softly.assertThat(response.jsonPath().getString("token")).isNotNull(); + softly.assertThat(response.jsonPath().getString("refresh_token")).isNotNull(); + softly.assertThat(response.jsonPath().getString("refresh_token")) + .isEqualTo(token.getRefreshToken()); + } + ); + } }