Skip to content

Commit

Permalink
Merge pull request #39 from LoveMarker/feat/#37
Browse files Browse the repository at this point in the history
feat: 토큰 재발급 API
  • Loading branch information
funnysunny08 authored Nov 9, 2024
2 parents 49e48bb + be9ee8b commit afb2de8
Show file tree
Hide file tree
Showing 9 changed files with 195 additions and 3 deletions.
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
package com.lovemarker.domain.auth.controller;

import com.lovemarker.domain.auth.dto.request.SignInRequest;
import com.lovemarker.domain.auth.dto.response.ReissueTokenResponse;
import com.lovemarker.domain.auth.dto.response.SignInResponse;
import com.lovemarker.domain.auth.service.AuthService;
import com.lovemarker.global.constant.SuccessCode;
import com.lovemarker.global.dto.ApiResponseDto;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

Expand All @@ -26,4 +30,12 @@ public ApiResponseDto<SignInResponse> signIn(
return ApiResponseDto.success(SuccessCode.LOGIN_SUCCESS,
authService.signIn(signInRequest.token(), signInRequest.provider()));
}

@GetMapping("/reissue-token")
public ApiResponseDto<ReissueTokenResponse> reissueToken(
@RequestHeader @NotBlank String refreshToken
) {
return ApiResponseDto.success(SuccessCode.REISSUE_TOKEN_SUCCESS,
authService.reissueToken(refreshToken));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.lovemarker.domain.auth.dto.response;

public record ReissueTokenResponse(
String accessToken
) {
public static ReissueTokenResponse of(String accessToken) {
return new ReissueTokenResponse(accessToken);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.lovemarker.domain.auth.exception;

import com.lovemarker.global.constant.ErrorCode;
import com.lovemarker.global.exception.BasicException;

public class InvalidTokenException extends BasicException {

public InvalidTokenException(ErrorCode errorCode, String message) {
super(errorCode, message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.lovemarker.domain.auth.exception;

import com.lovemarker.global.constant.ErrorCode;
import com.lovemarker.global.exception.BasicException;

public class TimeExpiredTokenException extends BasicException {

public TimeExpiredTokenException(ErrorCode errorCode, String message) {
super(errorCode, message);
}
}
41 changes: 41 additions & 0 deletions src/main/java/com/lovemarker/domain/auth/service/AuthService.java
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
package com.lovemarker.domain.auth.service;

import com.lovemarker.domain.auth.RandomNicknameGenerator;
import com.lovemarker.domain.auth.dto.response.ReissueTokenResponse;
import com.lovemarker.domain.auth.dto.response.SignInResponse;
import com.lovemarker.domain.auth.dto.response.SocialInfoResponse;
import com.lovemarker.domain.auth.exception.InvalidTokenException;
import com.lovemarker.domain.auth.exception.TimeExpiredTokenException;
import com.lovemarker.domain.user.User;
import com.lovemarker.domain.user.exception.UserNotFoundException;
import com.lovemarker.domain.user.repository.UserRepository;
import com.lovemarker.domain.user.vo.SocialType;
import com.lovemarker.global.constant.ErrorCode;
import com.lovemarker.global.constant.TokenStatus;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand All @@ -20,6 +24,7 @@ public class AuthService {
private final SocialSignInService socialSignInService;
private final JwtService jwtService;
private final RandomNicknameGenerator randomNicknameGenerator;
private final TokenCacheService tokenCacheService;

@Transactional
public SignInResponse signIn(String token, String type) {
Expand All @@ -41,8 +46,44 @@ public SignInResponse signIn(String token, String type) {
return SignInResponse.of(isRegistered ? "Login" : "Signup", accessToken, refreshToken);
}

@Transactional
public ReissueTokenResponse reissueToken(String refreshToken) {
if (jwtService.verifyToken(refreshToken) == TokenStatus.TOKEN_INVALID) {
throw new InvalidTokenException(ErrorCode.INVALID_REFRESH_TOKEN_EXCEPTION,
ErrorCode.INVALID_REFRESH_TOKEN_EXCEPTION.getMessage());
} else if (jwtService.verifyToken(refreshToken) == TokenStatus.TOKEN_EXPIRED) {
throw new TimeExpiredTokenException(ErrorCode.REFRESH_TOKEN_TIME_EXPIRED_EXCEPTION,
ErrorCode.REFRESH_TOKEN_TIME_EXPIRED_EXCEPTION.getMessage());
}

final String tokenContents = jwtService.getJwtContents(refreshToken);
try {
final long userId = Long.parseLong(tokenContents);
if (tokenCacheService.getValuesByKey(String.valueOf(userId)).isBlank()) {
throw new InvalidTokenException(ErrorCode.INVALID_REFRESH_TOKEN_EXCEPTION,
ErrorCode.INVALID_REFRESH_TOKEN_EXCEPTION.getMessage());
}
User user = getUserByUserId(userId);
String newAccessToken = jwtService.issuedAccessToken(user.getUserId());
return new ReissueTokenResponse(newAccessToken);

} catch (NumberFormatException e) {
throw new UserNotFoundException(ErrorCode.NOT_FOUND_USER_EXCEPTION,
ErrorCode.NOT_FOUND_USER_EXCEPTION.getMessage());
} catch (NullPointerException e) {
throw new InvalidTokenException(ErrorCode.INVALID_REFRESH_TOKEN_EXCEPTION,
ErrorCode.INVALID_REFRESH_TOKEN_EXCEPTION.getMessage());
}
}

private User getUserBySocialInfo(String socialToken, SocialType provider) {
return userRepository.findBySocialToken_SocialTokenAndProvider(socialToken, provider)
.orElseThrow(() -> new UserNotFoundException(ErrorCode.NOT_FOUND_USER_EXCEPTION, ErrorCode.NOT_FOUND_USER_EXCEPTION.getMessage()));
}

private User getUserByUserId(Long userId) {
return userRepository.findById(userId)
.orElseThrow(() -> new UserNotFoundException(ErrorCode.NOT_FOUND_USER_EXCEPTION,
ErrorCode.NOT_FOUND_USER_EXCEPTION.getMessage()));
}
}
4 changes: 2 additions & 2 deletions src/main/java/com/lovemarker/global/constant/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ public enum ErrorCode {
IMAGE_VALIDATION_EXCEPTION(HttpStatus.BAD_REQUEST, "잘못된 파일입니다."),

// 401
ACCESS_TOKEN_TIME_EXPIRED_EXCEPTION(HttpStatus.UNAUTHORIZED, "만료된 토큰입니다."),
REFRESH_TOKEN_TIME_EXPIRED_EXCEPTION(HttpStatus.UNAUTHORIZED, "만료된 토큰입니다."),
ACCESS_TOKEN_TIME_EXPIRED_EXCEPTION(HttpStatus.UNAUTHORIZED, "만료된 액세스 토큰입니다."),
REFRESH_TOKEN_TIME_EXPIRED_EXCEPTION(HttpStatus.UNAUTHORIZED, "만료된 리프레시 토큰입니다."),
INVALID_REFRESH_TOKEN_EXCEPTION(HttpStatus.UNAUTHORIZED, "유효하지 않은 리프레시 토큰입니다."),
INVALID_ACCESS_TOKEN_EXCEPTION(HttpStatus.UNAUTHORIZED, "유효하지 않은 엑세스 토큰입니다."),
INVALID_GOOGLE_ID_TOKEN_EXCEPTION(HttpStatus.UNAUTHORIZED, "유효하지 않은 구글 아이디 토큰입니다."),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ public enum SuccessCode {
// 201
CREATE_INVITATION_CODE_SUCCESS(HttpStatus.CREATED, "초대 코드 생성을 성공했습니다."),
JOIN_COUPLE_SUCCESS(HttpStatus.CREATED, "커플 연결을 성공했습니다."),
CREATE_MEMORY_SUCCESS(HttpStatus.CREATED, "추억 생성을 성공했습니다.");
CREATE_MEMORY_SUCCESS(HttpStatus.CREATED, "추억 생성을 성공했습니다."),
REISSUE_TOKEN_SUCCESS(HttpStatus.CREATED, "토큰 재발급에 성공했습니다.");

private final HttpStatus httpStatus;
private final String message;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,17 @@
import static javax.swing.text.html.parser.DTDConstants.NUMBER;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName;
import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields;
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;

import com.lovemarker.base.BaseControllerTest;
import com.lovemarker.domain.auth.dto.request.SignInRequest;
import com.lovemarker.domain.auth.dto.response.ReissueTokenResponse;
import com.lovemarker.domain.auth.dto.response.SignInResponse;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -51,4 +55,31 @@ void signIn() throws Exception {
)
));
}

@Test
@DisplayName("성공: 토큰 재발급 api 호출 시")
void reissueToken() throws Exception {
//given
ReissueTokenResponse response = new ReissueTokenResponse("new token");

given(authService.reissueToken(any())).willReturn(response);

//when
ResultActions resultActions = mockMvc.perform(get("/api/auth/reissue-token")
.header("refreshToken", "token"));

//then
resultActions
.andDo(restDocs.document(
requestHeaders(
headerWithName("refreshToken").description("리프레시 토큰")
),
responseFields(
fieldWithPath("status").type(NUMBER).description("상태 코드"),
fieldWithPath("success").type(BOOLEAN).description("성공 여부"),
fieldWithPath("message").type(STRING).description("메시지"),
fieldWithPath("data.accessToken").type(STRING).description("액세스 토큰")
)
));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,20 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.BDDAssertions.catchException;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.BDDMockito.given;

import com.lovemarker.domain.auth.RandomNicknameGenerator;
import com.lovemarker.domain.auth.dto.response.ReissueTokenResponse;
import com.lovemarker.domain.auth.dto.response.SignInResponse;
import com.lovemarker.domain.auth.dto.response.SocialInfoResponse;
import com.lovemarker.domain.auth.exception.InvalidTokenException;
import com.lovemarker.domain.auth.exception.TimeExpiredTokenException;
import com.lovemarker.domain.user.User;
import com.lovemarker.domain.user.exception.UserNotFoundException;
import com.lovemarker.domain.user.fixture.UserFixture;
import com.lovemarker.domain.user.repository.UserRepository;
import com.lovemarker.global.constant.TokenStatus;
import java.util.Optional;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
Expand Down Expand Up @@ -39,6 +44,9 @@ class AuthServiceTest {
@Mock
RandomNicknameGenerator randomNicknameGenerator;

@Mock
TokenCacheService tokenCacheService;

@Nested
@DisplayName("signIn 메서드 실행 시")
class SignInTest {
Expand Down Expand Up @@ -95,4 +103,72 @@ void exceptionWhenUserNotFound() {
assertThat(exception).isInstanceOf(UserNotFoundException.class);
}
}

@Nested
@DisplayName("reissueToken 메서드 실행 시")
class ReissueTokenTest {

String refreshToken = "validRefreshToken";
String newAccessToken = "newAccessToken";
User user = UserFixture.user();

@Test
@DisplayName("성공")
void reissueToken() {
//given
given(jwtService.verifyToken(any())).willReturn(TokenStatus.TOKEN_VALID);
given(jwtService.getJwtContents(any())).willReturn("1");
given(tokenCacheService.getValuesByKey(any())).willReturn("token");
given(userRepository.findById(anyLong())).willReturn(Optional.of(user));
given(jwtService.issuedAccessToken(any())).willReturn(newAccessToken);

//when
ReissueTokenResponse response = authService.reissueToken(refreshToken);

//then
assertThat(response).isNotNull();
assertThat(response.accessToken()).isEqualTo(newAccessToken);
}

@Test
@DisplayName("예외(InvalidTokenException): refresh token verify 실패")
void exceptionWhenInvalidToken() {
//given
given(jwtService.verifyToken(any())).willReturn(TokenStatus.TOKEN_INVALID);

//when
Exception exception = catchException(() -> authService.reissueToken(any()));

//then
assertThat(exception).isInstanceOf(InvalidTokenException.class);
}

@Test
@DisplayName("예외(TimeExpiredTokenException): refresh token time expired")
void exceptionWhenExpiredToken() {
//given
given(jwtService.verifyToken(any())).willReturn(TokenStatus.TOKEN_EXPIRED);

//when
Exception exception = catchException(() -> authService.reissueToken(any()));

//then
assertThat(exception).isInstanceOf(TimeExpiredTokenException.class);
}

@Test
@DisplayName("예외(UserNotFoundException): 존재하지 않는 유저")
void exceptionWhenUserNotFound() {
//given
given(jwtService.verifyToken(any())).willReturn(TokenStatus.TOKEN_VALID);
given(jwtService.getJwtContents(any())).willReturn("1");
given(tokenCacheService.getValuesByKey(any())).willReturn("token");

//when
Exception exception = catchException(() -> authService.reissueToken(any()));

//then
assertThat(exception).isInstanceOf(UserNotFoundException.class);
}
}
}

0 comments on commit afb2de8

Please sign in to comment.