Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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");
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -54,19 +63,50 @@ private void validateNickNameDuplicated(String nickName) {
);
}
@Transactional
public ManagerLoginResponseDto login(ManagerLoginRequestDto managerLoginRequestDto) {
public ResponseEntity<ManagerLoginResponseDto> 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일
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

SUPER_ADMIN 토큰 만료 시간이 너무 길 수 있습니다.

100일의 액세스 토큰 만료 시간은 보안상 위험할 수 있습니다. 더 짧은 기간(예: 7일)을 고려해보세요.

-			currentAccessTokenExpiration = 100L * 24 * 60 * 60 * 1000L; // 100일
+			currentAccessTokenExpiration = 7L * 24 * 60 * 60 * 1000L; // 7일
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
currentAccessTokenExpiration = 100L * 24 * 60 * 60 * 1000L; // 100일
currentAccessTokenExpiration = 7L * 24 * 60 * 60 * 1000L; // 7일
🤖 Prompt for AI Agents
In
nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/user/serivce/UserService.java
at line 78, the currentAccessTokenExpiration is set to 100 days, which is too
long for a SUPER_ADMIN token. Change the expiration time to a shorter duration
such as 7 days by adjusting the multiplier accordingly to reduce security risks.

}

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);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

하드코딩된 만료 시간을 설정값으로 추출하세요.

30일 만료 시간이 여러 곳에 하드코딩되어 있습니다. 유지보수성을 위해 설정값으로 추출하는 것을 권장합니다.

설정 파일에 다음을 추가하세요:

jwt.refresh-token-expiration-days=30

그리고 다음과 같이 수정하세요:

+@Value("${jwt.refresh-token-expiration-days}")
+private long refreshTokenExpirationDays;

-String refreshToken = jwtUtil.createRefreshToken("refreshToken", user.getId(), 30L * 24 * 60 * 60 * 1000L);
+String refreshToken = jwtUtil.createRefreshToken("refreshToken", user.getId(), refreshTokenExpirationDays * 24 * 60 * 60 * 1000L);

-token.updateRefreshToken(refreshToken, LocalDateTime.now().plusDays(30L));
+token.updateRefreshToken(refreshToken, LocalDateTime.now().plusDays(refreshTokenExpirationDays));

-expiredDate(LocalDateTime.now().plusDays(30L))
+expiredDate(LocalDateTime.now().plusDays(refreshTokenExpirationDays))

-maxAge(30L * 24 * 60 * 60)
+maxAge(refreshTokenExpirationDays * 24 * 60 * 60)

Also applies to: 88-88, 94-94, 102-102

🤖 Prompt for AI Agents
In
nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/user/serivce/UserService.java
at lines 82, 88, 94, and 102, the refresh token expiration time is hardcoded as
30 days in milliseconds. To improve maintainability, extract this hardcoded
value into a configuration property by adding
'jwt.refresh-token-expiration-days=30' to the properties file. Then, modify the
code to read this value from the configuration, convert it to milliseconds, and
use it instead of the hardcoded literal.


// 기존 토큰 존재 확인
Optional<Token> 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));

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {

Expand All @@ -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<Token> 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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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<Token, Long> {
Optional<Token> findByUserId(Long userId);

Optional<Token> findByUser(User user);

}