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 @@ -3,6 +3,7 @@
import com.example.solidconnection.auth.dto.EmailSignInRequest;
import com.example.solidconnection.auth.dto.EmailSignUpTokenRequest;
import com.example.solidconnection.auth.dto.EmailSignUpTokenResponse;
import com.example.solidconnection.auth.dto.ReissueRequest;
import com.example.solidconnection.auth.dto.ReissueResponse;
import com.example.solidconnection.auth.dto.SignInResponse;
import com.example.solidconnection.auth.dto.SignUpRequest;
Expand Down Expand Up @@ -114,13 +115,9 @@ public ResponseEntity<Void> quit(

@PostMapping("/reissue")
public ResponseEntity<ReissueResponse> reissueToken(
Authentication authentication
ReissueRequest reissueRequest
) {
String token = authentication.getCredentials().toString();
if (token == null) {
throw new CustomException(ErrorCode.AUTHENTICATION_FAILED, "토큰이 없습니다.");
}
ReissueResponse reissueResponse = authService.reissue(token);
ReissueResponse reissueResponse = authService.reissue(reissueRequest);
return ResponseEntity.ok(reissueResponse);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@
@Getter
public enum TokenType {

ACCESS("ACCESS:", 1000 * 60 * 60), // 1hour
REFRESH("REFRESH:", 1000 * 60 * 60 * 24 * 7), // 7days
ACCESS("ACCESS:", 1000L * 60 * 60), // 1hour
REFRESH("REFRESH:", 1000L * 60 * 60 * 24 * 90), // 90days
BLACKLIST("BLACKLIST:", ACCESS.expireTime),
SIGN_UP("SIGN_UP:", 1000 * 60 * 10), // 10min
SIGN_UP("SIGN_UP:", 1000L * 60 * 10), // 10min
;

private final String prefix;
private final int expireTime;
private final long expireTime;

TokenType(String prefix, int expireTime) {
TokenType(String prefix, long expireTime) {
this.prefix = prefix;
this.expireTime = expireTime;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.example.solidconnection.auth.dto;

import jakarta.validation.constraints.NotBlank;

public record ReissueRequest(
@NotBlank(message = "리프레시 토큰과 함께 요청해주세요.")
Copy link
Contributor

Choose a reason for hiding this comment

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

사소하지만 ㅎㅎ.. 이 컨벤션이 늘 헷갈렸는데 제가 들어오기전 솔커 컨벤션에선 dto에서 어노테이션 쓸 때 한 칸 개행하고 썼더라구요! 나중에 개행한 거 다 없앨까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

아하 지금 어노테이션이 안붙어있는 값들은 개행이 안되어있고,
어노테이션이 붙어있는 것들만 시작 개행이 되어있는 상태군요 🫢

둘중에 어느것으로든 통일하면 좋겠는데.. 규혁님은 둘 다 개행 안한게 더 좋으신가요?
저는 둘 중 어느것으로 가도 큰 상관 없습니다!

Copy link
Contributor

Choose a reason for hiding this comment

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

읽는 측면에서 큰 차이는 없는 거 같아서 저도 큰 상관 없지만, 어노테이션 앞에 시작개행이 불필요한 빈 줄이라면 없애는 게 더 나은 거 같네요!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

그럼 시작 개행 안하도록 통일하는게 좋겠네요 ㅎㅎ

String refreshToken) {
}
Original file line number Diff line number Diff line change
@@ -1,23 +1,28 @@
package com.example.solidconnection.auth.service;


import com.example.solidconnection.auth.dto.ReissueRequest;
import com.example.solidconnection.auth.dto.ReissueResponse;
import com.example.solidconnection.config.security.JwtProperties;
import com.example.solidconnection.custom.exception.CustomException;
import com.example.solidconnection.siteuser.domain.SiteUser;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDate;
import java.util.Objects;
import java.util.Optional;

import static com.example.solidconnection.custom.exception.ErrorCode.REFRESH_TOKEN_EXPIRED;
import static com.example.solidconnection.util.JwtUtils.parseSubject;

@RequiredArgsConstructor
@Service
public class AuthService {

private final AuthTokenProvider authTokenProvider;
private final JwtProperties jwtProperties;

/*
* 로그아웃 한다.
Expand All @@ -40,13 +45,15 @@ public void quit(SiteUser siteUser) {

/*
* 액세스 토큰을 재발급한다.
* - 리프레시 토큰이 만료되었거나, 존재하지 않는다면 예외 응답을 반환한다.
* - 리프레시 토큰이 존재한다면, 액세스 토큰을 재발급한다.
* - 요청된 리프레시 토큰과 동일한 subject 의 토큰이 저장되어 있으며 값이 일치할 경우, 액세스 토큰을 재발급한다.
* - 그렇지 않으면 예외를 반환한다.
* */
public ReissueResponse reissue(String subject) {
// 리프레시 토큰 만료 확인
Optional<String> optionalRefreshToken = authTokenProvider.findRefreshToken(subject);
if (optionalRefreshToken.isEmpty()) {
public ReissueResponse reissue(ReissueRequest reissueRequest) {
// 리프레시 토큰 확인
String requestedRefreshToken = reissueRequest.refreshToken();
String subject = parseSubject(requestedRefreshToken, jwtProperties.secret());
Optional<String> savedRefreshToken = authTokenProvider.findRefreshToken(subject);
if (!Objects.equals(requestedRefreshToken, savedRefreshToken.orElse(null))) {
throw new CustomException(REFRESH_TOKEN_EXPIRED);
}
// 액세스 토큰 재발급
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@

import java.util.Optional;

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

@Component
public class AuthTokenProvider extends TokenProvider {

Expand All @@ -18,7 +16,7 @@ public AuthTokenProvider(JwtProperties jwtProperties, RedisTemplate<String, Stri
}

public String generateAccessToken(SiteUser siteUser) {
String subject = siteUser.getId().toString();
String subject = getSubject(siteUser);
return generateToken(subject, TokenType.ACCESS);
}

Expand All @@ -27,7 +25,7 @@ public String generateAccessToken(String subject) {
}

public String generateAndSaveRefreshToken(SiteUser siteUser) {
String subject = siteUser.getId().toString();
String subject = getSubject(siteUser);
String refreshToken = generateToken(subject, TokenType.REFRESH);
return saveToken(refreshToken, TokenType.REFRESH);
}
Expand All @@ -47,7 +45,7 @@ public Optional<String> findBlackListToken(String subject) {
return Optional.ofNullable(redisTemplate.opsForValue().get(blackListTokenKey));
}

public String getEmail(String token) {
return parseSubjectIgnoringExpiration(token, jwtProperties.secret());
private String getSubject(SiteUser siteUser) {
return siteUser.getId().toString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,19 @@ public static String parseTokenFromRequest(HttpServletRequest request) {
return token.substring(TOKEN_PREFIX.length());
}

public static String parseSubjectIgnoringExpiration(String token, String secretKey) {
public static String parseSubject(String token, String secretKey) {
try {
return parseClaims(token, secretKey).getSubject();
} catch (ExpiredJwtException e) {
return e.getClaims().getSubject();
} catch (Exception e) {
throw new CustomException(INVALID_TOKEN);
}
}

public static String parseSubject(String token, String secretKey) {
public static String parseSubjectIgnoringExpiration(String token, String secretKey) {
try {
return parseClaims(token, secretKey).getSubject();
} catch (ExpiredJwtException e) {
return e.getClaims().getSubject();
} catch (Exception e) {
throw new CustomException(INVALID_TOKEN);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package com.example.solidconnection.auth.service;

import com.example.solidconnection.auth.domain.TokenType;
import com.example.solidconnection.auth.dto.ReissueRequest;
import com.example.solidconnection.auth.dto.ReissueResponse;
import com.example.solidconnection.config.security.JwtProperties;
import com.example.solidconnection.custom.exception.CustomException;
import com.example.solidconnection.siteuser.domain.SiteUser;
import com.example.solidconnection.siteuser.repository.SiteUserRepository;
import com.example.solidconnection.support.TestContainerSpringBootTest;
import com.example.solidconnection.type.PreparationStatus;
import com.example.solidconnection.type.Role;
import com.example.solidconnection.util.JwtUtils;
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 java.time.LocalDate;

import static com.example.solidconnection.custom.exception.ErrorCode.REFRESH_TOKEN_EXPIRED;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;

@DisplayName("인증 서비스 테스트")
@TestContainerSpringBootTest
class AuthServiceTest {

@Autowired
private AuthService authService;

@Autowired
private AuthTokenProvider authTokenProvider;

@Autowired
private SiteUserRepository siteUserRepository;

@Autowired
private JwtProperties jwtProperties;

@Test
void 로그아웃한다() {
// given
String accessToken = "accessToken";

// when
authService.signOut(accessToken);

// then
assertThat(authTokenProvider.findBlackListToken(accessToken)).isNotNull();
}

@Test
void 탈퇴한다() {
// given
SiteUser siteUser = createSiteUser();

// when
authService.quit(siteUser);

// then
LocalDate tomorrow = LocalDate.now().plusDays(1);
assertThat(siteUser.getQuitedAt()).isEqualTo(tomorrow);
}

@Nested
class 토큰을_재발급한다 {

@Test
void 요청의_리프레시_토큰이_저장되어_있고_값이_일치면_액세스_토큰을_재발급한다() {
// given
SiteUser siteUser = createSiteUser();
String refreshToken = authTokenProvider.generateAndSaveRefreshToken(siteUser);
ReissueRequest reissueRequest = new ReissueRequest(refreshToken);

// when
ReissueResponse reissuedAccessToken = authService.reissue(reissueRequest);

// then
String actualSubject = JwtUtils.parseSubject(reissuedAccessToken.accessToken(), jwtProperties.secret());
String expectedSubject = JwtUtils.parseSubject(refreshToken, jwtProperties.secret());
assertThat(actualSubject).isEqualTo(expectedSubject);
}

@Test
void 요청의_리프레시_토큰이_저장되어있지_않다면_예외_응답을_반환한다() {
// given
String refreshToken = authTokenProvider.generateToken("subject", TokenType.REFRESH);
ReissueRequest reissueRequest = new ReissueRequest(refreshToken);

// when, then
assertThatCode(() -> authService.reissue(reissueRequest))
.isInstanceOf(CustomException.class)
.hasMessage(REFRESH_TOKEN_EXPIRED.getMessage());
}
}

private SiteUser createSiteUser() {
SiteUser siteUser = new SiteUser(
"test@example.com",
"nickname",
"profileImageUrl",
PreparationStatus.CONSIDERING,
Role.MENTEE
);
return siteUserRepository.save(siteUser);
}
}
Loading