Skip to content

Commit fe7a7cb

Browse files
authored
Merge pull request #70 from GTable/feature/#69-리프레시토큰중복저장로직수정
feat(User,Token): 리프레시 토큰 중복 저장 로직 수정
2 parents a68a42f + e432706 commit fe7a7cb

7 files changed

Lines changed: 96 additions & 50 deletions

File tree

nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/order/service/OrderService.java

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,12 @@
99
import com.nowait.applicationadmin.order.dto.OrderResponseDto;
1010
import com.nowait.applicationadmin.order.dto.OrderStatusUpdateResponseDto;
1111
import com.nowait.common.enums.Role;
12-
import com.nowait.domaincorerdb.menu.entity.Menu;
13-
import com.nowait.domaincorerdb.menu.exception.MenuNotFoundException;
14-
import com.nowait.domaincorerdb.menu.repository.MenuRepository;
1512
import com.nowait.domaincorerdb.order.entity.OrderStatus;
1613
import com.nowait.domaincorerdb.order.entity.UserOrder;
1714
import com.nowait.domaincorerdb.order.exception.OrderNotFoundException;
1815
import com.nowait.domaincorerdb.order.exception.OrderUpdateUnauthorizedException;
1916
import com.nowait.domaincorerdb.order.exception.OrderViewUnauthorizedException;
2017
import com.nowait.domaincorerdb.order.repository.OrderRepository;
21-
import com.nowait.domaincorerdb.store.entity.Store;
2218
import com.nowait.domaincorerdb.store.exception.StoreNotFoundException;
2319
import com.nowait.domaincorerdb.store.repository.StoreRepository;
2420
import com.nowait.domaincorerdb.user.entity.MemberDetails;

nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/token/controller/TokenController.java

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,13 @@
33
import org.springframework.beans.factory.annotation.Value;
44
import org.springframework.http.HttpStatus;
55
import org.springframework.http.ResponseEntity;
6+
import org.springframework.web.bind.annotation.CookieValue;
67
import org.springframework.web.bind.annotation.PostMapping;
7-
import org.springframework.web.bind.annotation.RequestBody;
88
import org.springframework.web.bind.annotation.RequestMapping;
99
import org.springframework.web.bind.annotation.RestController;
1010

1111
import com.nowait.applicationadmin.security.jwt.JwtUtil;
1212
import com.nowait.applicationadmin.token.dto.AuthenticationResponse;
13-
import com.nowait.applicationadmin.token.dto.RefreshTokenRequest;
1413
import com.nowait.applicationadmin.token.service.TokenService;
1514

