From e432706d4b75ca885e887fce7ff0d1d2ff832232 Mon Sep 17 00:00:00 2001 From: jeonghyemin Date: Tue, 8 Jul 2025 16:42:50 +0900 Subject: [PATCH] =?UTF-8?q?feat(User,Token):=20=EB=A6=AC=ED=94=84=EB=A0=88?= =?UTF-8?q?=EC=8B=9C=20=ED=86=A0=ED=81=B0=20=EC=A4=91=EB=B3=B5=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../order/service/OrderService.java | 4 -- .../token/controller/TokenController.java | 17 ++++--- .../user/serivce/UserService.java | 48 +++++++++++++++++-- .../oauth2/OAuth2LoginSuccessHandler.java | 40 +++++++++------- .../token/controller/TokenController.java | 26 +++++----- .../domaincorerdb/token/entity/Token.java | 7 ++- .../token/repository/TokenRepository.java | 4 ++ 7 files changed, 96 insertions(+), 50 deletions(-) diff --git a/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/order/service/OrderService.java b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/order/service/OrderService.java index ab54a7c8..19ec26c7 100644 --- a/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/order/service/OrderService.java +++ b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/order/service/OrderService.java @@ -9,16 +9,12 @@ import com.nowait.applicationadmin.order.dto.OrderResponseDto; import com.nowait.applicationadmin.order.dto.OrderStatusUpdateResponseDto; import com.nowait.common.enums.Role; -import com.nowait.domaincorerdb.menu.entity.Menu; -import com.nowait.domaincorerdb.menu.exception.MenuNotFoundException; -import com.nowait.domaincorerdb.menu.repository.MenuRepository; import com.nowait.domaincorerdb.order.entity.OrderStatus; import com.nowait.domaincorerdb.order.entity.UserOrder; import com.nowait.domaincorerdb.order.exception.OrderNotFoundException; import com.nowait.domaincorerdb.order.exception.OrderUpdateUnauthorizedException; import com.nowait.domaincorerdb.order.exception.OrderViewUnauthorizedException; import com.nowait.domaincorerdb.order.repository.OrderRepository; -import com.nowait.domaincorerdb.store.entity.Store; import com.nowait.domaincorerdb.store.exception.StoreNotFoundException; import com.nowait.domaincorerdb.store.repository.StoreRepository; import com.nowait.domaincorerdb.user.entity.MemberDetails; diff --git a/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/token/controller/TokenController.java b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/token/controller/TokenController.java index 5b07ce24..3805508b 100644 --- a/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/token/controller/TokenController.java +++ b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/token/controller/TokenController.java @@ -3,14 +3,13 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.CookieValue; import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import com.nowait.applicationadmin.security.jwt.JwtUtil; import com.nowait.applicationadmin.token.dto.AuthenticationResponse; -import com.nowait.applicationadmin.token.dto.RefreshTokenRequest; import com.nowait.applicationadmin.token.service.TokenService; import io.swagger.v3.oas.annotations.Operation; @@ -35,25 +34,25 @@ public class TokenController { @PostMapping @Operation(summary = "리프레시 토큰", description = "리프레시 토큰을 사용하여 새로운 액세스 토큰과 리프레시 토큰을 발급합니다.") @ApiResponse(responseCode = "200", description = "새로운 액세스 토큰과 리프레시 토큰 발급 성공") - public ResponseEntity refreshToken(@RequestBody RefreshTokenRequest request){ - String refreshToken = request.getRefreshToken(); + public ResponseEntity refreshToken( + @CookieValue(value = "refreshToken", required = false) String refreshToken) { + + if (refreshToken == null) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Refresh token not found in cookies"); + } // 리프레시 토큰 검증 Long userId = jwtUtil.getUserId(refreshToken); String role = jwtUtil.getRole(refreshToken); - // 리프레시 토큰 유효성 검증 if (tokenService.validateToken(refreshToken, userId)){ - // 유효한 토큰이라면, 새로운 accessToken, refreshToken 생성 String newAccessToken = jwtUtil.createAccessToken("accessToken", userId, role, accessTokenExpiration); String newRefreshToken = jwtUtil.createRefreshToken("refreshToken", userId, refreshTokenExpiration); - // DB에 새로운 refreshToken으로 교체 tokenService.updateRefreshToken(userId, refreshToken, newRefreshToken); - AuthenticationResponse authenticationResponse = new AuthenticationResponse(newAccessToken, refreshToken); + AuthenticationResponse authenticationResponse = new AuthenticationResponse(newAccessToken, newRefreshToken); return ResponseEntity.ok().body(authenticationResponse); - } return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid or expired refresh token"); diff --git a/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/user/serivce/UserService.java b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/user/serivce/UserService.java index 669a202b..837d2b8b 100644 --- a/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/user/serivce/UserService.java +++ b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/user/serivce/UserService.java @@ -1,6 +1,12 @@ package com.nowait.applicationadmin.user.serivce; +import java.time.LocalDateTime; +import java.util.Optional; + import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseCookie; +import org.springframework.http.ResponseEntity; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; @@ -13,6 +19,8 @@ import com.nowait.applicationadmin.user.dto.ManagerLoginResponseDto; import com.nowait.applicationadmin.user.dto.ManagerSignupRequestDto; import com.nowait.applicationadmin.user.dto.ManagerSignupResponseDto; +import com.nowait.domaincorerdb.token.entity.Token; +import com.nowait.domaincorerdb.token.repository.TokenRepository; import com.nowait.domaincorerdb.user.entity.MemberDetails; import com.nowait.domaincorerdb.user.entity.User; import com.nowait.domaincorerdb.user.repository.UserRepository; @@ -25,6 +33,7 @@ @Slf4j public class UserService { private final UserRepository userRepository; + private final TokenRepository tokenRepository; private final PasswordEncoder passwordEncoder; private final AuthenticationProvider authenticationProvider; private final JwtUtil jwtUtil; @@ -54,19 +63,50 @@ private void validateNickNameDuplicated(String nickName) { ); } @Transactional - public ManagerLoginResponseDto login(ManagerLoginRequestDto managerLoginRequestDto) { + public ResponseEntity login(ManagerLoginRequestDto managerLoginRequestDto) { Authentication authentication = authenticationProvider.authenticate( - new UsernamePasswordAuthenticationToken(managerLoginRequestDto.getEmail(), managerLoginRequestDto.getPassword()) + new UsernamePasswordAuthenticationToken( + managerLoginRequestDto.getEmail(), + managerLoginRequestDto.getPassword() + ) ); MemberDetails memberDetails = (MemberDetails) authentication.getPrincipal(); User user = userRepository.getReferenceById(memberDetails.getId()); long currentAccessTokenExpiration = accessTokenExpiration; if (user.getRole() == com.nowait.common.enums.Role.SUPER_ADMIN) { - currentAccessTokenExpiration = 7L * 24 * 60 * 60 * 1000L; // 7일 + currentAccessTokenExpiration = 100L * 24 * 60 * 60 * 1000L; // 100일 } String accessToken = jwtUtil.createAccessToken("accessToken", user.getId(), String.valueOf(user.getRole()), currentAccessTokenExpiration); - return ManagerLoginResponseDto.fromEntity(user,accessToken); + String refreshToken = jwtUtil.createRefreshToken("refreshToken", user.getId(), 30L * 24 * 60 * 60 * 1000L); + + // 기존 토큰 존재 확인 + Optional tokenOptional = tokenRepository.findByUserId(user.getId()); + if (tokenOptional.isPresent()) { + Token token = tokenOptional.get(); + token.updateRefreshToken(refreshToken, LocalDateTime.now().plusDays(30L)); // 엔티티에 update 메소드 구현 권장 + } else { + tokenRepository.save( + Token.builder() + .user(user) + .refreshToken(refreshToken) + .expiredDate(LocalDateTime.now().plusDays(30L)) + .build() + ); + } + ResponseCookie refreshTokenCookie = ResponseCookie.from("refreshToken", refreshToken) + .httpOnly(true) + .secure(true) // 운영환경에 맞게 + .path("/") + .maxAge(30L * 24 * 60 * 60) + .sameSite("Strict") + .build(); + + return ResponseEntity.ok() + .header(HttpHeaders.SET_COOKIE, refreshTokenCookie.toString()) + .body(ManagerLoginResponseDto.fromEntity(user, accessToken)); + } + } diff --git a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/oauth/oauth2/OAuth2LoginSuccessHandler.java b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/oauth/oauth2/OAuth2LoginSuccessHandler.java index 0417d1c9..f583cc28 100644 --- a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/oauth/oauth2/OAuth2LoginSuccessHandler.java +++ b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/oauth/oauth2/OAuth2LoginSuccessHandler.java @@ -2,23 +2,20 @@ import java.io.IOException; import java.time.LocalDateTime; -import java.util.Collection; -import java.util.Iterator; -import java.util.Map; +import java.util.Optional; +import org.springframework.http.ResponseCookie; import org.springframework.security.core.Authentication; -import org.springframework.security.core.GrantedAuthority; import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; -import com.fasterxml.jackson.databind.ObjectMapper; import com.nowait.applicationuser.security.jwt.JwtUtil; import com.nowait.domaincorerdb.token.entity.Token; import com.nowait.domaincorerdb.token.repository.TokenRepository; import com.nowait.domaincorerdb.user.entity.User; import com.nowait.domainuserrdb.oauth.dto.CustomOAuth2User; -import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; @@ -37,6 +34,7 @@ public class OAuth2LoginSuccessHandler extends SimpleUrlAuthenticationSuccessHan private final TokenRepository tokenRepository; @Override + @Transactional public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { @@ -49,19 +47,27 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo String accessToken = jwtUtil.createAccessToken("accessToken", userId, role, 30 * 60 * 1000L); // 30분 String refreshToken = jwtUtil.createRefreshToken("refreshToken", userId, 30L * 24 * 60 * 60 * 1000L); // 30일 - // 1. refreshToken을 DB에 저장 - Token refreshTokenEntity = Token.toEntity(user, refreshToken, LocalDateTime.now().plusDays(30)); - tokenRepository.save(refreshTokenEntity); + // 1. refreshToken을 DB에 저장 or update + Optional tokenOptional = tokenRepository.findByUserId(user.getId()); + if (tokenOptional.isPresent()) { + Token token = tokenOptional.get(); + token.updateRefreshToken(refreshToken, LocalDateTime.now().plusDays(30)); + } else { + Token token = Token.toEntity(user, refreshToken, LocalDateTime.now().plusDays(30)); + tokenRepository.save(token); + } - // 2. refreshToken을 HttpOnly 쿠키로 설정 - Cookie refreshTokenCookie = new Cookie("refreshToken", refreshToken); - refreshTokenCookie.setHttpOnly(true); // JS 접근 불가 - refreshTokenCookie.setSecure(false); // 운영환경 https라면 true로 변경 필요 - refreshTokenCookie.setPath("/"); - refreshTokenCookie.setMaxAge(30 * 24 * 60 * 60); // 30일 - response.addCookie(refreshTokenCookie); - response.addHeader("Set-Cookie", response.getHeader("Set-Cookie") + "; SameSite=Lax"); + // 2. refreshToken을 HttpOnly 쿠키로 설정 (ResponseCookie로) + ResponseCookie refreshTokenCookie = ResponseCookie.from("refreshToken", refreshToken) + .httpOnly(true) + .secure(false) // 운영환경에서는 true + .path("/") + .maxAge(30L * 24 * 60 * 60) // 30일 (초 단위) + .sameSite("Lax") + .build(); + // 기존 방식 대신 ResponseCookie.toString()을 헤더로 추가 + response.setHeader("Set-Cookie", refreshTokenCookie.toString()); // 3. 프론트엔드로 리다이렉트 (accessToken만 쿼리로 전달) String targetUrl = "http://localhost:5173/login/success?accessToken=" + accessToken; diff --git a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/token/controller/TokenController.java b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/token/controller/TokenController.java index f1929331..bc04bc11 100644 --- a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/token/controller/TokenController.java +++ b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/token/controller/TokenController.java @@ -3,6 +3,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.CookieValue; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -31,32 +32,27 @@ public class TokenController { private long refreshTokenExpiration; @PostMapping - @Operation(summary = "리프레시 토큰으로 새로운 액세스 토큰 발급", description = "리프레시 토큰을 사용하여 새로운 액세스 토큰을 발급합니다.") - @ApiResponse(responseCode = "200", description = "새로운 액세스 토큰 발급 성공") - public ResponseEntity refreshToken(@RequestBody RefreshTokenRequest request){ - String refreshToken = request.getRefreshToken(); + @Operation(summary = "리프레시 토큰", description = "리프레시 토큰을 사용하여 새로운 액세스 토큰과 리프레시 토큰을 발급합니다.") + @ApiResponse(responseCode = "200", description = "새로운 액세스 토큰과 리프레시 토큰 발급 성공") + public ResponseEntity refreshToken( + @CookieValue(value = "refreshToken", required = false) String refreshToken) { + + if (refreshToken == null) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Refresh token not found in cookies"); + } // 리프레시 토큰 검증 Long userId = jwtUtil.getUserId(refreshToken); String role = jwtUtil.getRole(refreshToken); - long currentAccessTokenExpiration = accessTokenExpiration; - if (role.equals("SUPER_ADMIN")) { - currentAccessTokenExpiration = 100L * 24 * 60 * 60 * 1000L; // 100일 - } - - // 리프레시 토큰 유효성 검증 if (tokenService.validateToken(refreshToken, userId)){ - // 유효한 토큰이라면, 새로운 accessToken, refreshToken 생성 - String newAccessToken = jwtUtil.createAccessToken("accessToken", userId, role, currentAccessTokenExpiration); + String newAccessToken = jwtUtil.createAccessToken("accessToken", userId, role, accessTokenExpiration); String newRefreshToken = jwtUtil.createRefreshToken("refreshToken", userId, refreshTokenExpiration); - // DB에 새로운 refreshToken으로 교체 tokenService.updateRefreshToken(userId, refreshToken, newRefreshToken); - AuthenticationResponse authenticationResponse = new AuthenticationResponse(newAccessToken, refreshToken); + AuthenticationResponse authenticationResponse = new AuthenticationResponse(newAccessToken, newRefreshToken); return ResponseEntity.ok().body(authenticationResponse); - } return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid or expired refresh token"); diff --git a/nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/token/entity/Token.java b/nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/token/entity/Token.java index 14378dd6..29206c5f 100644 --- a/nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/token/entity/Token.java +++ b/nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/token/entity/Token.java @@ -28,7 +28,7 @@ public class Token { private Long tokenId; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id", nullable = false) + @JoinColumn(name = "user_id", nullable = false,unique = true) private User user; @Column @@ -52,4 +52,9 @@ public static Token toEntity(User user, String refreshToken, LocalDateTime expir .build(); } + public void updateRefreshToken(String refreshToken, LocalDateTime expiredDate) { + this.refreshToken = refreshToken; + this.expiredDate = expiredDate; + } + } diff --git a/nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/token/repository/TokenRepository.java b/nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/token/repository/TokenRepository.java index ab74bc46..4cc7b9ec 100644 --- a/nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/token/repository/TokenRepository.java +++ b/nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/token/repository/TokenRepository.java @@ -6,8 +6,12 @@ import org.springframework.stereotype.Repository; import com.nowait.domaincorerdb.token.entity.Token; +import com.nowait.domaincorerdb.user.entity.User; @Repository public interface TokenRepository extends JpaRepository { Optional findByUserId(Long userId); + + Optional findByUser(User user); + }