From f2b816e053749344a66c67af3b6cae280c40fc5b Mon Sep 17 00:00:00 2001 From: yooooonshine Date: Sun, 9 Nov 2025 14:01:43 +0900 Subject: [PATCH 01/11] =?UTF-8?q?feat:=20CookieUtil=EC=9D=84=20=EA=B0=9C?= =?UTF-8?q?=EB=B0=9C=EC=84=9C=EB=B2=84=EC=9A=A9=EA=B3=BC=20=EB=B0=B0?= =?UTF-8?q?=ED=8F=AC=EC=84=9C=EB=B2=84=EC=9A=A9=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/oauth/OAuthSuccessHandler.java | 3 ++- .../backend/domain/auth/util/CookieUtil.java | 16 ++---------- .../domain/auth/util/DevCookieUtil.java | 24 ++++++++++++++++++ .../domain/auth/util/ProdCookieUtil.java | 25 +++++++++++++++++++ .../web/auth/controller/AuthController.java | 5 ++-- .../common/oauth/OAuthSuccessHandlerTest.java | 14 +++++++++++ .../AuthControllerIntegrationTest.java | 5 +++- .../auth/controller/AuthControllerTest.java | 7 ++++++ 8 files changed, 81 insertions(+), 18 deletions(-) create mode 100644 src/main/java/hanium/modic/backend/domain/auth/util/DevCookieUtil.java create mode 100644 src/main/java/hanium/modic/backend/domain/auth/util/ProdCookieUtil.java 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..85b41ed4 100644 --- a/src/main/java/hanium/modic/backend/common/oauth/OAuthSuccessHandler.java +++ b/src/main/java/hanium/modic/backend/common/oauth/OAuthSuccessHandler.java @@ -26,6 +26,7 @@ public class OAuthSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { private final JwtTokenProvider jwtTokenProvider; private final RefreshTokenRepository refreshTokenRepository; + private final CookieUtil cookieUtil; @Override @Transactional @@ -44,7 +45,7 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo // 엑세스 토큰과 리프레시 토큰을 응답 헤더와 쿠키에 설정 response.addHeader(AuthConstant.AUTHORIZATION, AuthConstant.BEARER + token.accessToken()); - Cookie refreshTokenCookie = CookieUtil.createRefreshCookie(token.refreshToken()); + Cookie refreshTokenCookie = cookieUtil.createRefreshCookie(token.refreshToken()); response.addCookie(refreshTokenCookie); // Todo: 리다이렉트 URL을 환경 변수로 관리 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..81026dde 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 @@ -2,18 +2,6 @@ import jakarta.servlet.http.Cookie; -public class CookieUtil { - - 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; - } +public interface CookieUtil { + Cookie createRefreshCookie(final String refreshToken); } 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..912d5006 --- /dev/null +++ b/src/main/java/hanium/modic/backend/domain/auth/util/DevCookieUtil.java @@ -0,0 +1,24 @@ +package hanium.modic.backend.domain.auth.util; + +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +import jakarta.servlet.http.Cookie; + +// 개발 환경용 쿠키 유틸리티 +@Component +@Profile({"local", "dev", "test"}) +public class DevCookieUtil implements CookieUtil { + + private final String REFRESH_TOKEN_COOKIE_NAME = "refreshToken"; + + private final int COOKIE_MAX_AGE = 60 * 60 * 24 * 3; + + public Cookie createRefreshCookie(final String refreshToken) { + Cookie cookie = new Cookie(REFRESH_TOKEN_COOKIE_NAME, refreshToken); + cookie.setHttpOnly(true); + cookie.setPath("/"); + cookie.setMaxAge(COOKIE_MAX_AGE); + return cookie; + } +} 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..37791de7 --- /dev/null +++ b/src/main/java/hanium/modic/backend/domain/auth/util/ProdCookieUtil.java @@ -0,0 +1,25 @@ +package hanium.modic.backend.domain.auth.util; + +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +import jakarta.servlet.http.Cookie; + +// Production 환경용 쿠키 유틸리티 +@Component +@Profile({"main"}) +public class ProdCookieUtil implements CookieUtil { + + private final String REFRESH_TOKEN_COOKIE_NAME = "refreshToken"; + + private final int COOKIE_MAX_AGE = 60 * 60 * 24 * 3; + + public Cookie createRefreshCookie(final String refreshToken) { + Cookie cookie = new Cookie(REFRESH_TOKEN_COOKIE_NAME, refreshToken); + cookie.setHttpOnly(true); + cookie.setPath("/"); + cookie.setSecure(true); + cookie.setMaxAge(COOKIE_MAX_AGE); + return cookie; + } +} 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..5b7883b7 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 @@ -37,6 +37,7 @@ public class AuthController { private final AuthService authService; + private final CookieUtil cookieUtil; @PostMapping("/login") @Operation( @@ -52,7 +53,7 @@ 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()); + Cookie refreshTokenCookie = cookieUtil.createRefreshCookie(loginResponse.refreshToken()); response.addCookie(refreshTokenCookie); return ResponseEntity.ok(AppResponse.ok(loginResponse)); @@ -73,7 +74,7 @@ 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()); + Cookie refreshTokenCookie = cookieUtil.createRefreshCookie(reissueResponse.refreshToken()); response.addCookie(refreshTokenCookie); return ResponseEntity.ok().build(); 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..6082b7a9 100644 --- a/src/test/java/hanium/modic/backend/common/oauth/OAuthSuccessHandlerTest.java +++ b/src/test/java/hanium/modic/backend/common/oauth/OAuthSuccessHandlerTest.java @@ -16,16 +16,19 @@ 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 +47,9 @@ class OAuthSuccessHandlerTest { @Mock private CustomOAuth2User customOAuth2User; + @Mock + private CookieUtil cookieUtil; + private MockHttpServletRequest request; private MockHttpServletResponse response; private UserEntity mockUser; @@ -64,6 +70,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(new Cookie("refreshToken", mockToken.refreshToken())); // when oAuthSuccessHandler.onAuthenticationSuccess(request, response, authentication); @@ -88,6 +96,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(new Cookie("refreshToken", mockToken.refreshToken())); // when oAuthSuccessHandler.onAuthenticationSuccess(request, response, authentication); @@ -104,6 +114,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(new Cookie("refreshToken", mockToken.refreshToken())); // when oAuthSuccessHandler.onAuthenticationSuccess(request, response, authentication); @@ -123,6 +135,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(new Cookie("refreshToken", mockToken.refreshToken())); // 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..d4c71d83 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 @@ -41,6 +41,9 @@ public class AuthControllerIntegrationTest extends BaseIntegrationTest { @Autowired private AuthCodeRepository authCodeRepository; + @Autowired + private CookieUtil cookieUtil; + @Test @DisplayName("로그인 API 테스트") void loginApiSuccessTest() throws Exception { @@ -77,7 +80,7 @@ void reissueApiSuccess() throws Exception { .build(); refreshTokenRepository.save(refreshToken); - Cookie refreshCookie = CookieUtil.createRefreshCookie(token.refreshToken()); + Cookie refreshCookie = cookieUtil.createRefreshCookie(token.refreshToken()); // when, then mockMvc.perform(post("/api/auth/reissue") 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..af21ebce 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 @@ -28,6 +28,7 @@ import hanium.modic.backend.base.BaseControllerTest; 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,6 +45,8 @@ class AuthControllerTest extends BaseControllerTest { @Autowired private MockMvc mockMvc; + @MockitoBean + private CookieUtil cookieUtil; @MockitoBean private AuthService authService; @@ -59,6 +62,8 @@ void loginSuccess() throws Exception { when(authService.login(email, password)) .thenReturn(new LoginResponse("accessToken", "refreshToken")); + when(cookieUtil.createRefreshCookie("refreshToken")) + .thenReturn(new Cookie("refreshToken", "refreshToken")); // when MvcResult result = mockMvc.perform(post("/api/auth/login") @@ -127,6 +132,8 @@ void reissueSuccess() throws Exception { ReissueResponse mockResponse = new ReissueResponse(newAccessToken, newRefreshToken); when(authService.reissue(oldRefreshToken)).thenReturn(mockResponse); + when(cookieUtil.createRefreshCookie(newRefreshToken)) + .thenReturn(new Cookie("refreshToken", newRefreshToken)); // when, then mockMvc.perform(post("/api/auth/reissue") From be637e685ca110a9ca5047fc7d8462fc691a0594 Mon Sep 17 00:00:00 2001 From: yooooonshine Date: Sun, 9 Nov 2025 18:46:28 +0900 Subject: [PATCH 02/11] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/common/jwt/JwtTokenProvider.java | 8 ++++++ .../domain/auth/service/AuthService.java | 10 +++++++ .../backend/domain/auth/util/CookieUtil.java | 2 ++ .../domain/auth/util/DevCookieUtil.java | 8 ++++++ .../domain/auth/util/ProdCookieUtil.java | 9 ++++++ .../web/auth/controller/AuthController.java | 28 +++++++++++++++++++ 6 files changed, 65 insertions(+) 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/domain/auth/service/AuthService.java b/src/main/java/hanium/modic/backend/domain/auth/service/AuthService.java index 66cb0255..ce7b1a1a 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 @@ -58,6 +58,16 @@ public LoginResponse login(final String email, final String password) { return LoginResponse.from(token); } + public void logout(final String refreshToken, final String accessToken) { + jwtTokenProvider.setBlackList(refreshToken); + jwtTokenProvider.setBlackList(accessToken); + + UserEntity user = jwtTokenProvider.getUser(refreshToken) + .orElseThrow(() -> new AppException(ErrorCode.USER_NOT_FOUND_EXCEPTION)); + + refreshTokenRepository.deleteById(user.getId()); + } + 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 81026dde..cc31f09c 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 @@ -4,4 +4,6 @@ public interface CookieUtil { Cookie createRefreshCookie(final String refreshToken); + + Cookie 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 index 912d5006..347bf748 100644 --- a/src/main/java/hanium/modic/backend/domain/auth/util/DevCookieUtil.java +++ b/src/main/java/hanium/modic/backend/domain/auth/util/DevCookieUtil.java @@ -21,4 +21,12 @@ public Cookie createRefreshCookie(final String refreshToken) { cookie.setMaxAge(COOKIE_MAX_AGE); return cookie; } + + public Cookie deleteRefreshCookie() { + Cookie cookie = new Cookie(REFRESH_TOKEN_COOKIE_NAME, null); + cookie.setHttpOnly(true); + cookie.setPath("/"); // 생성 시와 동일해야 함 + cookie.setMaxAge(0); // 브라우저에서 즉시 삭제 + return cookie; + } } 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 index 37791de7..43e19d61 100644 --- a/src/main/java/hanium/modic/backend/domain/auth/util/ProdCookieUtil.java +++ b/src/main/java/hanium/modic/backend/domain/auth/util/ProdCookieUtil.java @@ -22,4 +22,13 @@ public Cookie createRefreshCookie(final String refreshToken) { cookie.setMaxAge(COOKIE_MAX_AGE); return cookie; } + + public Cookie deleteRefreshCookie() { + Cookie cookie = new Cookie(REFRESH_TOKEN_COOKIE_NAME, null); + cookie.setHttpOnly(true); + cookie.setPath("/"); // 생성 시와 동일해야 함 + cookie.setSecure(true); + cookie.setMaxAge(0); // 브라우저에서 즉시 삭제 + return cookie; + } } 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 5b7883b7..0ec6cf7a 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,7 @@ package hanium.modic.backend.web.auth.controller; +import static hanium.modic.backend.common.error.ErrorCode.*; + import org.springframework.http.ResponseEntity; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.CookieValue; @@ -10,7 +12,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; @@ -24,6 +28,7 @@ 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; @@ -38,6 +43,7 @@ public class AuthController { private final AuthService authService; private final CookieUtil cookieUtil; + private final JwtTokenProvider jwtTokenProvider; @PostMapping("/login") @Operation( @@ -59,6 +65,28 @@ public ResponseEntity> login(@RequestBody @Valid Logi 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).orElse(null); + authService.logout(refreshToken, accessToken); + + Cookie deleteRefreshTokenCookie = cookieUtil.deleteRefreshCookie(); + response.addCookie(deleteRefreshTokenCookie); + + return ResponseEntity.ok().build(); + } + @PostMapping("/reissue") @Operation( summary = "토큰 재발급 API", From 9996297420a78b6a988c87e0b897062b9725d560 Mon Sep 17 00:00:00 2001 From: yooooonshine Date: Sun, 9 Nov 2025 21:43:30 +0900 Subject: [PATCH 03/11] =?UTF-8?q?feat:=20=EC=9C=A0=EC=A0=80=ED=83=88?= =?UTF-8?q?=ED=87=B4=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../modic/backend/domain/user/entity/UserEntity.java | 6 ++++++ .../modic/backend/domain/user/enums/UserRole.java | 4 +++- .../backend/domain/user/service/UserService.java | 10 ++++++++++ .../backend/web/user/controller/UserController.java | 11 +++++++++++ 4 files changed, 30 insertions(+), 1 deletion(-) 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..65e3bd6e 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,16 @@ private void checkDuplicateEmail(final String email) { } } + // 회원탈퇴 + @Transactional + public void deleteUser(final long id) { + UserEntity user = userEntityRepository.findById(id) + .orElseThrow(() -> new AppException(ErrorCode.USER_NOT_FOUND_EXCEPTION)); + user.softWithdraw(); + + userEntityRepository.save(user); + } + // 회원 정보 조회 public UserInfoResponse getUserInfo(UserEntity user) { Optional userImageUrl = userImageService.createImageGetUrlOptional(user.getId()); 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..00f4b791 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 @@ -5,6 +5,7 @@ import org.springframework.data.domain.Page; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +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; @@ -63,6 +64,16 @@ public ResponseEntity> createUser(@RequestBody @ ))); } + @DeleteMapping + @Operation( + summary = "회원탈퇴 API", + description = "로그인한 유저의 계정을 삭제합니다." + ) + public ResponseEntity> deleteUser(@CurrentUser UserEntity user) { + userService.deleteUser(user.getId()); + return ResponseEntity.ok().build(); + } + @GetMapping("/me") @Operation( summary = "유저 정보 조회 API", From 34b5a191ec99fd5e3e7a32cf71cf7450ad50b137 Mon Sep 17 00:00:00 2001 From: yooooonshine Date: Sun, 9 Nov 2025 21:54:48 +0900 Subject: [PATCH 04/11] =?UTF-8?q?feat:=20=EB=A1=9C=EC=A7=81=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=EC=97=90=20=EB=94=B0=EB=A5=B8=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../modic/backend/web/auth/controller/AuthControllerTest.java | 3 +++ 1 file changed, 3 insertions(+) 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 af21ebce..13f0a9b5 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 @@ -27,6 +27,7 @@ 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; @@ -49,6 +50,8 @@ class AuthControllerTest extends BaseControllerTest { private CookieUtil cookieUtil; @MockitoBean private AuthService authService; + @MockitoBean + private JwtTokenProvider jwtTokenProvider; private final ObjectMapper objectMapper = new ObjectMapper(); From f832a230e563d580add064e1cda9207f56dee55d Mon Sep 17 00:00:00 2001 From: yooooonshine Date: Sun, 9 Nov 2025 22:30:08 +0900 Subject: [PATCH 05/11] =?UTF-8?q?refactor:=20Cookie=EB=A5=BC=20ResponseCoo?= =?UTF-8?q?kie=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/oauth/OAuthSuccessHandler.java | 7 ++-- .../backend/domain/auth/util/CookieUtil.java | 6 ++-- .../domain/auth/util/DevCookieUtil.java | 35 ++++++++++--------- .../domain/auth/util/ProdCookieUtil.java | 35 ++++++++++--------- .../web/auth/controller/AuthController.java | 15 ++++---- .../common/oauth/OAuthSuccessHandlerTest.java | 9 ++--- .../AuthControllerIntegrationTest.java | 13 +++++-- .../auth/controller/AuthControllerTest.java | 5 +-- 8 files changed, 72 insertions(+), 53 deletions(-) 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 85b41ed4..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; @@ -45,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/domain/auth/util/CookieUtil.java b/src/main/java/hanium/modic/backend/domain/auth/util/CookieUtil.java index cc31f09c..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,9 +1,9 @@ package hanium.modic.backend.domain.auth.util; -import jakarta.servlet.http.Cookie; +import org.springframework.http.ResponseCookie; public interface CookieUtil { - Cookie createRefreshCookie(final String refreshToken); + ResponseCookie createRefreshCookie(final String refreshToken); - Cookie deleteRefreshCookie(); + 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 index 347bf748..96ef6a83 100644 --- a/src/main/java/hanium/modic/backend/domain/auth/util/DevCookieUtil.java +++ b/src/main/java/hanium/modic/backend/domain/auth/util/DevCookieUtil.java @@ -1,32 +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; -import jakarta.servlet.http.Cookie; - // 개발 환경용 쿠키 유틸리티 @Component @Profile({"local", "dev", "test"}) public class DevCookieUtil implements CookieUtil { - private final String REFRESH_TOKEN_COOKIE_NAME = "refreshToken"; + private static final String REFRESH_TOKEN_COOKIE_NAME = "refreshToken"; - private final int COOKIE_MAX_AGE = 60 * 60 * 24 * 3; + private static final int COOKIE_MAX_AGE = 60 * 60 * 24 * 3; - public Cookie createRefreshCookie(final String refreshToken) { - Cookie cookie = new Cookie(REFRESH_TOKEN_COOKIE_NAME, refreshToken); - cookie.setHttpOnly(true); - cookie.setPath("/"); - cookie.setMaxAge(COOKIE_MAX_AGE); - return cookie; + 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 Cookie deleteRefreshCookie() { - Cookie cookie = new Cookie(REFRESH_TOKEN_COOKIE_NAME, null); - cookie.setHttpOnly(true); - cookie.setPath("/"); // 생성 시와 동일해야 함 - cookie.setMaxAge(0); // 브라우저에서 즉시 삭제 - return cookie; + 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 index 43e19d61..eef85f82 100644 --- a/src/main/java/hanium/modic/backend/domain/auth/util/ProdCookieUtil.java +++ b/src/main/java/hanium/modic/backend/domain/auth/util/ProdCookieUtil.java @@ -1,6 +1,7 @@ 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; @@ -10,25 +11,27 @@ @Profile({"main"}) public class ProdCookieUtil implements CookieUtil { - private final String REFRESH_TOKEN_COOKIE_NAME = "refreshToken"; + private static final String REFRESH_TOKEN_COOKIE_NAME = "refreshToken"; - private final int COOKIE_MAX_AGE = 60 * 60 * 24 * 3; + private static final int COOKIE_MAX_AGE = 60 * 60 * 24 * 3; - public Cookie createRefreshCookie(final String refreshToken) { - Cookie cookie = new Cookie(REFRESH_TOKEN_COOKIE_NAME, refreshToken); - cookie.setHttpOnly(true); - cookie.setPath("/"); - cookie.setSecure(true); - cookie.setMaxAge(COOKIE_MAX_AGE); - return cookie; + 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 Cookie deleteRefreshCookie() { - Cookie cookie = new Cookie(REFRESH_TOKEN_COOKIE_NAME, null); - cookie.setHttpOnly(true); - cookie.setPath("/"); // 생성 시와 동일해야 함 - cookie.setSecure(true); - cookie.setMaxAge(0); // 브라우저에서 즉시 삭제 - return cookie; + 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/web/auth/controller/AuthController.java b/src/main/java/hanium/modic/backend/web/auth/controller/AuthController.java index 0ec6cf7a..b98c4c71 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 @@ -2,6 +2,8 @@ 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; @@ -27,7 +29,6 @@ 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; @@ -59,8 +60,8 @@ 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)); } @@ -81,8 +82,8 @@ public ResponseEntity> logout( String accessToken = jwtTokenProvider.extractAccessToken(request).orElse(null); authService.logout(refreshToken, accessToken); - Cookie deleteRefreshTokenCookie = cookieUtil.deleteRefreshCookie(); - response.addCookie(deleteRefreshTokenCookie); + ResponseCookie deleteRefreshTokenCookie = cookieUtil.deleteRefreshCookie(); + response.addHeader(HttpHeaders.SET_COOKIE, deleteRefreshTokenCookie.toString()); return ResponseEntity.ok().build(); } @@ -102,8 +103,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/test/java/hanium/modic/backend/common/oauth/OAuthSuccessHandlerTest.java b/src/test/java/hanium/modic/backend/common/oauth/OAuthSuccessHandlerTest.java index 6082b7a9..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,6 +13,7 @@ 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; @@ -71,7 +72,7 @@ void onAuthenticationSuccess_success() throws IOException { when(customOAuth2User.getUserEntity()).thenReturn(mockUser); when(jwtTokenProvider.createToken(customOAuth2User)).thenReturn(mockToken); when(cookieUtil.createRefreshCookie(mockToken.refreshToken())) - .thenReturn(new Cookie("refreshToken", mockToken.refreshToken())); + .thenReturn(ResponseCookie.from("refreshToken", "refreshToken").build()); // when oAuthSuccessHandler.onAuthenticationSuccess(request, response, authentication); @@ -97,7 +98,7 @@ void onAuthenticationSuccess_setsAccessTokenInHeader() throws IOException { when(customOAuth2User.getUserEntity()).thenReturn(mockUser); when(jwtTokenProvider.createToken(customOAuth2User)).thenReturn(mockToken); when(cookieUtil.createRefreshCookie(mockToken.refreshToken())) - .thenReturn(new Cookie("refreshToken", mockToken.refreshToken())); + .thenReturn(ResponseCookie.from("refreshToken", "refreshToken").build()); // when oAuthSuccessHandler.onAuthenticationSuccess(request, response, authentication); @@ -115,7 +116,7 @@ void onAuthenticationSuccess_setsRefreshTokenInCookie() throws IOException { when(customOAuth2User.getUserEntity()).thenReturn(mockUser); when(jwtTokenProvider.createToken(customOAuth2User)).thenReturn(mockToken); when(cookieUtil.createRefreshCookie(mockToken.refreshToken())) - .thenReturn(new Cookie("refreshToken", mockToken.refreshToken())); + .thenReturn(ResponseCookie.from("refreshToken", mockToken.refreshToken()).build()); // when oAuthSuccessHandler.onAuthenticationSuccess(request, response, authentication); @@ -136,7 +137,7 @@ void onAuthenticationSuccess_redirectsToCorrectUrl() throws IOException { when(customOAuth2User.getUserEntity()).thenReturn(mockUser); when(jwtTokenProvider.createToken(customOAuth2User)).thenReturn(mockToken); when(cookieUtil.createRefreshCookie(mockToken.refreshToken())) - .thenReturn(new Cookie("refreshToken", 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 d4c71d83..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; @@ -80,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 13f0a9b5..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; @@ -66,7 +67,7 @@ void loginSuccess() throws Exception { when(authService.login(email, password)) .thenReturn(new LoginResponse("accessToken", "refreshToken")); when(cookieUtil.createRefreshCookie("refreshToken")) - .thenReturn(new Cookie("refreshToken", "refreshToken")); + .thenReturn(ResponseCookie.from("refreshToken", "refreshToken").build()); // when MvcResult result = mockMvc.perform(post("/api/auth/login") @@ -136,7 +137,7 @@ void reissueSuccess() throws Exception { when(authService.reissue(oldRefreshToken)).thenReturn(mockResponse); when(cookieUtil.createRefreshCookie(newRefreshToken)) - .thenReturn(new Cookie("refreshToken", newRefreshToken)); + .thenReturn(ResponseCookie.from("refreshToken", newRefreshToken).build()); // when, then mockMvc.perform(post("/api/auth/reissue") From 564fba0e59bcf8f4519d59059a8ef038dcd52b44 Mon Sep 17 00:00:00 2001 From: yooooonshine Date: Sun, 9 Nov 2025 22:34:03 +0900 Subject: [PATCH 06/11] =?UTF-8?q?refactor:=20=ED=83=88=ED=87=B4=ED=95=9C?= =?UTF-8?q?=20=ED=9A=8C=EC=9B=90=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EB=A7=89?= =?UTF-8?q?=EB=8A=94=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/hanium/modic/backend/common/error/ErrorCode.java | 1 + .../modic/backend/domain/auth/service/AuthService.java | 6 ++++++ .../modic/backend/web/auth/controller/AuthController.java | 7 +++++-- 3 files changed, 12 insertions(+), 2 deletions(-) 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/domain/auth/service/AuthService.java b/src/main/java/hanium/modic/backend/domain/auth/service/AuthService.java index 7f904706..3e613146 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 @@ -14,6 +14,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; @@ -44,6 +45,11 @@ 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); } 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 b98c4c71..37092bd2 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 @@ -69,10 +69,13 @@ public ResponseEntity> login(@RequestBody @Valid Logi @PostMapping("/logout") @Operation( summary = "로그아웃 API", - description = "리프레시 토큰을 통해 로그아웃합니다. 로그아웃된 토큰으로 요청시 [C-005] - 차단된 토큰입니다를 반환합니다." + description = """ + 리프레시 토큰을 통해 로그아웃합니다.
+ 로그아웃된 토큰으로 요청시 [C-005] - 차단된 토큰입니다를 반환합니다. + """ ) @ApiErrorMapping({ - USER_NOT_FOUND_EXCEPTION + USER_NOT_FOUND_EXCEPTION, }) public ResponseEntity> logout( @CookieValue(name = "refreshToken") String refreshToken, From 58e0f6ff345da4b259d11e28d5457a4c399c4f0b Mon Sep 17 00:00:00 2001 From: yooooonshine Date: Sun, 9 Nov 2025 22:39:44 +0900 Subject: [PATCH 07/11] =?UTF-8?q?refactor:=20=ED=9A=8C=EC=9B=90=ED=83=88?= =?UTF-8?q?=ED=87=B4=20=EC=8B=9C=20=ED=86=A0=ED=81=B0=EB=8F=84=20=EC=B4=88?= =?UTF-8?q?=EA=B8=B0=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/auth/service/AuthService.java | 4 ++++ .../domain/user/service/UserService.java | 6 ++++- .../web/user/controller/UserController.java | 23 +++++++++++++++++-- 3 files changed, 30 insertions(+), 3 deletions(-) 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 3e613146..37d4336f 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; @@ -41,6 +42,7 @@ 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)); @@ -65,6 +67,7 @@ public LoginResponse login(final String email, final String password) { return LoginResponse.from(token); } + @Transactional public void logout(final String refreshToken, final String accessToken) { jwtTokenProvider.setBlackList(refreshToken); jwtTokenProvider.setBlackList(accessToken); @@ -75,6 +78,7 @@ public void logout(final String refreshToken, final String 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/user/service/UserService.java b/src/main/java/hanium/modic/backend/domain/user/service/UserService.java index 65e3bd6e..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 @@ -74,12 +74,16 @@ private void checkDuplicateEmail(final String email) { // 회원탈퇴 @Transactional - public void deleteUser(final long id) { + 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); } // 회원 정보 조회 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 00f4b791..2c74433a 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,11 @@ 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; @@ -15,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; @@ -34,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; @@ -47,6 +54,8 @@ public class UserController { private final UserService userService; private final UserCoinService userCoinService; + private final CookieUtil cookieUtil; + private final JwtTokenProvider jwtTokenProvider; @PostMapping @Operation( @@ -69,8 +78,18 @@ public ResponseEntity> createUser(@RequestBody @ summary = "회원탈퇴 API", description = "로그인한 유저의 계정을 삭제합니다." ) - public ResponseEntity> deleteUser(@CurrentUser UserEntity user) { - userService.deleteUser(user.getId()); + public ResponseEntity> deleteUser( + @CurrentUser UserEntity user, + @CookieValue(name = "refreshToken") String refreshToken, + HttpServletRequest request, + HttpServletResponse response + ) { + String accessToken = jwtTokenProvider.extractAccessToken(request).orElse(null); + userService.deleteAndLogout(user.getId(), refreshToken, accessToken); + + ResponseCookie deleteRefreshTokenCookie = cookieUtil.deleteRefreshCookie(); + response.addHeader(HttpHeaders.SET_COOKIE, deleteRefreshTokenCookie.toString()); + return ResponseEntity.ok().build(); } From f499194a5325ec7a1d544a188e3c544683a44c17 Mon Sep 17 00:00:00 2001 From: yooooonshine Date: Sun, 9 Nov 2025 22:42:02 +0900 Subject: [PATCH 08/11] =?UTF-8?q?refactor:=20WITHDRAW=20=EC=9C=A0=EC=A0=80?= =?UTF-8?q?=20Security=20=EB=8B=A8=EC=97=90=EC=84=9C=20=EC=B0=A8=EB=8B=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hanium/modic/backend/common/security/SecurityConfig.java | 2 +- .../backend/common/security/principal/UserPrincipal.java | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) 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 From d2d34d8185c608467914b23e7ffc8f0a29b1bc85 Mon Sep 17 00:00:00 2001 From: yooooonshine Date: Sun, 9 Nov 2025 22:43:51 +0900 Subject: [PATCH 09/11] =?UTF-8?q?refactor:=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=20=EB=A1=9C=EC=A7=81=20=EC=88=9C=EC=84=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20-=20User=20=EC=A1=B0=ED=9A=8C=EB=A5=BC=20=EB=A8=BC?= =?UTF-8?q?=EC=A0=80=ED=95=98=EA=B2=8C=20=ED=95=A8=EC=9C=BC=EB=A1=9C?= =?UTF-8?q?=EC=84=9C=20=EC=95=88=EC=A0=95=EC=84=B1=20=ED=99=95=EB=B3=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../modic/backend/domain/auth/service/AuthService.java | 6 +++--- .../modic/backend/web/auth/controller/AuthController.java | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) 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 37d4336f..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 @@ -69,12 +69,12 @@ public LoginResponse login(final String email, final String password) { @Transactional public void logout(final String refreshToken, final String accessToken) { - jwtTokenProvider.setBlackList(refreshToken); - jwtTokenProvider.setBlackList(accessToken); - UserEntity user = jwtTokenProvider.getUser(refreshToken) .orElseThrow(() -> new AppException(ErrorCode.USER_NOT_FOUND_EXCEPTION)); + jwtTokenProvider.setBlackList(refreshToken); + jwtTokenProvider.setBlackList(accessToken); + refreshTokenRepository.deleteById(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 37092bd2..35607aec 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 @@ -88,7 +88,7 @@ public ResponseEntity> logout( ResponseCookie deleteRefreshTokenCookie = cookieUtil.deleteRefreshCookie(); response.addHeader(HttpHeaders.SET_COOKIE, deleteRefreshTokenCookie.toString()); - return ResponseEntity.ok().build(); + return ResponseEntity.ok(AppResponse.noContent()); } @PostMapping("/reissue") From 9268fb8d36a94f8aaa1209505f677253d2185387 Mon Sep 17 00:00:00 2001 From: yooooonshine Date: Sun, 9 Nov 2025 22:48:01 +0900 Subject: [PATCH 10/11] =?UTF-8?q?refactor:=20jwtTokenProvider.extractAcces?= =?UTF-8?q?sToken(request).get()=EC=97=90=EC=84=9CorElse=EB=A5=BC=20get()?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../modic/backend/web/auth/controller/AuthController.java | 2 +- .../modic/backend/web/user/controller/UserController.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 35607aec..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 @@ -82,7 +82,7 @@ public ResponseEntity> logout( HttpServletRequest request, HttpServletResponse response ) { - String accessToken = jwtTokenProvider.extractAccessToken(request).orElse(null); + String accessToken = jwtTokenProvider.extractAccessToken(request).get(); // accessToken은 무조건 존재함 authService.logout(refreshToken, accessToken); ResponseCookie deleteRefreshTokenCookie = cookieUtil.deleteRefreshCookie(); 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 2c74433a..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 @@ -84,7 +84,7 @@ public ResponseEntity> deleteUser( HttpServletRequest request, HttpServletResponse response ) { - String accessToken = jwtTokenProvider.extractAccessToken(request).orElse(null); + String accessToken = jwtTokenProvider.extractAccessToken(request).get(); // accessToken은 무조건 존재함 userService.deleteAndLogout(user.getId(), refreshToken, accessToken); ResponseCookie deleteRefreshTokenCookie = cookieUtil.deleteRefreshCookie(); From 752481cc4c7e3fe6b83a542796b14591c16f5352 Mon Sep 17 00:00:00 2001 From: yooooonshine Date: Sun, 9 Nov 2025 22:51:56 +0900 Subject: [PATCH 11/11] =?UTF-8?q?refactor:=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=EC=97=90=20=EB=94=B0=EB=A5=B8=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/web/user/controller/UserControllerTest.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) 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();