Skip to content

Commit ff0125d

Browse files
authored
fix: 토큰 재발급 로직 수정 (#289)
* refactor: 함수 분리 - subject의 기준이 SiteUser의 id가 아니게 바뀌더라도 변경 부분이 최소화되게 한다. - SiteUser의 subject를 사용하는 곳은 accessToken과 refreshToken를 관리하는 AuthTokenProvider 의 영역이므로 이 위치에 함수를 생성한다. * chore: 사용하지 않는 함수 삭제 * refactor: 가독성을 위해 함수 위치 변경 - parseSubject 가 parseSubjectIgnoringExpiration 보다 먼저 읽히도록 * refactor: 재발급 로직 수정 - subject가 아니라 엑세스 토큰으로 리프레시 토큰을 찾도록 * refactor: 리프레시 토큰 기간 연장 - 참고: discussion #292 * refactor: 액세스 토큰 재발급 로직 수정 * test: authServiceTest 작성
1 parent 2ec3edf commit ff0125d

File tree

7 files changed

+145
-27
lines changed

7 files changed

+145
-27
lines changed

src/main/java/com/example/solidconnection/auth/controller/AuthController.java

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import com.example.solidconnection.auth.dto.EmailSignInRequest;
44
import com.example.solidconnection.auth.dto.EmailSignUpTokenRequest;
55
import com.example.solidconnection.auth.dto.EmailSignUpTokenResponse;
6+
import com.example.solidconnection.auth.dto.ReissueRequest;
67
import com.example.solidconnection.auth.dto.ReissueResponse;
78
import com.example.solidconnection.auth.dto.SignInResponse;
89
import com.example.solidconnection.auth.dto.SignUpRequest;
@@ -114,13 +115,9 @@ public ResponseEntity<Void> quit(
114115

115116
@PostMapping("/reissue")
116117
public ResponseEntity<ReissueResponse> reissueToken(
117-
Authentication authentication
118+
ReissueRequest reissueRequest
118119
) {
119-
String token = authentication.getCredentials().toString();
120-
if (token == null) {
121-
throw new CustomException(ErrorCode.AUTHENTICATION_FAILED, "토큰이 없습니다.");
122-
}
123-
ReissueResponse reissueResponse = authService.reissue(token);
120+
ReissueResponse reissueResponse = authService.reissue(reissueRequest);
124121
return ResponseEntity.ok(reissueResponse);
125122
}
126123
}

src/main/java/com/example/solidconnection/auth/domain/TokenType.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,16 @@
55
@Getter
66
public enum TokenType {
77

8-
ACCESS("ACCESS:", 1000 * 60 * 60), // 1hour
9-
REFRESH("REFRESH:", 1000 * 60 * 60 * 24 * 7), // 7days
8+
ACCESS("ACCESS:", 1000L * 60 * 60), // 1hour
9+
REFRESH("REFRESH:", 1000L * 60 * 60 * 24 * 90), // 90days
1010
BLACKLIST("BLACKLIST:", ACCESS.expireTime),
11-
SIGN_UP("SIGN_UP:", 1000 * 60 * 10), // 10min
11+
SIGN_UP("SIGN_UP:", 1000L * 60 * 10), // 10min
1212
;
1313

1414
private final String prefix;
15-
private final int expireTime;
15+
private final long expireTime;
1616

17-
TokenType(String prefix, int expireTime) {
17+
TokenType(String prefix, long expireTime) {
1818
this.prefix = prefix;
1919
this.expireTime = expireTime;
2020
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package com.example.solidconnection.auth.dto;
2+
3+
import jakarta.validation.constraints.NotBlank;
4+
5+
public record ReissueRequest(
6+
@NotBlank(message = "리프레시 토큰과 함께 요청해주세요.")
7+
String refreshToken) {
8+
}

src/main/java/com/example/solidconnection/auth/service/AuthService.java

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,28 @@
11
package com.example.solidconnection.auth.service;
22

33

4+
import com.example.solidconnection.auth.dto.ReissueRequest;
45
import com.example.solidconnection.auth.dto.ReissueResponse;
6+
import com.example.solidconnection.config.security.JwtProperties;
57
import com.example.solidconnection.custom.exception.CustomException;
68
import com.example.solidconnection.siteuser.domain.SiteUser;
79
import lombok.RequiredArgsConstructor;
810
import org.springframework.stereotype.Service;
911
import org.springframework.transaction.annotation.Transactional;
1012

1113
import java.time.LocalDate;
14+
import java.util.Objects;
1215
import java.util.Optional;
1316

1417
import static com.example.solidconnection.custom.exception.ErrorCode.REFRESH_TOKEN_EXPIRED;
18+
import static com.example.solidconnection.util.JwtUtils.parseSubject;
1519

1620
@RequiredArgsConstructor
1721
@Service
1822
public class AuthService {
1923

2024
private final AuthTokenProvider authTokenProvider;
25+
private final JwtProperties jwtProperties;
2126

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

4146
/*
4247
* 액세스 토큰을 재발급한다.
43-
* - 리프레시 토큰이 만료되었거나, 존재하지 않는다면 예외 응답을 반환한다.
44-
* - 리프레시 토큰이 존재한다면, 액세스 토큰을 재발급한다.
48+
* - 요청된 리프레시 토큰과 동일한 subject 의 토큰이 저장되어 있으며 값이 일치할 경우, 액세스 토큰을 재발급한다.
49+
* - 그렇지 않으면 예외를 반환한다.
4550
* */
46-
public ReissueResponse reissue(String subject) {
47-
// 리프레시 토큰 만료 확인
48-
Optional<String> optionalRefreshToken = authTokenProvider.findRefreshToken(subject);
49-
if (optionalRefreshToken.isEmpty()) {
51+
public ReissueResponse reissue(ReissueRequest reissueRequest) {
52+
// 리프레시 토큰 확인
53+
String requestedRefreshToken = reissueRequest.refreshToken();
54+
String subject = parseSubject(requestedRefreshToken, jwtProperties.secret());
55+
Optional<String> savedRefreshToken = authTokenProvider.findRefreshToken(subject);
56+
if (!Objects.equals(requestedRefreshToken, savedRefreshToken.orElse(null))) {
5057
throw new CustomException(REFRESH_TOKEN_EXPIRED);
5158
}
5259
// 액세스 토큰 재발급

src/main/java/com/example/solidconnection/auth/service/AuthTokenProvider.java

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@
88

99
import java.util.Optional;
1010

11-
import static com.example.solidconnection.util.JwtUtils.parseSubjectIgnoringExpiration;
12-
1311
@Component
1412
public class AuthTokenProvider extends TokenProvider {
1513

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

2018
public String generateAccessToken(SiteUser siteUser) {
21-
String subject = siteUser.getId().toString();
19+
String subject = getSubject(siteUser);
2220
return generateToken(subject, TokenType.ACCESS);
2321
}
2422

@@ -27,7 +25,7 @@ public String generateAccessToken(String subject) {
2725
}
2826

2927
public String generateAndSaveRefreshToken(SiteUser siteUser) {
30-
String subject = siteUser.getId().toString();
28+
String subject = getSubject(siteUser);
3129
String refreshToken = generateToken(subject, TokenType.REFRESH);
3230
return saveToken(refreshToken, TokenType.REFRESH);
3331
}
@@ -47,7 +45,7 @@ public Optional<String> findBlackListToken(String subject) {
4745
return Optional.ofNullable(redisTemplate.opsForValue().get(blackListTokenKey));
4846
}
4947

50-
public String getEmail(String token) {
51-
return parseSubjectIgnoringExpiration(token, jwtProperties.secret());
48+
private String getSubject(SiteUser siteUser) {
49+
return siteUser.getId().toString();
5250
}
5351
}

src/main/java/com/example/solidconnection/util/JwtUtils.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,19 +28,19 @@ public static String parseTokenFromRequest(HttpServletRequest request) {
2828
return token.substring(TOKEN_PREFIX.length());
2929
}
3030

31-
public static String parseSubjectIgnoringExpiration(String token, String secretKey) {
31+
public static String parseSubject(String token, String secretKey) {
3232
try {
3333
return parseClaims(token, secretKey).getSubject();
34-
} catch (ExpiredJwtException e) {
35-
return e.getClaims().getSubject();
3634
} catch (Exception e) {
3735
throw new CustomException(INVALID_TOKEN);
3836
}
3937
}
4038

41-
public static String parseSubject(String token, String secretKey) {
39+
public static String parseSubjectIgnoringExpiration(String token, String secretKey) {
4240
try {
4341
return parseClaims(token, secretKey).getSubject();
42+
} catch (ExpiredJwtException e) {
43+
return e.getClaims().getSubject();
4444
} catch (Exception e) {
4545
throw new CustomException(INVALID_TOKEN);
4646
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package com.example.solidconnection.auth.service;
2+
3+
import com.example.solidconnection.auth.domain.TokenType;
4+
import com.example.solidconnection.auth.dto.ReissueRequest;
5+
import com.example.solidconnection.auth.dto.ReissueResponse;
6+
import com.example.solidconnection.config.security.JwtProperties;
7+
import com.example.solidconnection.custom.exception.CustomException;
8+
import com.example.solidconnection.siteuser.domain.SiteUser;
9+
import com.example.solidconnection.siteuser.repository.SiteUserRepository;
10+
import com.example.solidconnection.support.TestContainerSpringBootTest;
11+
import com.example.solidconnection.type.PreparationStatus;
12+
import com.example.solidconnection.type.Role;
13+
import com.example.solidconnection.util.JwtUtils;
14+
import org.junit.jupiter.api.DisplayName;
15+
import org.junit.jupiter.api.Nested;
16+
import org.junit.jupiter.api.Test;
17+
import org.springframework.beans.factory.annotation.Autowired;
18+
19+
import java.time.LocalDate;
20+
21+
import static com.example.solidconnection.custom.exception.ErrorCode.REFRESH_TOKEN_EXPIRED;
22+
import static org.assertj.core.api.Assertions.assertThat;
23+
import static org.assertj.core.api.Assertions.assertThatCode;
24+
25+
@DisplayName("인증 서비스 테스트")
26+
@TestContainerSpringBootTest
27+
class AuthServiceTest {
28+
29+
@Autowired
30+
private AuthService authService;
31+
32+
@Autowired
33+
private AuthTokenProvider authTokenProvider;
34+
35+
@Autowired
36+
private SiteUserRepository siteUserRepository;
37+
38+
@Autowired
39+
private JwtProperties jwtProperties;
40+
41+
@Test
42+
void 로그아웃한다() {
43+
// given
44+
String accessToken = "accessToken";
45+
46+
// when
47+
authService.signOut(accessToken);
48+
49+
// then
50+
assertThat(authTokenProvider.findBlackListToken(accessToken)).isNotNull();
51+
}
52+
53+
@Test
54+
void 탈퇴한다() {
55+
// given
56+
SiteUser siteUser = createSiteUser();
57+
58+
// when
59+
authService.quit(siteUser);
60+
61+
// then
62+
LocalDate tomorrow = LocalDate.now().plusDays(1);
63+
assertThat(siteUser.getQuitedAt()).isEqualTo(tomorrow);
64+
}
65+
66+
@Nested
67+
class 토큰을_재발급한다 {
68+
69+
@Test
70+
void 요청의_리프레시_토큰이_저장되어_있고_값이_일치면_액세스_토큰을_재발급한다() {
71+
// given
72+
SiteUser siteUser = createSiteUser();
73+
String refreshToken = authTokenProvider.generateAndSaveRefreshToken(siteUser);
74+
ReissueRequest reissueRequest = new ReissueRequest(refreshToken);
75+
76+
// when
77+
ReissueResponse reissuedAccessToken = authService.reissue(reissueRequest);
78+
79+
// then
80+
String actualSubject = JwtUtils.parseSubject(reissuedAccessToken.accessToken(), jwtProperties.secret());
81+
String expectedSubject = JwtUtils.parseSubject(refreshToken, jwtProperties.secret());
82+
assertThat(actualSubject).isEqualTo(expectedSubject);
83+
}
84+
85+
@Test
86+
void 요청의_리프레시_토큰이_저장되어있지_않다면_예외_응답을_반환한다() {
87+
// given
88+
String refreshToken = authTokenProvider.generateToken("subject", TokenType.REFRESH);
89+
ReissueRequest reissueRequest = new ReissueRequest(refreshToken);
90+
91+
// when, then
92+
assertThatCode(() -> authService.reissue(reissueRequest))
93+
.isInstanceOf(CustomException.class)
94+
.hasMessage(REFRESH_TOKEN_EXPIRED.getMessage());
95+
}
96+
}
97+
98+
private SiteUser createSiteUser() {
99+
SiteUser siteUser = new SiteUser(
100+
"test@example.com",
101+
"nickname",
102+
"profileImageUrl",
103+
PreparationStatus.CONSIDERING,
104+
Role.MENTEE
105+
);
106+
return siteUserRepository.save(siteUser);
107+
}
108+
}

0 commit comments

Comments
 (0)