Skip to content

Commit 7d07231

Browse files
authored
[FEATURE] 액세스 토큰 리프레시 (#352)
* feat(token): 엑세스 토큰 요청 DTO 추가 * refactor(token): 코드 가독성 개선 * feat(token): 액세스 토큰 리프레시 컨트롤러 추가 * feat(token): 전역 예외 추가 * refactor(token): 패키지 이동 * refactor(token): 클래스명 수정 * feat(token): prefix 관리
1 parent 7c6de34 commit 7d07231

File tree

9 files changed

+71
-33
lines changed

9 files changed

+71
-33
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
package com.somemore.global.auth.jwt.controller;
22

3-
import com.somemore.global.auth.annotation.CurrentUser;
43
import com.somemore.global.auth.annotation.UserId;
54
import com.somemore.global.auth.jwt.domain.EncodedToken;
5+
import com.somemore.global.auth.jwt.dto.AccessTokenRequestDto;
66
import com.somemore.global.auth.jwt.manager.TokenManager;
7+
import com.somemore.global.auth.jwt.usecase.JwtUseCase;
78
import com.somemore.global.common.response.ApiResponse;
89
import io.swagger.v3.oas.annotations.tags.Tag;
10+
import jakarta.validation.Valid;
911
import lombok.RequiredArgsConstructor;
1012
import org.springframework.http.HttpStatus;
1113
import org.springframework.web.bind.annotation.GetMapping;
14+
import org.springframework.web.bind.annotation.PostMapping;
15+
import org.springframework.web.bind.annotation.RequestBody;
1216
import org.springframework.web.bind.annotation.RequestMapping;
1317
import org.springframework.web.bind.annotation.RestController;
1418

@@ -17,10 +21,11 @@
1721
@RestController
1822
@RequiredArgsConstructor
1923
@RequestMapping("/api/auth/token")
20-
@Tag(name = "Token Exchange API", description = "SignInToken을 AccessToken으로 교환하는 API")
21-
public class TokenExchangeController {
24+
@Tag(name = "Token API", description = "JWT 관련 API")
25+
public class TokenController {
2226

2327
private final TokenManager tokenManager;
28+
private final JwtUseCase jwtUseCase;
2429

2530
@GetMapping("/exchange")
2631
public ApiResponse<String> exchangeToken(
@@ -30,6 +35,19 @@ public ApiResponse<String> exchangeToken(
3035

3136
return ApiResponse.ok(HttpStatus.OK.value(),
3237
accessToken.getValueWithPrefix(),
33-
"액세스 토큰 응답 성공");
38+
"액세스 토큰 교환 성공");
39+
}
40+
41+
@PostMapping("/refresh")
42+
public ApiResponse<String> refreshAccessToken(
43+
@Valid @RequestBody AccessTokenRequestDto accessTokenRequestDto
44+
) {
45+
EncodedToken accessToken = EncodedToken.from(accessTokenRequestDto.accessToken())
46+
.removePrefix();
47+
EncodedToken newAccessToken = jwtUseCase.refreshAccessToken(accessToken);
48+
49+
return ApiResponse.ok(HttpStatus.OK.value(),
50+
newAccessToken.getValueWithPrefix(),
51+
"액세스 토큰 갱신 성공");
3452
}
3553
}

src/main/java/com/somemore/global/auth/jwt/domain/EncodedToken.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ public boolean isUninitialized() {
1919
|| value.equals(UNINITIALIZED);
2020
}
2121

22-
public EncodedToken removePrefix(String prefix) {
23-
if (this.value.startsWith(prefix)) {
24-
return new EncodedToken(this.value.substring(prefix.length()));
22+
public EncodedToken removePrefix() {
23+
if (this.value.startsWith(PREFIX)) {
24+
return new EncodedToken(this.value.substring(PREFIX.length()));
2525
}
2626
return this;
2727
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.somemore.global.auth.jwt.dto;
2+
3+
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
4+
import com.fasterxml.jackson.databind.annotation.JsonNaming;
5+
import io.swagger.v3.oas.annotations.media.Schema;
6+
import jakarta.validation.constraints.NotBlank;
7+
8+
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
9+
public record AccessTokenRequestDto(
10+
11+
@Schema(description = "액세스 토큰", example = "액세스 토큰 문자열")
12+
@NotBlank(message = "액세스 토큰 문자열")
13+
String accessToken
14+
) {
15+
}

src/main/java/com/somemore/global/auth/jwt/filter/JwtAuthFilter.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ protected void doFilterInternal(HttpServletRequest request,
4646
FilterChain filterChain) throws ServletException, IOException {
4747

4848
EncodedToken accessToken = getAccessToken(request);
49-
jwtUseCase.processAccessToken(accessToken, response);
49+
jwtUseCase.validateAccessToken(accessToken, response);
5050

5151
Claims claims = jwtUseCase.getClaims(accessToken);
5252
JwtAuthenticationToken auth = createAuthenticationToken(claims, accessToken);
@@ -66,8 +66,7 @@ private EncodedToken getAccessToken(HttpServletRequest request) {
6666
throw new JwtException(JwtErrorType.MISSING_TOKEN);
6767
}
6868

69-
String prefix = "Bearer ";
70-
return accessToken.removePrefix(prefix);
69+
return accessToken.removePrefix();
7170
}
7271

7372
private static EncodedToken findAccessTokenFromHeader(HttpServletRequest request) {

src/main/java/com/somemore/global/auth/jwt/refresher/DefaultJwtRefresher.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ public EncodedToken refreshAccessToken(EncodedToken accessToken) {
3030
jwtValidator.validateToken(refreshTokenValue);
3131

3232
Claims claims = jwtParser.parseToken(refreshTokenValue);
33-
refreshToken.updateAccessToken(generateAccessToken(claims));
33+
EncodedToken newAccessToken = generateAccessToken(claims);
34+
refreshToken.updateAccessToken(newAccessToken);
3435
tokenManager.save(refreshToken);
3536

3637
return EncodedToken.from(refreshToken.getAccessToken());

src/main/java/com/somemore/global/auth/jwt/service/JwtService.java

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
import com.somemore.global.auth.authentication.UserIdentity;
44
import com.somemore.global.auth.jwt.domain.EncodedToken;
55
import com.somemore.global.auth.jwt.domain.TokenType;
6-
import com.somemore.global.auth.jwt.exception.JwtErrorType;
7-
import com.somemore.global.auth.jwt.exception.JwtException;
86
import com.somemore.global.auth.jwt.generator.JwtGenerator;
97
import com.somemore.global.auth.jwt.parser.JwtParser;
108
import com.somemore.global.auth.jwt.refresher.JwtRefresher;
@@ -31,25 +29,17 @@ public EncodedToken generateToken(UserIdentity userIdentity, TokenType tokenType
3129
}
3230

3331
@Override
34-
public void processAccessToken(EncodedToken accessToken, HttpServletResponse response) {
35-
try {
36-
jwtValidator.validateToken(accessToken);
37-
} catch (JwtException e) {
38-
handleJwtExpiredException(e, accessToken, response);
39-
}
32+
public void validateAccessToken(EncodedToken accessToken, HttpServletResponse response) {
33+
jwtValidator.validateToken(accessToken);
4034
}
4135

4236
@Override
43-
public Claims getClaims(EncodedToken token) {
44-
return jwtParser.parseToken(token);
37+
public Claims getClaims(EncodedToken accessToken) {
38+
return jwtParser.parseToken(accessToken);
4539
}
4640

47-
private void handleJwtExpiredException(JwtException e, EncodedToken accessToken, HttpServletResponse response) {
48-
if (e.getErrorType() == JwtErrorType.EXPIRED_TOKEN) {
49-
EncodedToken refreshedToken = jwtRefresher.refreshAccessToken(accessToken);
50-
// TODO 프론트엔드와 협의 : 만료된 액세스 토큰 관리 방법
51-
return;
52-
}
53-
throw e;
41+
@Override
42+
public EncodedToken refreshAccessToken(EncodedToken accessToken) {
43+
return jwtRefresher.refreshAccessToken(accessToken);
5444
}
5545
}

src/main/java/com/somemore/global/auth/jwt/usecase/JwtUseCase.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ public interface JwtUseCase {
1010

1111
EncodedToken generateToken(UserIdentity userIdentity, TokenType tokenType);
1212

13-
void processAccessToken(EncodedToken token, HttpServletResponse response);
13+
void validateAccessToken(EncodedToken accessToken, HttpServletResponse response);
1414

15-
Claims getClaims(EncodedToken token);
15+
Claims getClaims(EncodedToken accessToken);
16+
17+
EncodedToken refreshAccessToken(EncodedToken accessToken);
1618
}

src/main/java/com/somemore/global/exception/handler/GlobalExceptionHandler.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.somemore.global.exception.handler;
22

3+
import com.somemore.global.auth.jwt.exception.JwtException;
34
import com.somemore.global.exception.BadRequestException;
45
import com.somemore.global.exception.DuplicateException;
56
import com.somemore.global.exception.ImageUploadException;
@@ -81,4 +82,16 @@ ProblemDetail handleInvalidAuthenticationException(InvalidAuthenticationExceptio
8182
return problemDetail;
8283
}
8384

85+
@ExceptionHandler(JwtException.class)
86+
ProblemDetail handleJwtException(JwtException e) {
87+
88+
ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, e.getMessage());
89+
problemDetail.setTitle("토큰 문제");
90+
problemDetail.setDetail("토큰 처리에 문제가 발생했습니다.");
91+
92+
log.warn("JwtException: {}", e.getMessage());
93+
94+
return problemDetail;
95+
}
96+
8497
}

src/test/java/com/somemore/global/auth/jwt/service/JwtServiceTest.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ void throwExceptionWhenRefreshTokenIsInvalid() {
132132
// then
133133
MockHttpServletResponse mockResponse = new MockHttpServletResponse();
134134

135-
assertThatThrownBy(() -> jwtService.processAccessToken(expiredAccessToken, mockResponse))
135+
assertThatThrownBy(() -> jwtService.validateAccessToken(expiredAccessToken, mockResponse))
136136
.isInstanceOf(JwtException.class)
137137
.hasMessage(JwtErrorType.EXPIRED_TOKEN.getMessage());
138138
}
@@ -149,7 +149,7 @@ void throwExceptionWhenRefreshTokenIsMissing() {
149149
// then
150150
MockHttpServletResponse mockResponse = new MockHttpServletResponse();
151151

152-
assertThatThrownBy(() -> jwtService.processAccessToken(expiredAccessToken, mockResponse))
152+
assertThatThrownBy(() -> jwtService.validateAccessToken(expiredAccessToken, mockResponse))
153153
.isInstanceOf(JwtException.class)
154154
.hasMessage(JwtErrorType.EXPIRED_TOKEN.getMessage());
155155
}
@@ -217,7 +217,7 @@ void refreshTokenNotFoundThrowsJwtException() {
217217

218218
// when
219219
// then
220-
assertThatThrownBy(() -> jwtService.processAccessToken(expiredAccessToken, new MockHttpServletResponse()))
220+
assertThatThrownBy(() -> jwtService.validateAccessToken(expiredAccessToken, new MockHttpServletResponse()))
221221
.isInstanceOf(JwtException.class)
222222
.hasMessage(JwtErrorType.EXPIRED_TOKEN.getMessage());
223223
}

0 commit comments

Comments
 (0)