Skip to content

Commit 219be6d

Browse files
authored
Merge pull request #151 from Pinback-Team/feat/#145
feat: 토큰 재발급 기능 구현
2 parents a72256e + 12b00cf commit 219be6d

13 files changed

Lines changed: 183 additions & 17 deletions

File tree

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package com.pinback.api.auth.controller;
2+
3+
import org.springframework.beans.factory.annotation.Value;
4+
import org.springframework.http.HttpHeaders;
5+
import org.springframework.http.ResponseCookie;
6+
import org.springframework.http.ResponseEntity;
7+
import org.springframework.web.bind.annotation.CookieValue;
8+
import org.springframework.web.bind.annotation.PostMapping;
9+
import org.springframework.web.bind.annotation.RequestMapping;
10+
import org.springframework.web.bind.annotation.RestController;
11+
12+
import com.pinback.application.auth.dto.SignUpResponseV3;
13+
import com.pinback.application.auth.dto.TokenResponse;
14+
import com.pinback.application.auth.usecase.AuthUsecase;
15+
import com.pinback.shared.dto.ResponseDto;
16+
17+
import io.swagger.v3.oas.annotations.tags.Tag;
18+
import lombok.RequiredArgsConstructor;
19+
20+
@RestController
21+
@RequestMapping("/api/v3/auth")
22+
@RequiredArgsConstructor
23+
@Tag(name = "Authentication V3", description = "인증 관리 API V3")
24+
public class AuthControllerV3 {
25+
private final AuthUsecase authUsecase;
26+
27+
@Value("${jwt.refreshExpirationPeriod}")
28+
private long refreshTokenExpirationPeriod;
29+
30+
@PostMapping("/reissue")
31+
public ResponseEntity<ResponseDto<TokenResponse>> reissueAccessToken(
32+
@CookieValue(name = "refreshToken") String refreshToken
33+
) {
34+
SignUpResponseV3 response = authUsecase.getNewToken(refreshToken);
35+
36+
ResponseCookie cookie = ResponseCookie.from("refreshToken", response.refreshToken())
37+
.httpOnly(true)
38+
.secure(true)
39+
.path("/")
40+
.maxAge(refreshTokenExpirationPeriod / 1000)
41+
.sameSite("None")
42+
.build();
43+
44+
return ResponseEntity.ok()
45+
.header(HttpHeaders.SET_COOKIE, cookie.toString())
46+
.body(ResponseDto.ok(TokenResponse.of(response.accessToken())));
47+
}
48+
49+
}

api/src/main/java/com/pinback/api/config/filter/JwtAuthenticationFilter.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,8 @@ protected boolean shouldNotFilter(HttpServletRequest request) throws ServletExce
104104
path.startsWith("/api/v2/auth/signup") ||
105105
path.startsWith("/api/v3/auth/signup") ||
106106
path.startsWith("/api/v3/auth/google") ||
107-
path.startsWith("/api/v3/enums/jobs")
107+
path.startsWith("/api/v3/enums/jobs") ||
108+
path.startsWith("/api/v3/auth/reissue")
108109
;
109110
}
110111
}

api/src/main/java/com/pinback/api/config/security/SecurityConfig.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
5656
"/api/v2/auth/google",
5757
"/api/v2/auth/signup",
5858
"/api/v3/auth/signup",
59-
"/api/v3/auth/google"
59+
"/api/v3/auth/google",
60+
"api/v3/auth/reissue"
6061
).permitAll()
6162

