-
Notifications
You must be signed in to change notification settings - Fork 1
[Feat] : 로그아웃 및 회원탈퇴 기능 구현 #251
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 10 commits
f2b816e
be637e6
9996297
f83b904
34b5a19
f832a23
564fba0
58e0f6f
f499194
d2d34d8
9268fb8
752481c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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(); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,7 @@ | ||
| package hanium.modic.backend.domain.user.enums; | ||
|
|
||
| public enum UserRole { | ||
| ADMIN, USER | ||
| ADMIN, | ||
| USER, | ||
| WITHDRAWN | ||
yooooonshine marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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<AppResponse<LoginResponse>> 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 = """ | ||||||||||||
| 리프레시 토큰을 통해 로그아웃합니다. <br> | ||||||||||||
| 로그아웃된 토큰으로 요청시 [C-005] - 차단된 토큰입니다를 반환합니다. | ||||||||||||
| """ | ||||||||||||
| ) | ||||||||||||
| @ApiErrorMapping({ | ||||||||||||
| USER_NOT_FOUND_EXCEPTION, | ||||||||||||
| }) | ||||||||||||
| public ResponseEntity<AppResponse<Void>> logout( | ||||||||||||
| @CookieValue(name = "refreshToken") String refreshToken, | ||||||||||||
| HttpServletRequest request, | ||||||||||||
| HttpServletResponse response | ||||||||||||
| ) { | ||||||||||||
| String accessToken = jwtTokenProvider.extractAccessToken(request).orElse(null); | ||||||||||||
| authService.logout(refreshToken, accessToken); | ||||||||||||
|
|
||||||||||||
|
||||||||||||
| String accessToken = jwtTokenProvider.extractAccessToken(request).orElse(null); | |
| authService.logout(refreshToken, accessToken); | |
| String accessToken = jwtTokenProvider.extractAccessToken(request) | |
| .orElseThrow(() -> new AppException(USER_NOT_AUTHENTICATED_EXCEPTION)); | |
| authService.logout(refreshToken, accessToken); |
🤖 Prompt for AI Agents
In src/main/java/hanium/modic/backend/web/auth/controller/AuthController.java
around lines 85 to 87, the code extracts the access token as an Optional and
passes null to authService.logout when the Authorization header is missing;
instead fail fast: if the access token Optional is empty, throw an
authentication/authorization exception (e.g., BadCredentialsException or a
custom AuthException) immediately rather than passing null, otherwise call
authService.logout with the non-null access token; ensure
jwtTokenProvider.setBlackList is never called with null by enforcing the
presence of the token before calling logout.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
모든 요청에 인증을 요구하면 로그인이 불가능합니다.
.anyRequest().authenticated()로 변경하면 로그인, 회원가입, 이메일 인증 등의 공개 엔드포인트도 인증을 요구하게 되어 사용자가 로그인할 수 없습니다.다음과 같이 공개 엔드포인트를 명시적으로 허용해야 합니다:
📝 Committable suggestion
🤖 Prompt for AI Agents