1615
import io.swagger.v3.oas.annotations.Operation;
@@ -35,25 +34,25 @@ public class TokenController {
3534
@PostMapping
3635
@Operation(summary = "리프레시 토큰", description = "리프레시 토큰을 사용하여 새로운 액세스 토큰과 리프레시 토큰을 발급합니다.")
3736
@ApiResponse(responseCode = "200", description = "새로운 액세스 토큰과 리프레시 토큰 발급 성공")
38-
public ResponseEntity<?> refreshToken(@RequestBody RefreshTokenRequest request){
39-
String refreshToken = request.getRefreshToken();
37+
public ResponseEntity<?> refreshToken(
38+
@CookieValue(value = "refreshToken", required = false) String refreshToken) {
39+
40+
if (refreshToken == null) {
41+
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Refresh token not found in cookies");
42+
}
4043

4144
// 리프레시 토큰 검증
4245
Long userId = jwtUtil.getUserId(refreshToken);
4346
String role = jwtUtil.getRole(refreshToken);
4447

45-
// 리프레시 토큰 유효성 검증
4648
if (tokenService.validateToken(refreshToken, userId)){
47-
// 유효한 토큰이라면, 새로운 accessToken, refreshToken 생성
4849
String newAccessToken = jwtUtil.createAccessToken("accessToken", userId, role, accessTokenExpiration);
4950
String newRefreshToken = jwtUtil.createRefreshToken("refreshToken", userId, refreshTokenExpiration);
5051

51-
// DB에 새로운 refreshToken으로 교체
5252
tokenService.updateRefreshToken(userId, refreshToken, newRefreshToken);
5353

54-
AuthenticationResponse authenticationResponse = new AuthenticationResponse(newAccessToken, refreshToken);
54+
AuthenticationResponse authenticationResponse = new AuthenticationResponse(newAccessToken, newRefreshToken);
5555
return ResponseEntity.ok().body(authenticationResponse);
56-
5756
}
5857

5958
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid or expired refresh token");

nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/user/serivce/UserService.java

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
package com.nowait.applicationadmin.user.serivce;
22

3+
import java.time.LocalDateTime;
4+
import java.util.Optional;
5+
36
import org.springframework.beans.factory.annotation.Value;
7+
import org.springframework.http.HttpHeaders;
8+
import org.springframework.http.ResponseCookie;
9+
import org.springframework.http.ResponseEntity;
410
import org.springframework.security.authentication.AuthenticationProvider;
511
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
612
import org.springframework.security.core.Authentication;
@@ -13,6 +19,8 @@
1319
import com.nowait.applicationadmin.user.dto.ManagerLoginResponseDto;
1420
import com.nowait.applicationadmin.user.dto.ManagerSignupRequestDto;
1521
import com.nowait.applicationadmin.user.dto.ManagerSignupResponseDto;
22+
import com.nowait.domaincorerdb.token.entity.Token;
23+
import com.nowait.domaincorerdb.token.repository.TokenRepository;
1624
import com.nowait.domaincorerdb.user.entity.MemberDetails;
1725
import com.nowait.domaincorerdb.user.entity.User;
1826
import com.nowait.domaincorerdb.user.repository.UserRepository;
@@ -25,6 +33,7 @@
2533
@Slf4j
2634
public class UserService {
2735
private final UserRepository userRepository;
36+
private final TokenRepository tokenRepository;
2837
private final PasswordEncoder passwordEncoder;
2938
private final AuthenticationProvider authenticationProvider;
3039
private final JwtUtil jwtUtil;
@@ -54,19 +63,50 @@ private void validateNickNameDuplicated(String nickName) {
5463
);
5564
}
5665
@Transactional
57-
public ManagerLoginResponseDto login(ManagerLoginRequestDto managerLoginRequestDto) {
66+
public ResponseEntity<ManagerLoginResponseDto> login(ManagerLoginRequestDto managerLoginRequestDto) {
5867
Authentication authentication = authenticationProvider.authenticate(
59-
new UsernamePasswordAuthenticationToken(managerLoginRequestDto.getEmail(), managerLoginRequestDto.getPassword())
68+
new UsernamePasswordAuthenticationToken(
69+
managerLoginRequestDto.getEmail(),
70+
managerLoginRequestDto.getPassword()
71+
)
6072
);
6173
MemberDetails memberDetails = (MemberDetails) authentication.getPrincipal();
6274
User user = userRepository.getReferenceById(memberDetails.getId());
6375

6476
long currentAccessTokenExpiration = accessTokenExpiration;
6577
if (user.getRole() == com.nowait.common.enums.Role.SUPER_ADMIN) {
66-
currentAccessTokenExpiration = 7L * 24 * 60 * 60 * 1000L; // 7일
78+
currentAccessTokenExpiration = 100L * 24 * 60 * 60 * 1000L; // 100일
6779
}
6880

6981
String accessToken = jwtUtil.createAccessToken("accessToken", user.getId(), String.valueOf(user.getRole()), currentAccessTokenExpiration);
70-
return ManagerLoginResponseDto.fromEntity(user,accessToken);
82+
String refreshToken = jwtUtil.createRefreshToken("refreshToken", user.getId(), 30L * 24 * 60 * 60 * 1000L);
83+
84+
// 기존 토큰 존재 확인
85+
Optional<Token> tokenOptional = tokenRepository.findByUserId(user.getId());
86+
if (tokenOptional.isPresent()) {
87+
Token token = tokenOptional.get();
88+
token.updateRefreshToken(refreshToken, LocalDateTime.now().plusDays(30L)); // 엔티티에 update 메소드 구현 권장
89+
} else {
90+
tokenRepository.save(
91+
Token.builder()
92+
.user(user)
93+
.refreshToken(refreshToken)
94+
.expiredDate(LocalDateTime.now().plusDays(30L))
95+
.build()
96+
);
97+
}
98+
ResponseCookie refreshTokenCookie = ResponseCookie.from("refreshToken", refreshToken)
99+
.httpOnly(true)
100+
.secure(true) // 운영환경에 맞게
101+
.path("/")
102+
.maxAge(30L * 24 * 60 * 60)
103+
.sameSite("Strict")
104+
.build();
105+
106+
return ResponseEntity.ok()
107+
.header(HttpHeaders.SET_COOKIE, refreshTokenCookie.toString())
108+
.body(ManagerLoginResponseDto.fromEntity(user, accessToken));
109+
71110
}
111+
72112
}

nowait-app-user-api/src/main/java/com/nowait/applicationuser/oauth/oauth2/OAuth2LoginSuccessHandler.java

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,20 @@
22

33
import java.io.IOException;
44
import java.time.LocalDateTime;
5-
import java.util.Collection;
6-
import java.util.Iterator;
7-
import java.util.Map;
5+
import java.util.Optional;
86

7+
import org.springframework.http.ResponseCookie;
98
import org.springframework.security.core.Authentication;
10-
import org.springframework.security.core.GrantedAuthority;
119
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
1210
import org.springframework.stereotype.Component;
11+
import org.springframework.transaction.annotation.Transactional;
1312

14-
import com.fasterxml.jackson.databind.ObjectMapper;
1513
import com.nowait.applicationuser.security.jwt.JwtUtil;
1614
import com.nowait.domaincorerdb.token.entity.Token;
1715
import com.nowait.domaincorerdb.token.repository.TokenRepository;
1816
import com.nowait.domaincorerdb.user.entity.User;
1917
import com.nowait.domainuserrdb.oauth.dto.CustomOAuth2User;
2018

21-
import jakarta.servlet.http.Cookie;
2219
import jakarta.servlet.http.HttpServletRequest;
2320
import jakarta.servlet.http.HttpServletResponse;
2421
import lombok.RequiredArgsConstructor;
@@ -37,6 +34,7 @@ public class OAuth2LoginSuccessHandler extends SimpleUrlAuthenticationSuccessHan
3734
private final TokenRepository tokenRepository;
3835

3936
@Override
37+
@Transactional
4038
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
4139
Authentication authentication) throws IOException {
4240

@@ -49,19 +47,27 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo
4947
String accessToken = jwtUtil.createAccessToken("accessToken", userId, role, 30 * 60 * 1000L); // 30분
5048
String refreshToken = jwtUtil.createRefreshToken("refreshToken", userId, 30L * 24 * 60 * 60 * 1000L); // 30일
5149

52-
// 1. refreshToken을 DB에 저장
53-
Token refreshTokenEntity = Token.toEntity(user, refreshToken, LocalDateTime.now().plusDays(30));
54-
tokenRepository.save(refreshTokenEntity);
50+
// 1. refreshToken을 DB에 저장 or update
51+
Optional<Token> tokenOptional = tokenRepository.findByUserId(user.getId());
52+
if (tokenOptional.isPresent()) {
53+
Token token = tokenOptional.get();
54+
token.updateRefreshToken(refreshToken, LocalDateTime.now().plusDays(30));
55+
} else {
56+
Token token = Token.toEntity(user, refreshToken, LocalDateTime.now().plusDays(30));
57+
tokenRepository.save(token);
58+
}
5559

56-
// 2. refreshToken을 HttpOnly 쿠키로 설정
57-
Cookie refreshTokenCookie = new Cookie("refreshToken", refreshToken);
58-
refreshTokenCookie.setHttpOnly(true); // JS 접근 불가
59-
refreshTokenCookie.setSecure(false); // 운영환경 https라면 true로 변경 필요
60-
refreshTokenCookie.setPath("/");
61-
refreshTokenCookie.setMaxAge(30 * 24 * 60 * 60); // 30일
62-
response.addCookie(refreshTokenCookie);
63-
response.addHeader("Set-Cookie", response.getHeader("Set-Cookie") + "; SameSite=Lax");
60+
// 2. refreshToken을 HttpOnly 쿠키로 설정 (ResponseCookie로)
61+
ResponseCookie refreshTokenCookie = ResponseCookie.from("refreshToken", refreshToken)
62+
.httpOnly(true)
63+
.secure(false) // 운영환경에서는 true
64+
.path("/")
65+
.maxAge(30L * 24 * 60 * 60) // 30일 (초 단위)
66+
.sameSite("Lax")
67+
.build();
6468

69+
// 기존 방식 대신 ResponseCookie.toString()을 헤더로 추가
70+
response.setHeader("Set-Cookie", refreshTokenCookie.toString());
6571

6672
// 3. 프론트엔드로 리다이렉트 (accessToken만 쿼리로 전달)
6773
String targetUrl = "http://localhost:5173/login/success?accessToken=" + accessToken;

nowait-app-user-api/src/main/java/com/nowait/applicationuser/token/controller/TokenController.java

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import org.springframework.beans.factory.annotation.Value;
44
import org.springframework.http.HttpStatus;
55
import org.springframework.http.ResponseEntity;
6+
import org.springframework.web.bind.annotation.CookieValue;
67
import org.springframework.web.bind.annotation.PostMapping;
78
import org.springframework.web.bind.annotation.RequestBody;
89
import org.springframework.web.bind.annotation.RequestMapping;
@@ -31,32 +32,27 @@ public class TokenController {
3132
private long refreshTokenExpiration;
3233

3334
@PostMapping
34-
@Operation(summary = "리프레시 토큰으로 새로운 액세스 토큰 발급", description = "리프레시 토큰을 사용하여 새로운 액세스 토큰을 발급합니다.")
35-
@ApiResponse(responseCode = "200", description = "새로운 액세스 토큰 발급 성공")
36-
public ResponseEntity<?> refreshToken(@RequestBody RefreshTokenRequest request){
37-
String refreshToken = request.getRefreshToken();
35+
@Operation(summary = "리프레시 토큰", description = "리프레시 토큰을 사용하여 새로운 액세스 토큰과 리프레시 토큰을 발급합니다.")
36+
@ApiResponse(responseCode = "200", description = "새로운 액세스 토큰과 리프레시 토큰 발급 성공")
37+
public ResponseEntity<?> refreshToken(
38+
@CookieValue(value = "refreshToken", required = false) String refreshToken) {
39+
40+
if (refreshToken == null) {
41+
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Refresh token not found in cookies");
42+
}
3843

3944
// 리프레시 토큰 검증
4045
Long userId = jwtUtil.getUserId(refreshToken);
4146
String role = jwtUtil.getRole(refreshToken);
4247

43-
long currentAccessTokenExpiration = accessTokenExpiration;
44-
if (role.equals("SUPER_ADMIN")) {
45-
currentAccessTokenExpiration = 100L * 24 * 60 * 60 * 1000L; // 100일
46-
}
47-
48-
// 리프레시 토큰 유효성 검증
4948
if (tokenService.validateToken(refreshToken, userId)){
50-
// 유효한 토큰이라면, 새로운 accessToken, refreshToken 생성
51-
String newAccessToken = jwtUtil.createAccessToken("accessToken", userId, role, currentAccessTokenExpiration);
49+
String newAccessToken = jwtUtil.createAccessToken("accessToken", userId, role, accessTokenExpiration);
5250
String newRefreshToken = jwtUtil.createRefreshToken("refreshToken", userId, refreshTokenExpiration);
5351

54-
// DB에 새로운 refreshToken으로 교체
5552
tokenService.updateRefreshToken(userId, refreshToken, newRefreshToken);
5653

57-
AuthenticationResponse authenticationResponse = new AuthenticationResponse(newAccessToken, refreshToken);
54+
AuthenticationResponse authenticationResponse = new AuthenticationResponse(newAccessToken, newRefreshToken);
5855
return ResponseEntity.ok().body(authenticationResponse);
59-
6056
}
6157

6258
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid or expired refresh token");

nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/token/entity/Token.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public class Token {
2828
private Long tokenId;
2929

3030
@ManyToOne(fetch = FetchType.LAZY)
31-
@JoinColumn(name = "user_id", nullable = false)
31+
@JoinColumn(name = "user_id", nullable = false,unique = true)
3232
private User user;
3333

3434
@Column
@@ -52,4 +52,9 @@ public static Token toEntity(User user, String refreshToken, LocalDateTime expir
5252
.build();
5353
}
5454

55+
public void updateRefreshToken(String refreshToken, LocalDateTime expiredDate) {
56+
this.refreshToken = refreshToken;
57+
this.expiredDate = expiredDate;
58+
}
59+
5560
}

nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/token/repository/TokenRepository.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,12 @@
66
import org.springframework.stereotype.Repository;
77

88
import com.nowait.domaincorerdb.token.entity.Token;
9+
import com.nowait.domaincorerdb.user.entity.User;
910

1011
@Repository
1112
public interface TokenRepository extends JpaRepository<Token, Long> {
1213
Optional<Token> findByUserId(Long userId);
14+
15+
Optional<Token> findByUser(User user);
16+
1317
}

0 commit comments

Comments
 (0)