diff --git a/backend/src/main/java/org/sejongisc/backend/common/auth/filter/JwtAuthenticationFilter.java b/backend/src/main/java/org/sejongisc/backend/common/auth/filter/JwtAuthenticationFilter.java index 949d1c6d..4b26d634 100644 --- a/backend/src/main/java/org/sejongisc/backend/common/auth/filter/JwtAuthenticationFilter.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/filter/JwtAuthenticationFilter.java @@ -13,7 +13,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.sejongisc.backend.common.auth.jwt.JwtParser; +import org.sejongisc.backend.common.auth.jwt.JwtUtils; import org.sejongisc.backend.common.config.security.SecurityConstants; import org.sejongisc.backend.common.exception.ErrorCode; import org.sejongisc.backend.common.exception.ErrorResponse; @@ -31,7 +31,7 @@ @RequiredArgsConstructor public class JwtAuthenticationFilter extends OncePerRequestFilter { - private final JwtParser jwtParser; + private final JwtUtils jwtUtils; private final AntPathMatcher pathMatcher = new AntPathMatcher(); private final ObjectMapper objectMapper; @@ -55,8 +55,8 @@ protected void doFilterInternal(@NotNull HttpServletRequest request, token = resolveTokenFromCookie(request); } - if (token != null && jwtParser.validationToken(token) ) { - UsernamePasswordAuthenticationToken authentication = jwtParser.getAuthentication(token); + if (token != null && jwtUtils.validationToken(token) ) { + UsernamePasswordAuthenticationToken authentication = jwtUtils.getAuthentication(token); SecurityContextHolder.getContext().setAuthentication(authentication); log.debug("SecurityContext에 인증 저장됨: {}", authentication.getName()); } else { diff --git a/backend/src/main/java/org/sejongisc/backend/common/auth/jwt/JwtProvider.java b/backend/src/main/java/org/sejongisc/backend/common/auth/jwt/JwtProvider.java deleted file mode 100644 index 73009ac5..00000000 --- a/backend/src/main/java/org/sejongisc/backend/common/auth/jwt/JwtProvider.java +++ /dev/null @@ -1,115 +0,0 @@ -package org.sejongisc.backend.common.auth.jwt; - -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.JwtException; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.security.Keys; -import jakarta.annotation.PostConstruct; -import lombok.RequiredArgsConstructor; -import org.sejongisc.backend.user.entity.Role; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; - -import javax.crypto.SecretKey; -import java.nio.charset.StandardCharsets; -import java.util.Base64; -import java.util.Date; -import java.util.UUID; - -@Component -@RequiredArgsConstructor -public class JwtProvider { - - private final TokenEncryptor tokenEncryptor; - - @Value("${jwt.secret}") - private String rawSecretKey; - - private SecretKey secretKey; - - @Value("${jwt.expireDate.accessToken}") - private long accessTokenValidityInMillis; - - @Value("${jwt.expireDate.refreshToken}") - private long refreshTokenValidityInMillis; - - @PostConstruct - public void init() { - //byte[] keyBytes = Base64.getDecoder().decode(rawSecretKey); // rawSecretKey의 base64 규격 이슈로 아래 코드로 대체 - this.secretKey = Keys.hmacShaKeyFor(rawSecretKey.getBytes(StandardCharsets.UTF_8)); - } - - // JWT 토큰 생성 - public String createToken(UUID userId, Role role, String email) { - Date now = new Date(); - Date expiryDate = new Date(now.getTime() + accessTokenValidityInMillis); - - return Jwts.builder() - .setSubject(email) - .claim("uid", userId.toString()) - .claim("role", role.name()) - .setIssuedAt(now) - .setExpiration(expiryDate) - .signWith(secretKey, SignatureAlgorithm.HS256) - .compact(); - } - - // RefreshToken - public String createRefreshToken(UUID userId) { - Date now = new Date(); - Date expiryDate = new Date(now.getTime() + refreshTokenValidityInMillis); - - String rawRefreshToken = Jwts.builder() - .setSubject(userId.toString()) - .setIssuedAt(now) - .setExpiration(expiryDate) - .signWith(secretKey, SignatureAlgorithm.HS256) - .compact(); - - return tokenEncryptor.encrypt(rawRefreshToken); - } - - public Date getExpiration(String token) { - return Jwts.parserBuilder() - .setSigningKey(secretKey) - .build() - .parseClaimsJws(token) - .getBody() - .getExpiration(); - } - - // 토큰에서 사용자 ID 추출 - public String getUserIdFromToken(String token) { - Claims claims = Jwts.parserBuilder() - .setSigningKey(secretKey) - .build() - .parseClaimsJws(token) - .getBody(); - - return claims.getSubject(); - } - - public String getRoleFromToken(String token) { - Claims claims = Jwts.parserBuilder() - .setSigningKey(secretKey) - .build() - .parseClaimsJws(token) - .getBody(); - - return claims.get("role", String.class); - } - - - - // 토큰 유효성 검증 - public boolean validationToken(String token) { - try { - Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(token); - return true; - } catch (JwtException | IllegalArgumentException e) { - return false; - } - } - -} \ No newline at end of file diff --git a/backend/src/main/java/org/sejongisc/backend/common/auth/jwt/JwtParser.java b/backend/src/main/java/org/sejongisc/backend/common/auth/jwt/JwtUtils.java similarity index 53% rename from backend/src/main/java/org/sejongisc/backend/common/auth/jwt/JwtParser.java rename to backend/src/main/java/org/sejongisc/backend/common/auth/jwt/JwtUtils.java index 9041aff7..77dbf5cf 100644 --- a/backend/src/main/java/org/sejongisc/backend/common/auth/jwt/JwtParser.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/jwt/JwtUtils.java @@ -14,42 +14,112 @@ import javax.crypto.SecretKey; import java.nio.charset.StandardCharsets; -import java.util.*; +import java.util.Date; +import java.util.UUID; @Slf4j @Component @RequiredArgsConstructor -public class JwtParser { +public class JwtUtils { + + private static final String CLAIM_USER_ID = "uid"; + private static final String CLAIM_ROLE = "role"; + + private final TokenEncryptor tokenEncryptor; private final CustomUserDetailsService customUserDetailsService; + @Value("${jwt.secret}") private String rawSecretKey; + @Value("${jwt.expireDate.accessToken}") + private long accessTokenValidityInMillis; + + @Value("${jwt.expireDate.refreshToken}") + private long refreshTokenValidityInMillis; + private SecretKey secretKey; @PostConstruct public void init() { - //byte[] keyBytes = Base64.getDecoder().decode(rawSecretKey); // rawSecretKey의 base64 규격 이슈로 아래 코드로 대체 this.secretKey = Keys.hmacShaKeyFor(rawSecretKey.getBytes(StandardCharsets.UTF_8)); } - // 토큰 유효성 검사 - public boolean validationToken(String token) { + // Access Token 생성 + public String createToken(UUID userId, Role role, String email) { + Date now = new Date(); + Date expiryDate = new Date(now.getTime() + accessTokenValidityInMillis); + + return Jwts.builder() + .setSubject(email) + .claim(CLAIM_USER_ID, userId.toString()) + .claim(CLAIM_ROLE, role.name()) + .setIssuedAt(now) + .setExpiration(expiryDate) + .signWith(secretKey, SignatureAlgorithm.HS256) + .compact(); + } + + // Refresh Token 생성 (AES-GCM 암호화 포함) + public String createRefreshToken(UUID userId) { + Date now = new Date(); + Date expiryDate = new Date(now.getTime() + refreshTokenValidityInMillis); + + String rawRefreshToken = Jwts.builder() + .setSubject(userId.toString()) + .setIssuedAt(now) + .setExpiration(expiryDate) + .signWith(secretKey, SignatureAlgorithm.HS256) + .compact(); + + return tokenEncryptor.encrypt(rawRefreshToken); + } + + // 토큰 만료일 조회 + public Date getExpiration(String token) { + return Jwts.parserBuilder() + .setSigningKey(secretKey) + .build() + .parseClaimsJws(token) + .getBody() + .getExpiration(); + } + + // 토큰에서 userId 추출 (uid 클레임 우선, 없으면 subject 사용 - RefreshToken 호환) + public UUID getUserIdFromToken(String token) { + Claims claims = parseClaims(token); + String userIdStr = claims.get(CLAIM_USER_ID, String.class); + + if (userIdStr == null || userIdStr.isBlank()) { + userIdStr = claims.getSubject(); + } + + if (userIdStr == null || userIdStr.isBlank()) { + throw new JwtException("JWT에 userId(uid/subject)가 없습니다."); + } + try { - Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(token); - log.info("Token validation success"); - return true; - } catch (JwtException | IllegalArgumentException e) { - log.error("Token validation failed: {}", e.getMessage()); - return false; + return UUID.fromString(userIdStr); + } catch (IllegalArgumentException | NullPointerException e) { + throw new JwtException("잘못된 userId 형식의 JWT입니다."); } } - // Authentication 생성 + // 토큰에서 Role 추출 + public String getRoleFromToken(String token) { + return Jwts.parserBuilder() + .setSigningKey(secretKey) + .build() + .parseClaimsJws(token) + .getBody() + .get(CLAIM_ROLE, String.class); + } + + // Authentication 객체 생성 public UsernamePasswordAuthenticationToken getAuthentication(String token) { Claims claims = parseClaims(token); - // TODO : 유지보수성을 위해 클레임 키 상수화 고려 - String userId = claims.get("uid", String.class); - String roleStr = claims.get("role", String.class); + String userId = claims.get(CLAIM_USER_ID, String.class); + String roleStr = claims.get(CLAIM_ROLE, String.class); + if (roleStr == null) { throw new JwtException("JWT에 role 클레임이 없습니다."); } @@ -62,18 +132,28 @@ public UsernamePasswordAuthenticationToken getAuthentication(String token) { } if (userId == null) { - throw new JwtException("JWT에 userId(uid)가 없습니다."); + throw new JwtException("JWT에 userId(uid)가 없습니다."); } - // DB에서 다시 유저를 불러오기 (CustomUserDetailsService 사용) - UserDetails userDetails = customUserDetailsService.loadUserByUsername(userId); // TODO : 성능 고려해서 DB 조회 제거 고민 - + // TODO: 성능 고려해서 DB 조회 제거 고민 + UserDetails userDetails = customUserDetailsService.loadUserByUsername(userId); log.debug("인증 객체 생성 완료"); return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); + } + // 토큰 유효성 검증 + public boolean validationToken(String token) { + try { + Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(token); + log.info("Token validation success"); + return true; + } catch (JwtException | IllegalArgumentException e) { + log.error("Token validation failed: {}", e.getMessage()); + return false; + } } - // Claims 파싱 + // Claims 파싱 (만료된 토큰도 클레임 추출 가능) private Claims parseClaims(String token) { try { return Jwts.parserBuilder() @@ -85,25 +165,4 @@ private Claims parseClaims(String token) { return e.getClaims(); } } - - public UUID getUserIdFromToken(String token) { - Claims claims = parseClaims(token); - String userIdStr = claims.get("uid", String.class); - - // uid 클레임이 없을 경우 subject로 대체 (RefreshToken 호환) - if (userIdStr == null || userIdStr.isBlank()) { - userIdStr = claims.getSubject(); - } - - // 여전히 없거나 비어 있으면 명시적 예외 처리 - if (userIdStr == null || userIdStr.isBlank()) { - throw new JwtException("JWT에 userId(uid/subject)가 없습니다."); - } - - try { - return UUID.fromString(userIdStr); - } catch (IllegalArgumentException | NullPointerException e) { - throw new JwtException("잘못된 userId 형식의 JWT입니다."); - } - } } diff --git a/backend/src/main/java/org/sejongisc/backend/common/auth/jwt/TokenEncryptor.java b/backend/src/main/java/org/sejongisc/backend/common/auth/jwt/TokenEncryptor.java index 50586d9b..7df2201d 100644 --- a/backend/src/main/java/org/sejongisc/backend/common/auth/jwt/TokenEncryptor.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/jwt/TokenEncryptor.java @@ -1,7 +1,6 @@ package org.sejongisc.backend.common.auth.jwt; import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Configuration; import org.springframework.stereotype.Component; import javax.crypto.Cipher; diff --git a/backend/src/main/java/org/sejongisc/backend/common/auth/service/AuthService.java b/backend/src/main/java/org/sejongisc/backend/common/auth/service/AuthService.java index 68f7b002..de29700b 100644 --- a/backend/src/main/java/org/sejongisc/backend/common/auth/service/AuthService.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/service/AuthService.java @@ -7,8 +7,7 @@ import org.sejongisc.backend.activity.event.ActivityEvent; import org.sejongisc.backend.common.auth.entity.RefreshToken; import org.sejongisc.backend.common.auth.repository.RefreshTokenRepository; -import org.sejongisc.backend.common.auth.jwt.JwtParser; -import org.sejongisc.backend.common.auth.jwt.JwtProvider; +import org.sejongisc.backend.common.auth.jwt.JwtUtils; import org.sejongisc.backend.common.exception.CustomException; import org.sejongisc.backend.common.exception.ErrorCode; import org.sejongisc.backend.user.entity.Role; @@ -29,8 +28,9 @@ public class AuthService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; - private final JwtProvider jwtProvider; + private final JwtUtils jwtUtils; private final RefreshTokenRepository refreshTokenRepository; + private final JwtParser jwtParser; private final ApplicationEventPublisher eventPublisher; @@ -49,14 +49,14 @@ public AuthResponse login(AuthRequest request) { !passwordEncoder.matches(request.getPassword().trim(), user.getPasswordHash())) { throw new CustomException(ErrorCode.UNAUTHORIZED); } + + String accessToken = jwtUtils.createToken(user.getUserId(), user.getRole(), user.getEmail()); + String refreshToken = jwtUtils.createRefreshToken(user.getUserId()); if (user.getRole().equals(Role.PENDING_MEMBER)) { throw new CustomException(ErrorCode.NEED_PENDING_APPROVAL); } - String accessToken = jwtProvider.createToken(user.getUserId(), user.getRole(), user.getEmail()); - String refreshToken = jwtProvider.createRefreshToken(user.getUserId()); - // 기존 토큰 삭제 후 새로 저장 refreshTokenRepository.findByUserId(user.getUserId()) .ifPresent(refreshTokenRepository::delete); @@ -89,7 +89,7 @@ public AuthResponse login(AuthRequest request) { @Transactional public void logout(String accessToken) { - UUID userId = jwtParser.getUserIdFromToken(accessToken); + UUID userId = jwtUtils.getUserIdFromToken(accessToken); refreshTokenRepository.deleteByUserId(userId); log.info("로그아웃 완료: userId={}", userId); } diff --git a/backend/src/main/java/org/sejongisc/backend/common/auth/service/RefreshTokenService.java b/backend/src/main/java/org/sejongisc/backend/common/auth/service/RefreshTokenService.java index d7cc614a..ad43e205 100644 --- a/backend/src/main/java/org/sejongisc/backend/common/auth/service/RefreshTokenService.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/service/RefreshTokenService.java @@ -5,7 +5,7 @@ import lombok.extern.slf4j.Slf4j; import org.sejongisc.backend.common.auth.entity.RefreshToken; import org.sejongisc.backend.common.auth.repository.RefreshTokenRepository; -import org.sejongisc.backend.common.auth.jwt.JwtProvider; +import org.sejongisc.backend.common.auth.jwt.JwtUtils; import org.sejongisc.backend.common.auth.jwt.TokenEncryptor; import org.sejongisc.backend.common.exception.CustomException; import org.sejongisc.backend.common.exception.ErrorCode; @@ -27,7 +27,7 @@ public class RefreshTokenService { private final RefreshTokenRepository refreshTokenRepository; private final UserRepository userRepository; - private final JwtProvider jwtProvider; + private final JwtUtils jwtUtils; private final TokenEncryptor tokenEncryptor; @Transactional @@ -40,7 +40,7 @@ public Map reissueTokens(String encryptedRefreshToken) { String rawRefreshToken = tokenEncryptor.decrypt(encryptedRefreshToken); // refreshToken에서 userId 추출 - UUID userId = UUID.fromString(jwtProvider.getUserIdFromToken(rawRefreshToken)); + UUID userId = jwtUtils.getUserIdFromToken(rawRefreshToken); // DB에서 저장된 refreshToken 확인 RefreshToken saved = refreshTokenRepository.findByUserId(userId) @@ -58,17 +58,17 @@ public Map reissueTokens(String encryptedRefreshToken) { .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); // 새 Access Token 발급 - String newAccessToken = jwtProvider.createToken( + String newAccessToken = jwtUtils.createToken( user.getUserId(), user.getRole(), user.getEmail()); // Refresh Token 만료 임박 시 새로 발급 - Date expiration = jwtProvider.getExpiration(rawRefreshToken); + Date expiration = jwtUtils.getExpiration(rawRefreshToken); long remainingMillis = expiration.getTime() - System.currentTimeMillis(); String newRefreshToken = null; // 예: 남은 기간이 3일 미만이면 refreshToken도 갱신 if (remainingMillis < (3L * 24 * 60 * 60 * 1000)) { - newRefreshToken = jwtProvider.createRefreshToken(user.getUserId()); + newRefreshToken = jwtUtils.createRefreshToken(user.getUserId()); saved.setToken(newRefreshToken); refreshTokenRepository.save(saved); log.info("RefreshToken 재발급 완료: userId={}", userId); diff --git a/backend/src/test/java/org/sejongisc/backend/attendance/controller/AttendanceRoundControllerTest.java b/backend/src/test/java/org/sejongisc/backend/attendance/controller/AttendanceRoundControllerTest.java index 5b46e986..4c83bd02 100644 --- a/backend/src/test/java/org/sejongisc/backend/attendance/controller/AttendanceRoundControllerTest.java +++ b/backend/src/test/java/org/sejongisc/backend/attendance/controller/AttendanceRoundControllerTest.java @@ -4,7 +4,7 @@ import org.sejongisc.backend.attendance.service.AttendanceRoundService; import org.sejongisc.backend.attendance.service.AttendanceService; -import org.sejongisc.backend.common.auth.jwt.JwtProvider; +import org.sejongisc.backend.common.auth.jwt.JwtUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.context.annotation.Import; @@ -45,7 +45,7 @@ public class AttendanceRoundControllerTest { private AttendanceService attendanceService; @MockitoBean - private JwtProvider jwtProvider; + private JwtUtils jwtUtils; @MockitoBean private JpaMetamodelMappingContext jpaMetamodelMappingContext; diff --git a/backend/src/test/java/org/sejongisc/backend/attendance/controller/TestSecurityConfig.java b/backend/src/test/java/org/sejongisc/backend/attendance/controller/TestSecurityConfig.java index 302d7d60..09f3aa25 100644 --- a/backend/src/test/java/org/sejongisc/backend/attendance/controller/TestSecurityConfig.java +++ b/backend/src/test/java/org/sejongisc/backend/attendance/controller/TestSecurityConfig.java @@ -1,7 +1,7 @@ package org.sejongisc.backend.attendance.controller; import org.mockito.Mockito; -import org.sejongisc.backend.common.auth.jwt.JwtParser; +import org.sejongisc.backend.common.auth.jwt.JwtUtils; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.http.HttpMethod; @@ -14,8 +14,8 @@ public class TestSecurityConfig { @Bean - public JwtParser jwtParser() { - return Mockito.mock(JwtParser.class); + public JwtUtils jwtUtils() { + return Mockito.mock(JwtUtils.class); } @Bean diff --git a/backend/src/test/java/org/sejongisc/backend/auth/controller/AuthControllerTest.java b/backend/src/test/java/org/sejongisc/backend/auth/controller/AuthControllerTest.java index c3b8de4a..fe138551 100644 --- a/backend/src/test/java/org/sejongisc/backend/auth/controller/AuthControllerTest.java +++ b/backend/src/test/java/org/sejongisc/backend/auth/controller/AuthControllerTest.java @@ -19,7 +19,7 @@ import org.sejongisc.backend.common.auth.service.oauth2.Oauth2Service; import org.sejongisc.backend.common.auth.service.oauth2.OauthStateService; import org.sejongisc.backend.common.auth.service.RefreshTokenService; -import org.sejongisc.backend.common.auth.jwt.JwtProvider; +import org.sejongisc.backend.common.auth.jwt.JwtUtils; import org.sejongisc.backend.common.exception.CustomException; import org.sejongisc.backend.common.exception.ErrorCode; import org.sejongisc.backend.common.exception.controller.GlobalExceptionHandler; @@ -63,7 +63,7 @@ class AuthControllerTest { @Mock AuthService authService; @Mock UserService userService; - @Mock JwtProvider jwtProvider; + @Mock JwtUtils jwtUtils; @Mock OauthStateService oauthStateService; @Mock RefreshTokenService refreshTokenService; @Mock @@ -189,9 +189,9 @@ void googleLogin_success() throws Exception { when(googleService.getAccessToken("test-code")).thenReturn(tokenResponse); when(googleService.getUserInfo("mock-google-access-token")).thenReturn(userInfo); when(userService.findOrCreateUser(any())).thenReturn(user); - when(jwtProvider.createToken(user.getUserId(), user.getRole(), user.getEmail())) + when(jwtUtils.createToken(user.getUserId(), user.getRole(), user.getEmail())) .thenReturn("jwt-token"); - when(jwtProvider.createRefreshToken(user.getUserId())).thenReturn("refresh-token"); + when(jwtUtils.createRefreshToken(user.getUserId())).thenReturn("refresh-token"); mockMvc.perform(post("/api/auth/login/GOOGLE") .param("code", "test-code") @@ -352,8 +352,8 @@ void kakaoLogin_partialCoverage() throws Exception { User.builder().userId(UUID.randomUUID()).name("NullInfoUser").role(Role.TEAM_MEMBER).build() ); - when(jwtProvider.createToken(any(), any(), any())).thenReturn("access-token"); - when(jwtProvider.createRefreshToken(any())).thenReturn("refresh-token"); + when(jwtUtils.createToken(any(), any(), any())).thenReturn("access-token"); + when(jwtUtils.createRefreshToken(any())).thenReturn("refresh-token"); mockMvc.perform(post("/api/auth/login/KAKAO") .param("code", "dummy-code") @@ -375,8 +375,8 @@ void githubLogin_partialCoverage() throws Exception { User.builder().userId(UUID.randomUUID()).name("GH-NullUser").role(Role.TEAM_MEMBER).build() ); - when(jwtProvider.createToken(any(), any(), any())).thenReturn("gh-token"); - when(jwtProvider.createRefreshToken(any())).thenReturn("gh-refresh"); + when(jwtUtils.createToken(any(), any(), any())).thenReturn("gh-token"); + when(jwtUtils.createRefreshToken(any())).thenReturn("gh-refresh"); mockMvc.perform(post("/api/auth/login/GITHUB") .param("code", "dummy-code") diff --git a/backend/src/test/java/org/sejongisc/backend/auth/service/AuthServiceTest.java b/backend/src/test/java/org/sejongisc/backend/auth/service/AuthServiceTest.java index ddaecbf5..c9fa66c4 100644 --- a/backend/src/test/java/org/sejongisc/backend/auth/service/AuthServiceTest.java +++ b/backend/src/test/java/org/sejongisc/backend/auth/service/AuthServiceTest.java @@ -7,8 +7,7 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.sejongisc.backend.common.auth.repository.RefreshTokenRepository; -import org.sejongisc.backend.common.auth.jwt.JwtParser; -import org.sejongisc.backend.common.auth.jwt.JwtProvider; +import org.sejongisc.backend.common.auth.jwt.JwtUtils; import org.sejongisc.backend.common.auth.service.AuthService; import org.sejongisc.backend.common.exception.CustomException; import org.sejongisc.backend.common.exception.ErrorCode; @@ -38,10 +37,7 @@ class AuthServiceTest { private PasswordEncoder passwordEncoder; @Mock - private JwtProvider jwtProvider; - - @Mock - private JwtParser jwtParser; + private JwtUtils jwtUtils; @Mock private RefreshTokenRepository refreshTokenRepository; @@ -74,7 +70,7 @@ void login_success() { given(userRepository.findUserByEmail("test@example.com")) .willReturn(Optional.of(user)); given(passwordEncoder.matches(rawPassword, encodedPassword)).willReturn(true); - given(jwtProvider.createToken(any(UUID.class), any(Role.class), anyString())) + given(jwtUtils.createToken(any(UUID.class), any(Role.class), anyString())) .willReturn("mocked-jwt-token"); // when @@ -174,13 +170,13 @@ void logout_success() { UUID userId = UUID.randomUUID(); String fakeToken = "fake.jwt.token"; - when(jwtParser.getUserIdFromToken(fakeToken)).thenReturn(userId); + when(jwtUtils.getUserIdFromToken(fakeToken)).thenReturn(userId); authService.logout(fakeToken); - verify(jwtParser, times(1)).getUserIdFromToken(fakeToken); + verify(jwtUtils, times(1)).getUserIdFromToken(fakeToken); verify(refreshTokenRepository, times(1)).deleteByUserId(userId); } diff --git a/backend/src/test/java/org/sejongisc/backend/auth/service/RefreshTokenServiceTest.java b/backend/src/test/java/org/sejongisc/backend/auth/service/RefreshTokenServiceTest.java index d869aacd..e674fd81 100644 --- a/backend/src/test/java/org/sejongisc/backend/auth/service/RefreshTokenServiceTest.java +++ b/backend/src/test/java/org/sejongisc/backend/auth/service/RefreshTokenServiceTest.java @@ -8,7 +8,8 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.sejongisc.backend.common.auth.entity.RefreshToken; import org.sejongisc.backend.common.auth.repository.RefreshTokenRepository; -import org.sejongisc.backend.common.auth.jwt.JwtProvider; +import org.sejongisc.backend.common.auth.jwt.JwtUtils; +import org.sejongisc.backend.common.auth.jwt.TokenEncryptor; import org.sejongisc.backend.common.auth.service.RefreshTokenService; import org.sejongisc.backend.common.exception.CustomException; import org.sejongisc.backend.common.exception.ErrorCode; @@ -32,7 +33,10 @@ class RefreshTokenServiceTest { private UserRepository userRepository; @Mock - private JwtProvider jwtProvider; + private JwtUtils jwtUtils; + + @Mock + private TokenEncryptor tokenEncryptor; @InjectMocks private RefreshTokenService refreshTokenService; @@ -63,14 +67,15 @@ void setUp() { @DisplayName("정상 토큰 재발급 (RefreshToken 만료 여유 충분)") void reissueTokens_Success_NoRefreshRenewal() { // given - when(jwtProvider.getUserIdFromToken(refreshToken)).thenReturn(userId.toString()); + when(tokenEncryptor.decrypt(refreshToken)).thenReturn(refreshToken); + when(jwtUtils.getUserIdFromToken(refreshToken)).thenReturn(userId); when(refreshTokenRepository.findByUserId(userId)).thenReturn(Optional.of(savedToken)); when(userRepository.findById(userId)).thenReturn(Optional.of(user)); - when(jwtProvider.createToken(any(), any(), any())).thenReturn("new-access-token"); + when(jwtUtils.createToken(any(), any(), any())).thenReturn("new-access-token"); // 만료 10일 남은 refresh token Date expiration = new Date(System.currentTimeMillis() + (10L * 24 * 60 * 60 * 1000)); - when(jwtProvider.getExpiration(refreshToken)).thenReturn(expiration); + when(jwtUtils.getExpiration(refreshToken)).thenReturn(expiration); // when Map result = refreshTokenService.reissueTokens(refreshToken); @@ -85,15 +90,16 @@ void reissueTokens_Success_NoRefreshRenewal() { @DisplayName("RefreshToken 남은 기간 3일 미만 → 새 RefreshToken도 재발급") void reissueTokens_Success_WithRefreshRenewal() { // given - when(jwtProvider.getUserIdFromToken(refreshToken)).thenReturn(userId.toString()); + when(tokenEncryptor.decrypt(refreshToken)).thenReturn(refreshToken); + when(jwtUtils.getUserIdFromToken(refreshToken)).thenReturn(userId); when(refreshTokenRepository.findByUserId(userId)).thenReturn(Optional.of(savedToken)); when(userRepository.findById(userId)).thenReturn(Optional.of(user)); - when(jwtProvider.createToken(any(), any(), any())).thenReturn("new-access-token"); - when(jwtProvider.createRefreshToken(userId)).thenReturn("new-refresh-token"); + when(jwtUtils.createToken(any(), any(), any())).thenReturn("new-access-token"); + when(jwtUtils.createRefreshToken(userId)).thenReturn("new-refresh-token"); // 만료 1일 남음 Date expiration = new Date(System.currentTimeMillis() + (1L * 24 * 60 * 60 * 1000)); - when(jwtProvider.getExpiration(refreshToken)).thenReturn(expiration); + when(jwtUtils.getExpiration(refreshToken)).thenReturn(expiration); // when Map result = refreshTokenService.reissueTokens(refreshToken); @@ -113,7 +119,9 @@ void reissueTokens_TokenMismatch() { .token("different-token") .build(); - when(jwtProvider.getUserIdFromToken(refreshToken)).thenReturn(userId.toString()); + when(tokenEncryptor.decrypt(refreshToken)).thenReturn(refreshToken); + when(tokenEncryptor.decrypt("different-token")).thenReturn("different-token"); + when(jwtUtils.getUserIdFromToken(refreshToken)).thenReturn(userId); when(refreshTokenRepository.findByUserId(userId)).thenReturn(Optional.of(wrongToken)); // when & then @@ -126,7 +134,8 @@ void reissueTokens_TokenMismatch() { @DisplayName("UserRepository에 사용자 없음 → USER_NOT_FOUND 예외 발생") void reissueTokens_UserNotFound() { // given - when(jwtProvider.getUserIdFromToken(refreshToken)).thenReturn(userId.toString()); + when(tokenEncryptor.decrypt(refreshToken)).thenReturn(refreshToken); + when(jwtUtils.getUserIdFromToken(refreshToken)).thenReturn(userId); when(refreshTokenRepository.findByUserId(userId)).thenReturn(Optional.of(savedToken)); when(userRepository.findById(userId)).thenReturn(Optional.empty()); @@ -140,7 +149,8 @@ void reissueTokens_UserNotFound() { @DisplayName("DB에 RefreshToken 없음 → UNAUTHORIZED 예외 발생") void reissueTokens_RefreshNotFound() { // given - when(jwtProvider.getUserIdFromToken(refreshToken)).thenReturn(userId.toString()); + when(tokenEncryptor.decrypt(refreshToken)).thenReturn(refreshToken); + when(jwtUtils.getUserIdFromToken(refreshToken)).thenReturn(userId); when(refreshTokenRepository.findByUserId(userId)).thenReturn(Optional.empty()); // when & then @@ -150,9 +160,10 @@ void reissueTokens_RefreshNotFound() { } @Test - @DisplayName("JwtProvider 내부 예외 → CustomException(UNAUTHORIZED) 반환") + @DisplayName("JwtUtils 내부 예외 → CustomException(UNAUTHORIZED) 반환") void reissueTokens_JwtException() { - when(jwtProvider.getUserIdFromToken(refreshToken)) + when(tokenEncryptor.decrypt(refreshToken)).thenReturn(refreshToken); + when(jwtUtils.getUserIdFromToken(refreshToken)) .thenThrow(new RuntimeException("토큰 파싱 오류")); CustomException ex = assertThrows(CustomException.class, diff --git a/backend/src/test/java/org/sejongisc/backend/backtest/controller/TemplateControllerTest.java b/backend/src/test/java/org/sejongisc/backend/backtest/controller/TemplateControllerTest.java index 938de6c7..5be91646 100644 --- a/backend/src/test/java/org/sejongisc/backend/backtest/controller/TemplateControllerTest.java +++ b/backend/src/test/java/org/sejongisc/backend/backtest/controller/TemplateControllerTest.java @@ -9,7 +9,7 @@ import org.sejongisc.backend.backtest.entity.Template; import org.sejongisc.backend.backtest.service.TemplateService; import org.sejongisc.backend.common.auth.dto.CustomUserDetails; -import org.sejongisc.backend.common.auth.jwt.JwtParser; +import org.sejongisc.backend.common.auth.jwt.JwtUtils; import org.sejongisc.backend.common.config.security.SecurityConfig; import org.sejongisc.backend.user.entity.Role; import org.sejongisc.backend.user.entity.User; @@ -52,7 +52,7 @@ class TemplateControllerTest { private TemplateService templateService; @MockitoBean - JwtParser jwtParser; + JwtUtils jwtUtils; @MockitoBean JpaMetamodelMappingContext jpaMetamodelMappingContext; @@ -82,8 +82,8 @@ class TemplateControllerTest { .templates(List.of(t1, t2)) .build(); - when(jwtParser.validationToken(TOKEN)).thenReturn(true); - when(jwtParser.getAuthentication(TOKEN)).thenReturn(인증토큰(uid)); + when(jwtUtils.validationToken(TOKEN)).thenReturn(true); + when(jwtUtils.getAuthentication(TOKEN)).thenReturn(인증토큰(uid)); when(templateService.findAllByUserId(uid)).thenReturn(resp); mockMvc.perform(get("/api/backtest/templates") @@ -101,8 +101,8 @@ class TemplateControllerTest { UUID tid = UUID.randomUUID(); Template t = Template.of(User.builder().userId(uid).build(), "title", "desc", true); - when(jwtParser.validationToken(TOKEN)).thenReturn(true); - when(jwtParser.getAuthentication(TOKEN)).thenReturn(인증토큰(uid)); + when(jwtUtils.validationToken(TOKEN)).thenReturn(true); + when(jwtUtils.getAuthentication(TOKEN)).thenReturn(인증토큰(uid)); when(templateService.findById(tid, uid)) .thenReturn(TemplateResponse.builder().template(t).build()); @@ -118,8 +118,8 @@ class TemplateControllerTest { void 생성_인증되어있으면_200과_생성결과를_반환한다() throws Exception { UUID uid = UUID.randomUUID(); - when(jwtParser.validationToken(TOKEN)).thenReturn(true); - when(jwtParser.getAuthentication(TOKEN)).thenReturn(인증토큰(uid)); + when(jwtUtils.validationToken(TOKEN)).thenReturn(true); + when(jwtUtils.getAuthentication(TOKEN)).thenReturn(인증토큰(uid)); // [변경] Record 생성자 사용 (userId, templateId 제거됨) TemplateRequest req = new TemplateRequest("new title", "new desc", true); @@ -149,8 +149,8 @@ class TemplateControllerTest { Template edited = Template.of(User.builder().userId(uid).build(), "edited", "edited desc", false); - when(jwtParser.validationToken(TOKEN)).thenReturn(true); - when(jwtParser.getAuthentication(TOKEN)).thenReturn(인증토큰(uid)); + when(jwtUtils.validationToken(TOKEN)).thenReturn(true); + when(jwtUtils.getAuthentication(TOKEN)).thenReturn(인증토큰(uid)); when(templateService.updateTemplate( eq(tid), @@ -173,8 +173,8 @@ class TemplateControllerTest { UUID uid = UUID.randomUUID(); UUID tid = UUID.randomUUID(); - when(jwtParser.validationToken(TOKEN)).thenReturn(true); - when(jwtParser.getAuthentication(TOKEN)).thenReturn(인증토큰(uid)); + when(jwtUtils.validationToken(TOKEN)).thenReturn(true); + when(jwtUtils.getAuthentication(TOKEN)).thenReturn(인증토큰(uid)); mockMvc.perform(delete("/api/backtest/templates/{templateId}", tid) .header("Authorization", "Bearer " + TOKEN))