diff --git a/src/main/java/hanium/modic/backend/common/error/ErrorCode.java b/src/main/java/hanium/modic/backend/common/error/ErrorCode.java index d0c63a9e..8ac91a23 100644 --- a/src/main/java/hanium/modic/backend/common/error/ErrorCode.java +++ b/src/main/java/hanium/modic/backend/common/error/ErrorCode.java @@ -38,6 +38,7 @@ public enum ErrorCode { USER_COIN_TRANSFER_FAIL_EXCEPTION(HttpStatus.INTERNAL_SERVER_ERROR, "U-006", "코인 송금에 실패하였습니다."), USER_IMAGE_NOT_FOUND_EXCEPTION(HttpStatus.NOT_FOUND, "U-007", "해당 유저의 프로필 이미지를 찾을 수 없습니다."), USER_UPDATE_TOKEN_INVALID_EXCEPTION(HttpStatus.BAD_REQUEST, "U-008", "토큰이 유효하지 않습니다."), + WITHDRAWN_USER_EXCEPTION(HttpStatus.BAD_REQUEST, "U-009", "탈퇴한 유저입니다."), // Post POST_NOT_FOUND_EXCEPTION(HttpStatus.NOT_FOUND, "P-001", "해당 포스트를 찾을 수 없습니다."), diff --git a/src/main/java/hanium/modic/backend/common/jwt/JwtTokenProvider.java b/src/main/java/hanium/modic/backend/common/jwt/JwtTokenProvider.java index 17f20256..26798b1f 100644 --- a/src/main/java/hanium/modic/backend/common/jwt/JwtTokenProvider.java +++ b/src/main/java/hanium/modic/backend/common/jwt/JwtTokenProvider.java @@ -14,6 +14,7 @@ import hanium.modic.backend.common.property.property.TokenProperty; import hanium.modic.backend.common.security.principal.AuthenticatedUser; import hanium.modic.backend.common.security.principal.UserPrincipal; +import hanium.modic.backend.domain.auth.constant.AuthConstant; import hanium.modic.backend.domain.auth.dto.Token; import hanium.modic.backend.domain.user.entity.UserEntity; import hanium.modic.backend.domain.user.repository.UserEntityRepository; @@ -21,6 +22,7 @@ import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.security.Keys; +import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -95,6 +97,12 @@ public void validateToken(final String accessToken) { } } + public Optional extractAccessToken(HttpServletRequest request) { + return Optional.ofNullable(request.getHeader(AuthConstant.AUTHORIZATION)).filter( + accessToken -> accessToken.startsWith(AuthConstant.BEARER) + ).map(accessToken -> accessToken.replace(AuthConstant.BEARER, "")); + } + public String getType(String token) { Claims claims = Jwts.parserBuilder() .setSigningKey(Keys.hmacShaKeyFor(tokenProperty.getSecretKey().getBytes(StandardCharsets.UTF_8))) diff --git a/src/main/java/hanium/modic/backend/common/oauth/OAuthSuccessHandler.java b/src/main/java/hanium/modic/backend/common/oauth/OAuthSuccessHandler.java index a782aa82..01b81937 100644 --- a/src/main/java/hanium/modic/backend/common/oauth/OAuthSuccessHandler.java +++ b/src/main/java/hanium/modic/backend/common/oauth/OAuthSuccessHandler.java @@ -2,6 +2,8 @@ import java.io.IOException; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseCookie; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; import org.springframework.stereotype.Component; @@ -13,7 +15,6 @@ import hanium.modic.backend.domain.auth.constant.AuthConstant; import hanium.modic.backend.domain.auth.dto.Token; import hanium.modic.backend.domain.auth.util.CookieUtil; -import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; @@ -26,6 +27,7 @@ public class OAuthSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { private final JwtTokenProvider jwtTokenProvider; private final RefreshTokenRepository refreshTokenRepository; + private final CookieUtil cookieUtil; @Override @Transactional @@ -44,8 +46,8 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo // 엑세스 토큰과 리프레시 토큰을 응답 헤더와 쿠키에 설정 response.addHeader(AuthConstant.AUTHORIZATION, AuthConstant.BEARER + token.accessToken()); - Cookie refreshTokenCookie = CookieUtil.createRefreshCookie(token.refreshToken()); - response.addCookie(refreshTokenCookie); + ResponseCookie refreshTokenCookie = cookieUtil.createRefreshCookie(token.refreshToken()); + response.addHeader(HttpHeaders.SET_COOKIE, refreshTokenCookie.toString()); // Todo: 리다이렉트 URL을 환경 변수로 관리 response.sendRedirect(AuthConstant.LOCAL_OAUTH_REDIRECT_URI); diff --git a/src/main/java/hanium/modic/backend/common/security/SecurityConfig.java b/src/main/java/hanium/modic/backend/common/security/SecurityConfig.java index ef08a44e..e556a6ef 100644 --- a/src/main/java/hanium/modic/backend/common/security/SecurityConfig.java +++ b/src/main/java/hanium/modic/backend/common/security/SecurityConfig.java @@ -35,7 +35,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .cors(cors -> cors.configurationSource(corsConfigurationSource)) .formLogin(AbstractHttpConfigurer::disable) .authorizeHttpRequests(auth -> auth - .anyRequest().permitAll()) + .anyRequest().authenticated()) .sessionManagement(session -> session .sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) diff --git a/src/main/java/hanium/modic/backend/common/security/principal/UserPrincipal.java b/src/main/java/hanium/modic/backend/common/security/principal/UserPrincipal.java index 8db64206..4965b001 100644 --- a/src/main/java/hanium/modic/backend/common/security/principal/UserPrincipal.java +++ b/src/main/java/hanium/modic/backend/common/security/principal/UserPrincipal.java @@ -1,5 +1,7 @@ package hanium.modic.backend.common.security.principal; +import static hanium.modic.backend.domain.user.enums.UserRole.*; + import java.util.ArrayList; import java.util.Collection; @@ -7,6 +9,7 @@ import org.springframework.security.core.userdetails.UserDetails; import hanium.modic.backend.domain.user.entity.UserEntity; +import hanium.modic.backend.domain.user.enums.UserRole; import lombok.Data; @Data @@ -58,7 +61,7 @@ public boolean isCredentialsNonExpired() { @Override public boolean isEnabled() { - return true; + return user.getUserRole() != WITHDRAWN; } @Override diff --git a/src/main/java/hanium/modic/backend/domain/auth/service/AuthService.java b/src/main/java/hanium/modic/backend/domain/auth/service/AuthService.java index 8aa90082..019001aa 100644 --- a/src/main/java/hanium/modic/backend/domain/auth/service/AuthService.java +++ b/src/main/java/hanium/modic/backend/domain/auth/service/AuthService.java @@ -2,6 +2,7 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import hanium.modic.backend.common.error.ErrorCode; import hanium.modic.backend.common.error.exception.AppException; @@ -14,6 +15,7 @@ import hanium.modic.backend.domain.auth.service.component.EmailSender; import hanium.modic.backend.domain.auth.service.dto.EmailDto; import hanium.modic.backend.domain.user.entity.UserEntity; +import hanium.modic.backend.domain.user.enums.UserRole; import hanium.modic.backend.domain.user.repository.UserEntityRepository; import hanium.modic.backend.web.auth.dto.CheckEmailDuplicateResponse; import hanium.modic.backend.web.auth.dto.LoginResponse; @@ -40,10 +42,16 @@ public class AuthService { private final EmailSender emailSender; // 로그인 처리 + @Transactional public LoginResponse login(final String email, final String password) { UserEntity user = userEntityRepository.findByEmail(email) .orElseThrow(() -> new AppException(ErrorCode.USER_NOT_FOUND_EXCEPTION)); + // 탈퇴한 회원이 로그인 시도 시, 로그인 막음 + if (user.getUserRole() == UserRole.WITHDRAWN) { + throw new AppException(ErrorCode.WITHDRAWN_USER_EXCEPTION); + } + if (!passwordEncoder.matches(password, user.getPassword())) { throw new AppException(ErrorCode.USER_PASSWORD_MISMATCH_EXCEPTION); } @@ -59,6 +67,18 @@ public LoginResponse login(final String email, final String password) { return LoginResponse.from(token); } + @Transactional + public void logout(final String refreshToken, final String accessToken) { + UserEntity user = jwtTokenProvider.getUser(refreshToken) + .orElseThrow(() -> new AppException(ErrorCode.USER_NOT_FOUND_EXCEPTION)); + + jwtTokenProvider.setBlackList(refreshToken); + jwtTokenProvider.setBlackList(accessToken); + + refreshTokenRepository.deleteById(user.getId()); + } + + @Transactional public ReissueResponse reissue(final String refreshToken) { if (blackListRepository.existsById(refreshToken)) { throw new AppException(ErrorCode.TOKEN_BLACKLISTED_EXCEPTION); diff --git a/src/main/java/hanium/modic/backend/domain/auth/util/CookieUtil.java b/src/main/java/hanium/modic/backend/domain/auth/util/CookieUtil.java index ced87d8f..bd1b69d2 100644 --- a/src/main/java/hanium/modic/backend/domain/auth/util/CookieUtil.java +++ b/src/main/java/hanium/modic/backend/domain/auth/util/CookieUtil.java @@ -1,19 +1,9 @@ package hanium.modic.backend.domain.auth.util; -import jakarta.servlet.http.Cookie; +import org.springframework.http.ResponseCookie; -public class CookieUtil { +public interface CookieUtil { + ResponseCookie createRefreshCookie(final String refreshToken); - private static final String REFRESH_TOKEN_COOKIE_NAME = "refreshToken"; - - private static final int COOKIE_MAX_AGE = 60 * 60 * 24 * 3; - - public static Cookie createRefreshCookie(final String refreshToken) { - Cookie cookie = new Cookie(REFRESH_TOKEN_COOKIE_NAME, refreshToken); - cookie.setHttpOnly(true); - cookie.setPath("/"); - // cookie.setSecure(true); // Todo: https://github.com/Modic-2025/modic_backend/issues/39 - cookie.setMaxAge(COOKIE_MAX_AGE); - return cookie; - } + ResponseCookie deleteRefreshCookie(); } diff --git a/src/main/java/hanium/modic/backend/domain/auth/util/DevCookieUtil.java b/src/main/java/hanium/modic/backend/domain/auth/util/DevCookieUtil.java new file mode 100644 index 00000000..96ef6a83 --- /dev/null +++ b/src/main/java/hanium/modic/backend/domain/auth/util/DevCookieUtil.java @@ -0,0 +1,35 @@ +package hanium.modic.backend.domain.auth.util; + +import org.springframework.context.annotation.Profile; +import org.springframework.http.ResponseCookie; +import org.springframework.stereotype.Component; + +// 개발 환경용 쿠키 유틸리티 +@Component +@Profile({"local", "dev", "test"}) +public class DevCookieUtil implements CookieUtil { + + private static final String REFRESH_TOKEN_COOKIE_NAME = "refreshToken"; + + private static final int COOKIE_MAX_AGE = 60 * 60 * 24 * 3; + + public ResponseCookie createRefreshCookie(final String refreshToken) { + return ResponseCookie.from(REFRESH_TOKEN_COOKIE_NAME, refreshToken) + .httpOnly(true) + .secure(true) + .path("/") + .maxAge(COOKIE_MAX_AGE) + .sameSite("Lax") // 또는 "Strict" + .build(); + } + + public ResponseCookie deleteRefreshCookie() { + return ResponseCookie.from(REFRESH_TOKEN_COOKIE_NAME, "") + .httpOnly(true) + .secure(true) + .path("/") + .maxAge(0) + .sameSite("Lax") + .build(); + } +} diff --git a/src/main/java/hanium/modic/backend/domain/auth/util/ProdCookieUtil.java b/src/main/java/hanium/modic/backend/domain/auth/util/ProdCookieUtil.java new file mode 100644 index 00000000..eef85f82 --- /dev/null +++ b/src/main/java/hanium/modic/backend/domain/auth/util/ProdCookieUtil.java @@ -0,0 +1,37 @@ +package hanium.modic.backend.domain.auth.util; + +import org.springframework.context.annotation.Profile; +import org.springframework.http.ResponseCookie; +import org.springframework.stereotype.Component; + +import jakarta.servlet.http.Cookie; + +// Production 환경용 쿠키 유틸리티 +@Component +@Profile({"main"}) +public class ProdCookieUtil implements CookieUtil { + + private static final String REFRESH_TOKEN_COOKIE_NAME = "refreshToken"; + + private static final int COOKIE_MAX_AGE = 60 * 60 * 24 * 3; + + public ResponseCookie createRefreshCookie(final String refreshToken) { + return ResponseCookie.from(REFRESH_TOKEN_COOKIE_NAME, refreshToken) + .httpOnly(true) + .secure(true) + .path("/") + .maxAge(COOKIE_MAX_AGE) + .sameSite("Lax") // 또는 "Strict" + .build(); + } + + public ResponseCookie deleteRefreshCookie() { + return ResponseCookie.from(REFRESH_TOKEN_COOKIE_NAME, "") + .httpOnly(true) + .secure(true) + .path("/") + .maxAge(0) + .sameSite("Lax") + .build(); + } +} diff --git a/src/main/java/hanium/modic/backend/domain/user/entity/UserEntity.java b/src/main/java/hanium/modic/backend/domain/user/entity/UserEntity.java index 97a37b97..10bae0b0 100644 --- a/src/main/java/hanium/modic/backend/domain/user/entity/UserEntity.java +++ b/src/main/java/hanium/modic/backend/domain/user/entity/UserEntity.java @@ -96,4 +96,10 @@ public void updateEmail(String email) { } this.email = email; } + + public void softWithdraw() { + this.userRole = UserRole.WITHDRAWN; + this.email = "withdrawn_" + this.id + "_" + java.util.UUID.randomUUID().toString() + "@modic.com"; + this.name = "탈퇴회원"; + } } diff --git a/src/main/java/hanium/modic/backend/domain/user/enums/UserRole.java b/src/main/java/hanium/modic/backend/domain/user/enums/UserRole.java index 97d98604..604a703d 100644 --- a/src/main/java/hanium/modic/backend/domain/user/enums/UserRole.java +++ b/src/main/java/hanium/modic/backend/domain/user/enums/UserRole.java @@ -1,5 +1,7 @@ package hanium.modic.backend.domain.user.enums; public enum UserRole { - ADMIN, USER + ADMIN, + USER, + WITHDRAWN } \ No newline at end of file diff --git a/src/main/java/hanium/modic/backend/domain/user/service/UserService.java b/src/main/java/hanium/modic/backend/domain/user/service/UserService.java index e0affc96..ccbffde5 100644 --- a/src/main/java/hanium/modic/backend/domain/user/service/UserService.java +++ b/src/main/java/hanium/modic/backend/domain/user/service/UserService.java @@ -72,6 +72,20 @@ private void checkDuplicateEmail(final String email) { } } + // 회원탈퇴 + @Transactional + public void deleteAndLogout(final long id, final String refreshToken, final String accessToken) { + // 소프트 삭제 처리 + UserEntity user = userEntityRepository.findById(id) + .orElseThrow(() -> new AppException(ErrorCode.USER_NOT_FOUND_EXCEPTION)); + user.softWithdraw(); + + userEntityRepository.save(user); + + // 관련 토큰 삭제 + authService.logout(refreshToken, accessToken); + } + // 회원 정보 조회 public UserInfoResponse getUserInfo(UserEntity user) { Optional userImageUrl = userImageService.createImageGetUrlOptional(user.getId()); diff --git a/src/main/java/hanium/modic/backend/web/auth/controller/AuthController.java b/src/main/java/hanium/modic/backend/web/auth/controller/AuthController.java index 7f4c8c5b..78a45dfa 100644 --- a/src/main/java/hanium/modic/backend/web/auth/controller/AuthController.java +++ b/src/main/java/hanium/modic/backend/web/auth/controller/AuthController.java @@ -1,5 +1,9 @@ package hanium.modic.backend.web.auth.controller; +import static hanium.modic.backend.common.error.ErrorCode.*; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseCookie; import org.springframework.http.ResponseEntity; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.CookieValue; @@ -10,7 +14,9 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import hanium.modic.backend.common.jwt.JwtTokenProvider; import hanium.modic.backend.common.response.AppResponse; +import hanium.modic.backend.common.swagger.ApiErrorMapping; import hanium.modic.backend.domain.auth.constant.AuthConstant; import hanium.modic.backend.domain.auth.service.AuthService; import hanium.modic.backend.domain.auth.util.CookieUtil; @@ -23,7 +29,7 @@ import hanium.modic.backend.web.auth.dto.VerifyEmailCodeResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; -import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import jakarta.validation.constraints.Email; @@ -37,6 +43,8 @@ public class AuthController { private final AuthService authService; + private final CookieUtil cookieUtil; + private final JwtTokenProvider jwtTokenProvider; @PostMapping("/login") @Operation( @@ -52,12 +60,37 @@ public ResponseEntity> login(@RequestBody @Valid Logi LoginResponse loginResponse = authService.login(request.email(), request.password()); response.addHeader(AuthConstant.AUTHORIZATION, AuthConstant.BEARER + loginResponse.accessToken()); - Cookie refreshTokenCookie = CookieUtil.createRefreshCookie(loginResponse.refreshToken()); - response.addCookie(refreshTokenCookie); + ResponseCookie refreshTokenCookie = cookieUtil.createRefreshCookie(loginResponse.refreshToken()); + response.addHeader(HttpHeaders.SET_COOKIE, refreshTokenCookie.toString()); return ResponseEntity.ok(AppResponse.ok(loginResponse)); } + @PostMapping("/logout") + @Operation( + summary = "로그아웃 API", + description = """ + 리프레시 토큰을 통해 로그아웃합니다.
+ 로그아웃된 토큰으로 요청시 [C-005] - 차단된 토큰입니다를 반환합니다. + """ + ) + @ApiErrorMapping({ + USER_NOT_FOUND_EXCEPTION, + }) + public ResponseEntity> logout( + @CookieValue(name = "refreshToken") String refreshToken, + HttpServletRequest request, + HttpServletResponse response + ) { + String accessToken = jwtTokenProvider.extractAccessToken(request).get(); // accessToken은 무조건 존재함 + authService.logout(refreshToken, accessToken); + + ResponseCookie deleteRefreshTokenCookie = cookieUtil.deleteRefreshCookie(); + response.addHeader(HttpHeaders.SET_COOKIE, deleteRefreshTokenCookie.toString()); + + return ResponseEntity.ok(AppResponse.noContent()); + } + @PostMapping("/reissue") @Operation( summary = "토큰 재발급 API", @@ -73,8 +106,8 @@ public ResponseEntity> reissue(@CookieValue(name = "refreshTok ReissueResponse reissueResponse = authService.reissue(refreshToken); response.addHeader(AuthConstant.AUTHORIZATION, AuthConstant.BEARER + reissueResponse.accessToken()); - Cookie refreshTokenCookie = CookieUtil.createRefreshCookie(reissueResponse.refreshToken()); - response.addCookie(refreshTokenCookie); + ResponseCookie refreshTokenCookie = cookieUtil.createRefreshCookie(reissueResponse.refreshToken()); + response.addHeader(HttpHeaders.SET_COOKIE, refreshTokenCookie.toString()); return ResponseEntity.ok().build(); } diff --git a/src/main/java/hanium/modic/backend/web/user/controller/UserController.java b/src/main/java/hanium/modic/backend/web/user/controller/UserController.java index a3a4ad66..3df1f263 100644 --- a/src/main/java/hanium/modic/backend/web/user/controller/UserController.java +++ b/src/main/java/hanium/modic/backend/web/user/controller/UserController.java @@ -3,8 +3,12 @@ import static hanium.modic.backend.common.error.ErrorCode.*; import org.springframework.data.domain.Page; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseCookie; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.CookieValue; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PostMapping; @@ -14,9 +18,11 @@ import org.springframework.web.bind.annotation.RestController; import hanium.modic.backend.common.annotation.user.CurrentUser; +import hanium.modic.backend.common.jwt.JwtTokenProvider; import hanium.modic.backend.common.response.AppResponse; import hanium.modic.backend.common.response.PageResponse; import hanium.modic.backend.common.swagger.ApiErrorMapping; +import hanium.modic.backend.domain.auth.util.CookieUtil; import hanium.modic.backend.domain.user.entity.UserEntity; import hanium.modic.backend.domain.user.service.UserCoinService; import hanium.modic.backend.domain.user.service.UserService; @@ -33,6 +39,8 @@ import hanium.modic.backend.web.user.dto.response.UserCreateResponse; import hanium.modic.backend.web.user.dto.response.UserInfoResponse; import io.swagger.v3.oas.annotations.Operation; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.Min; @@ -46,6 +54,8 @@ public class UserController { private final UserService userService; private final UserCoinService userCoinService; + private final CookieUtil cookieUtil; + private final JwtTokenProvider jwtTokenProvider; @PostMapping @Operation( @@ -63,6 +73,26 @@ public ResponseEntity> createUser(@RequestBody @ ))); } + @DeleteMapping + @Operation( + summary = "회원탈퇴 API", + description = "로그인한 유저의 계정을 삭제합니다." + ) + public ResponseEntity> deleteUser( + @CurrentUser UserEntity user, + @CookieValue(name = "refreshToken") String refreshToken, + HttpServletRequest request, + HttpServletResponse response + ) { + String accessToken = jwtTokenProvider.extractAccessToken(request).get(); // accessToken은 무조건 존재함 + userService.deleteAndLogout(user.getId(), refreshToken, accessToken); + + ResponseCookie deleteRefreshTokenCookie = cookieUtil.deleteRefreshCookie(); + response.addHeader(HttpHeaders.SET_COOKIE, deleteRefreshTokenCookie.toString()); + + return ResponseEntity.ok().build(); + } + @GetMapping("/me") @Operation( summary = "유저 정보 조회 API", diff --git a/src/test/java/hanium/modic/backend/common/oauth/OAuthSuccessHandlerTest.java b/src/test/java/hanium/modic/backend/common/oauth/OAuthSuccessHandlerTest.java index 1b78b6bc..2326b669 100644 --- a/src/test/java/hanium/modic/backend/common/oauth/OAuthSuccessHandlerTest.java +++ b/src/test/java/hanium/modic/backend/common/oauth/OAuthSuccessHandlerTest.java @@ -13,19 +13,23 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.ResponseCookie; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.security.core.Authentication; +import org.springframework.test.context.ActiveProfiles; import hanium.modic.backend.common.jwt.JwtTokenProvider; import hanium.modic.backend.common.jwt.RefreshToken; import hanium.modic.backend.common.jwt.RefreshTokenRepository; import hanium.modic.backend.domain.auth.constant.AuthConstant; import hanium.modic.backend.domain.auth.dto.Token; +import hanium.modic.backend.domain.auth.util.CookieUtil; import hanium.modic.backend.domain.user.entity.UserEntity; import hanium.modic.backend.domain.user.factory.UserFactory; import jakarta.servlet.http.Cookie; +@ActiveProfiles("test") @ExtendWith(MockitoExtension.class) class OAuthSuccessHandlerTest { @@ -44,6 +48,9 @@ class OAuthSuccessHandlerTest { @Mock private CustomOAuth2User customOAuth2User; + @Mock + private CookieUtil cookieUtil; + private MockHttpServletRequest request; private MockHttpServletResponse response; private UserEntity mockUser; @@ -64,6 +71,8 @@ void onAuthenticationSuccess_success() throws IOException { when(authentication.getPrincipal()).thenReturn(customOAuth2User); when(customOAuth2User.getUserEntity()).thenReturn(mockUser); when(jwtTokenProvider.createToken(customOAuth2User)).thenReturn(mockToken); + when(cookieUtil.createRefreshCookie(mockToken.refreshToken())) + .thenReturn(ResponseCookie.from("refreshToken", "refreshToken").build()); // when oAuthSuccessHandler.onAuthenticationSuccess(request, response, authentication); @@ -88,6 +97,8 @@ void onAuthenticationSuccess_setsAccessTokenInHeader() throws IOException { when(authentication.getPrincipal()).thenReturn(customOAuth2User); when(customOAuth2User.getUserEntity()).thenReturn(mockUser); when(jwtTokenProvider.createToken(customOAuth2User)).thenReturn(mockToken); + when(cookieUtil.createRefreshCookie(mockToken.refreshToken())) + .thenReturn(ResponseCookie.from("refreshToken", "refreshToken").build()); // when oAuthSuccessHandler.onAuthenticationSuccess(request, response, authentication); @@ -104,6 +115,8 @@ void onAuthenticationSuccess_setsRefreshTokenInCookie() throws IOException { when(authentication.getPrincipal()).thenReturn(customOAuth2User); when(customOAuth2User.getUserEntity()).thenReturn(mockUser); when(jwtTokenProvider.createToken(customOAuth2User)).thenReturn(mockToken); + when(cookieUtil.createRefreshCookie(mockToken.refreshToken())) + .thenReturn(ResponseCookie.from("refreshToken", mockToken.refreshToken()).build()); // when oAuthSuccessHandler.onAuthenticationSuccess(request, response, authentication); @@ -123,6 +136,8 @@ void onAuthenticationSuccess_redirectsToCorrectUrl() throws IOException { when(authentication.getPrincipal()).thenReturn(customOAuth2User); when(customOAuth2User.getUserEntity()).thenReturn(mockUser); when(jwtTokenProvider.createToken(customOAuth2User)).thenReturn(mockToken); + when(cookieUtil.createRefreshCookie(mockToken.refreshToken())) + .thenReturn(ResponseCookie.from("refreshToken", "refreshToken").build()); // when oAuthSuccessHandler.onAuthenticationSuccess(request, response, authentication); diff --git a/src/test/java/hanium/modic/backend/web/auth/controller/AuthControllerIntegrationTest.java b/src/test/java/hanium/modic/backend/web/auth/controller/AuthControllerIntegrationTest.java index 1726b5aa..d491f3ae 100644 --- a/src/test/java/hanium/modic/backend/web/auth/controller/AuthControllerIntegrationTest.java +++ b/src/test/java/hanium/modic/backend/web/auth/controller/AuthControllerIntegrationTest.java @@ -8,6 +8,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; +import org.springframework.http.ResponseCookie; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import hanium.modic.backend.base.BaseIntegrationTest; @@ -41,6 +42,9 @@ public class AuthControllerIntegrationTest extends BaseIntegrationTest { @Autowired private AuthCodeRepository authCodeRepository; + @Autowired + private CookieUtil cookieUtil; + @Test @DisplayName("로그인 API 테스트") void loginApiSuccessTest() throws Exception { @@ -77,11 +81,19 @@ void reissueApiSuccess() throws Exception { .build(); refreshTokenRepository.save(refreshToken); - Cookie refreshCookie = CookieUtil.createRefreshCookie(token.refreshToken()); + ResponseCookie refreshCookie = cookieUtil.createRefreshCookie(token.refreshToken()); + + // ResponseCookie → Cookie 변환 + Cookie servletCookie = new Cookie(refreshCookie.getName(), refreshCookie.getValue()); + + // 옵션 맞춰주기 (테스트용이라 최소 설정만) + servletCookie.setPath(refreshCookie.getPath()); + servletCookie.setHttpOnly(refreshCookie.isHttpOnly()); + servletCookie.setSecure(refreshCookie.isSecure()); // when, then mockMvc.perform(post("/api/auth/reissue") - .cookie(refreshCookie)) + .cookie(servletCookie)) .andExpect(status().isOk()) .andExpect(header().string("Authorization", startsWith("Bearer "))) .andExpect(cookie().exists("refreshToken")); diff --git a/src/test/java/hanium/modic/backend/web/auth/controller/AuthControllerTest.java b/src/test/java/hanium/modic/backend/web/auth/controller/AuthControllerTest.java index 626e1dad..9ed25653 100644 --- a/src/test/java/hanium/modic/backend/web/auth/controller/AuthControllerTest.java +++ b/src/test/java/hanium/modic/backend/web/auth/controller/AuthControllerTest.java @@ -18,6 +18,7 @@ import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.http.MediaType; +import org.springframework.http.ResponseCookie; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; @@ -27,7 +28,9 @@ import com.fasterxml.jackson.databind.ObjectMapper; import hanium.modic.backend.base.BaseControllerTest; +import hanium.modic.backend.common.jwt.JwtTokenProvider; import hanium.modic.backend.domain.auth.service.AuthService; +import hanium.modic.backend.domain.auth.util.CookieUtil; import hanium.modic.backend.web.auth.dto.CheckEmailDuplicateResponse; import hanium.modic.backend.web.auth.dto.LoginRequest; import hanium.modic.backend.web.auth.dto.LoginResponse; @@ -44,8 +47,12 @@ class AuthControllerTest extends BaseControllerTest { @Autowired private MockMvc mockMvc; + @MockitoBean + private CookieUtil cookieUtil; @MockitoBean private AuthService authService; + @MockitoBean + private JwtTokenProvider jwtTokenProvider; private final ObjectMapper objectMapper = new ObjectMapper(); @@ -59,6 +66,8 @@ void loginSuccess() throws Exception { when(authService.login(email, password)) .thenReturn(new LoginResponse("accessToken", "refreshToken")); + when(cookieUtil.createRefreshCookie("refreshToken")) + .thenReturn(ResponseCookie.from("refreshToken", "refreshToken").build()); // when MvcResult result = mockMvc.perform(post("/api/auth/login") @@ -127,6 +136,8 @@ void reissueSuccess() throws Exception { ReissueResponse mockResponse = new ReissueResponse(newAccessToken, newRefreshToken); when(authService.reissue(oldRefreshToken)).thenReturn(mockResponse); + when(cookieUtil.createRefreshCookie(newRefreshToken)) + .thenReturn(ResponseCookie.from("refreshToken", newRefreshToken).build()); // when, then mockMvc.perform(post("/api/auth/reissue") diff --git a/src/test/java/hanium/modic/backend/web/user/controller/UserControllerTest.java b/src/test/java/hanium/modic/backend/web/user/controller/UserControllerTest.java index 29c8cbca..f93ec581 100644 --- a/src/test/java/hanium/modic/backend/web/user/controller/UserControllerTest.java +++ b/src/test/java/hanium/modic/backend/web/user/controller/UserControllerTest.java @@ -28,6 +28,7 @@ import hanium.modic.backend.base.BaseControllerTest; import hanium.modic.backend.common.jwt.JwtTokenProvider; +import hanium.modic.backend.domain.auth.util.CookieUtil; import hanium.modic.backend.domain.user.entity.UserEntity; import hanium.modic.backend.domain.user.factory.UserFactory; import hanium.modic.backend.domain.user.service.UserCoinService; @@ -48,12 +49,14 @@ class UserControllerTest extends BaseControllerTest { private UserService userService; @MockitoBean private UserCoinService userCoinService; + @MockitoBean + private CookieUtil cookieUtil; + @MockitoBean + private JwtTokenProvider jwtTokenProvider; @Autowired private MockMvc mockMvc; - @MockitoBean - private JwtTokenProvider jwtTokenProvider; private final ObjectMapper objectMapper = new ObjectMapper();