6263
.requestMatchers(

api/src/main/java/com/pinback/api/google/controller/GoogleLoginControllerV3.java

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
package com.pinback.api.google.controller;
22

3+
import org.springframework.beans.factory.annotation.Value;
4+
import org.springframework.http.HttpHeaders;
5+
import org.springframework.http.ResponseCookie;
6+
import org.springframework.http.ResponseEntity;
37
import org.springframework.web.bind.annotation.PatchMapping;
48
import org.springframework.web.bind.annotation.PostMapping;
59
import org.springframework.web.bind.annotation.RequestBody;
@@ -8,7 +12,7 @@
812

913
import com.pinback.api.auth.dto.request.SignUpRequestV3;
1014
import com.pinback.api.google.dto.request.GoogleLoginRequestV3;
11-
import com.pinback.application.auth.dto.SignUpResponse;
15+
import com.pinback.application.auth.dto.SignUpResponseV3;
1216
import com.pinback.application.auth.usecase.AuthUsecase;
1317
import com.pinback.application.google.dto.response.GoogleLoginResponseV3;
1418
import com.pinback.application.google.usecase.GoogleUsecase;
@@ -27,28 +31,51 @@
2731
public class GoogleLoginControllerV3 {
2832
private final GoogleUsecase googleUsecase;
2933
private final AuthUsecase authUsecase;
34+
@Value("${jwt.refreshExpirationPeriod}")
35+
private long refreshTokenExpirationPeriod;
3036

3137
@Operation(summary = "구글 소셜 로그인 V3", description = "구글 소셜 로그인을 진행하며, 응답에 직무 선택 여부를 포함합니다.")
3238
@PostMapping("/google")
33-
public Mono<ResponseDto<GoogleLoginResponseV3>> googleLogin(
39+
public Mono<ResponseEntity<ResponseDto<GoogleLoginResponseV3>>> googleLogin(
3440
@Valid @RequestBody GoogleLoginRequestV3 request
3541
) {
3642
return googleUsecase.getUserInfoV3(request.toCommand())
3743
.flatMap(googleResponse -> {
3844
return authUsecase.getInfoAndTokenV3(googleResponse.email(), googleResponse.pictureUrl(),
3945
googleResponse.name())
4046
.map(loginResponse -> {
41-
return ResponseDto.ok(loginResponse);
47+
ResponseCookie cookie = ResponseCookie.from("refreshToken", loginResponse.refreshToken())
48+
.httpOnly(true)
49+
.secure(true)
50+
.path("/")
51+
.maxAge(refreshTokenExpirationPeriod / 1000)
52+
.sameSite("None")
53+
.build();
54+
55+
return ResponseEntity.ok()
56+
.header(HttpHeaders.SET_COOKIE, cookie.toString())
57+
.body(ResponseDto.ok(loginResponse));
4258
});
4359
});
4460
}
4561

4662
@Operation(summary = "신규 회원 온보딩 V3", description = "신규 회원의 기본 정보(직무 포함)를 등록합니다.")
4763
@PatchMapping("/signup")
48-
public ResponseDto<SignUpResponse> signUpV3(
64+
public ResponseEntity<ResponseDto<SignUpResponseV3>> signUpV3(
4965
@Valid @RequestBody SignUpRequestV3 request
5066
) {
51-
SignUpResponse response = authUsecase.signUpV3(request.toCommand());
52-
return ResponseDto.ok(response);
67+
SignUpResponseV3 response = authUsecase.signUpV3(request.toCommand());
68+
69+
ResponseCookie cookie = ResponseCookie.from("refreshToken", response.refreshToken())
70+
.httpOnly(true)
71+
.secure(true)
72+
.path("/")
73+
.maxAge(refreshTokenExpirationPeriod / 1000)
74+
.sameSite("None")
75+
.build();
76+
77+
return ResponseEntity.ok()
78+
.header(HttpHeaders.SET_COOKIE, cookie.toString())
79+
.body(ResponseDto.ok(response));
5380
}
5481
}

api/src/main/resources/application.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ springdoc:
2424
jwt:
2525
secret-key: ${SECRET_KEY}
2626
accessExpirationPeriod: ${EXPIRATION_PERIOD}
27+
refreshExpirationPeriod: ${REFRESH_EXPIRATION_PERIOD}
2728
issuer: ${ISSUER}
2829

2930
fcm: ${FCM_JSON}

application/build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,6 @@ dependencies {
1717
implementation 'org.springframework.boot:spring-boot-starter-webflux'
1818

1919
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
20+
21+
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
2022
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.pinback.application.auth.dto;
2+
3+
public record SignUpResponseV3(
4+
String accessToken,
5+
String refreshToken
6+
) {
7+
public static SignUpResponseV3 from(String accessToken, String refreshToken) {
8+
return new SignUpResponseV3(accessToken, refreshToken);
9+
}
10+
}

application/src/main/java/com/pinback/application/auth/dto/TokenResponse.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,7 @@
33
public record TokenResponse(
44
String token
55
) {
6+
public static TokenResponse of(String token) {
7+
return new TokenResponse(token);
8+
}
69
}

application/src/main/java/com/pinback/application/auth/service/JwtProvider.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,6 @@ public interface JwtProvider {
88
boolean validateToken(String token);
99

1010
UUID getUserIdFromToken(String token);
11+
12+
String createRefreshToken(UUID userId);
1113
}

application/src/main/java/com/pinback/application/auth/usecase/AuthUsecase.java

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
11
package com.pinback.application.auth.usecase;
22

33
import java.time.LocalTime;
4+
import java.util.UUID;
45

6+
import org.springframework.beans.factory.annotation.Value;
7+
import org.springframework.data.redis.core.StringRedisTemplate;
58
import org.springframework.stereotype.Service;
69
import org.springframework.transaction.annotation.Transactional;
710

811
import com.pinback.application.auth.dto.SignUpCommand;
912
import com.pinback.application.auth.dto.SignUpCommandV3;
1013
import com.pinback.application.auth.dto.SignUpResponse;
14+
import com.pinback.application.auth.dto.SignUpResponseV3;
1115
import com.pinback.application.auth.dto.TokenResponse;
1216
import com.pinback.application.auth.service.JwtProvider;
17+
import com.pinback.application.common.exception.InvalidTokenException;
1318
import com.pinback.application.config.ProfileImageConfig;
1419
import com.pinback.application.google.dto.response.GoogleLoginResponse;
1520
import com.pinback.application.google.dto.response.GoogleLoginResponseV3;
@@ -30,6 +35,7 @@
3035
@Service
3136
@RequiredArgsConstructor
3237
public class AuthUsecase {
38+
private static final String REDIS_REFRESH_TOKEN_PREFIX = "RT:";
3339

3440
private final UserValidateServicePort userValidateServicePort;
3541
private final UserSaveServicePort userSaveServicePort;
@@ -40,6 +46,10 @@ public class AuthUsecase {
4046
private final UserUpdateServicePort userUpdateServicePort;
4147
private final UserOAuthUsecase userOAuthUsecase;
4248
private final ProfileImageConfig profileImageConfig;
49+
private final StringRedisTemplate stringRedisTemplate;
50+
51+
@Value("${jwt.refreshExpirationPeriod}")
52+
private long refreshTokenExpirationPeriod;
4353

4454
@Transactional
4555
public SignUpResponse signUp(SignUpCommand signUpCommand) {
@@ -123,9 +133,11 @@ public SignUpResponse signUpV2(SignUpCommand signUpCommand) {
123133
}
124134

125135
@Transactional
126-
public SignUpResponse signUpV3(SignUpCommandV3 signUpCommand) {
136+
public SignUpResponseV3 signUpV3(SignUpCommandV3 signUpCommand) {
127137
User user = userGetServicePort.findByEmail(signUpCommand.email());
128138
String accessToken = jwtProvider.createAccessToken(user.getId());
139+
String refreshToken = jwtProvider.createRefreshToken(user.getId());
140+
saveRefreshTokenToRedis(user.getId(), refreshToken);
129141
userUpdateServicePort.updateRemindDefault(user.getId(), signUpCommand.remindDefault());
130142

131143
savePushSubscriptionPort.savePushSubscription(user, signUpCommand.fcmToken());
@@ -135,7 +147,7 @@ public SignUpResponse signUpV3(SignUpCommandV3 signUpCommand) {
135147
Job job = Job.from(signUpCommand.job());
136148
userUpdateServicePort.updateJob(user.getId(), job);
137149

138-
return SignUpResponse.from(accessToken);
150+
return SignUpResponseV3.from(accessToken, refreshToken);
139151
}
140152

141153
@Transactional
@@ -149,11 +161,15 @@ public Mono<GoogleLoginResponseV3> getInfoAndTokenV3(String email, String pictur
149161
if (updatedUser.getRemindDefault() != null && updatedUser.getProfileImage() != null) {
150162
log.info("기존 사용자 로그인 성공: User ID {}", updatedUser.getId());
151163

152-
//Access Token 발급
164+
//Access Token & Refresh Token 발급
153165
String accessToken = jwtProvider.createAccessToken(updatedUser.getId());
166+
String refreshToken = jwtProvider.createRefreshToken(updatedUser.getId());
167+
168+
saveRefreshTokenToRedis(updatedUser.getId(), refreshToken);
154169

155170
return Mono.just(GoogleLoginResponseV3.loggedIn(
156-
updatedUser.hasJob(), updatedUser.getId(), updatedUser.getEmail(), accessToken
171+
updatedUser.hasJob(), updatedUser.getId(), updatedUser.getEmail(), accessToken,
172+
refreshToken
157173
));
158174
} else {
159175
log.info("기존 사용자 - 온보딩 미완료 유저 처리: User ID {}", updatedUser.getId());
@@ -185,6 +201,25 @@ public Mono<GoogleLoginResponseV3> getInfoAndTokenV3(String email, String pictur
185201
}));
186202
}
187203

204+
@Transactional
205+
public SignUpResponseV3 getNewToken(String refreshToken) {
206+
UUID userId = jwtProvider.getUserIdFromToken(refreshToken);
207+
208+
String redisKey = REDIS_REFRESH_TOKEN_PREFIX + userId.toString();
209+
String savedToken = stringRedisTemplate.opsForValue().get(redisKey);
210+
if (savedToken == null || !savedToken.equals(refreshToken)) {
211+
stringRedisTemplate.delete(redisKey);
212+
throw new InvalidTokenException();
213+
}
214+
215+
String newAccessToken = jwtProvider.createAccessToken(userId);
216+
String newRefreshToken = jwtProvider.createRefreshToken(userId);
217+
218+
saveRefreshTokenToRedis(userId, newRefreshToken);
219+
220+
return SignUpResponseV3.from(newAccessToken, newRefreshToken);
221+
}
222+
188223
private Mono<User> applyMissingUserInfo(User existingUser, String pictureUrl, String name) {
189224
// 1. 이름 업데이트
190225
boolean nameUpdated = false;
@@ -224,4 +259,13 @@ private String matchingProfileImage(LocalTime remindDefault) {
224259
return "IMAGE3";
225260
}
226261
}
262+
263+
private void saveRefreshTokenToRedis(UUID userId, String refreshToken) {
264+
stringRedisTemplate.opsForValue().set(
265+
"RT:" + userId.toString(),
266+
refreshToken,
267+
refreshTokenExpirationPeriod,
268+
java.util.concurrent.TimeUnit.MILLISECONDS
269+
);
270+
}
227271
}

0 commit comments

Comments
 (0)