diff --git a/src/docs/asciidoc/auth-api.adoc b/src/docs/asciidoc/auth-api.adoc index 1890e27..744c062 100644 --- a/src/docs/asciidoc/auth-api.adoc +++ b/src/docs/asciidoc/auth-api.adoc @@ -54,7 +54,7 @@ include::{snippetsDir}/kakaoLogin/4/http-response.adoc[] === **3. 로그아웃** -로그아웃 api 입니다. 요청 헤더에 Key=Cookie, Value="SESSION=" 형식의 세션 쿠키가 존재해야합니다. +로그아웃 api 입니다. ==== Request include::{snippetsDir}/logout/1/http-request.adoc[] @@ -68,13 +68,10 @@ include::{snippetsDir}/logout/1/http-response.adoc[] ==== Response Body Fields include::{snippetsDir}/logout/1/response-fields.adoc[] -==== 실패 Response -include::{snippetsDir}/logout/2/http-response.adoc[] - === **4. Session 유효성 확인 API** -Session 유효성 확인 api입니다. +Session 유효성 확인 api 입니다. ==== Request include::{snippetsDir}/sessionValidityCheck/1/http-request.adoc[] diff --git a/src/main/java/com/ftm/server/adapter/in/web/auth/controller/LogoutController.java b/src/main/java/com/ftm/server/adapter/in/web/auth/controller/LogoutController.java index ebb3808..f6e3c8f 100644 --- a/src/main/java/com/ftm/server/adapter/in/web/auth/controller/LogoutController.java +++ b/src/main/java/com/ftm/server/adapter/in/web/auth/controller/LogoutController.java @@ -1,8 +1,10 @@ package com.ftm.server.adapter.in.web.auth.controller; +import com.ftm.server.application.port.in.auth.LogoutUseCase; import com.ftm.server.common.response.ApiResponse; import com.ftm.server.common.response.enums.SuccessResponseCode; import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpSession; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; @@ -14,13 +16,19 @@ @RequiredArgsConstructor public class LogoutController { + private final LogoutUseCase logoutUseCase; + @PostMapping("/api/auth/logout") - public ResponseEntity> logout(HttpServletRequest req) { + public ResponseEntity> logout( + HttpServletRequest req, HttpServletResponse res) { HttpSession session = req.getSession(false); if (session != null) { session.invalidate(); } + // 클라이언트 세션 쿠키 초기화 + res.addCookie(logoutUseCase.execute()); + return ResponseEntity.status(HttpStatus.OK) .body(ApiResponse.success(SuccessResponseCode.OK)); } diff --git a/src/main/java/com/ftm/server/application/port/in/auth/LogoutUseCase.java b/src/main/java/com/ftm/server/application/port/in/auth/LogoutUseCase.java new file mode 100644 index 0000000..ee23274 --- /dev/null +++ b/src/main/java/com/ftm/server/application/port/in/auth/LogoutUseCase.java @@ -0,0 +1,10 @@ +package com.ftm.server.application.port.in.auth; + +import com.ftm.server.common.annotation.UseCase; +import jakarta.servlet.http.Cookie; + +@UseCase +public interface LogoutUseCase { + + Cookie execute(); +} diff --git a/src/main/java/com/ftm/server/application/service/auth/LogoutService.java b/src/main/java/com/ftm/server/application/service/auth/LogoutService.java new file mode 100644 index 0000000..688f19d --- /dev/null +++ b/src/main/java/com/ftm/server/application/service/auth/LogoutService.java @@ -0,0 +1,20 @@ +package com.ftm.server.application.service.auth; + +import static com.ftm.server.common.consts.StaticConsts.CLIENT_SESSION_COOKIE_NAME; + +import com.ftm.server.application.port.in.auth.LogoutUseCase; +import com.ftm.server.common.utils.CookieUtils; +import jakarta.servlet.http.Cookie; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class LogoutService implements LogoutUseCase { + + @Override + public Cookie execute() { + // 클라이언트 세션 쿠키 초기화 + return CookieUtils.invalidateCookie(CLIENT_SESSION_COOKIE_NAME); + } +} diff --git a/src/main/java/com/ftm/server/common/consts/StaticConsts.java b/src/main/java/com/ftm/server/common/consts/StaticConsts.java index bf9a7e7..31ce48c 100644 --- a/src/main/java/com/ftm/server/common/consts/StaticConsts.java +++ b/src/main/java/com/ftm/server/common/consts/StaticConsts.java @@ -10,6 +10,7 @@ public class StaticConsts { public static final int MINIMUM_DAYS_BETWEEN_TESTS = 7; public static final String AUTHORIZATION_HEADER_PREFIX = "Bearer "; public static final String AUTHORIZATION_GRANT_TYPE = "authorization_code"; + public static final String CLIENT_SESSION_COOKIE_NAME = "SESSION"; public static final String PENDING_SOCIAL_USER_SESSION_KEY = "PENDING_SOCIAL_USER_INFO"; public static final int PENDING_SOCIAL_USER_SESSION_TTL = 300; // 5분 public static final String GROOMING_TESTS_INFO_CACHE_NAME = "ftm:grooming:tests:info"; diff --git a/src/main/java/com/ftm/server/common/utils/CookieUtils.java b/src/main/java/com/ftm/server/common/utils/CookieUtils.java new file mode 100644 index 0000000..89a4e6b --- /dev/null +++ b/src/main/java/com/ftm/server/common/utils/CookieUtils.java @@ -0,0 +1,16 @@ +package com.ftm.server.common.utils; + +import jakarta.servlet.http.Cookie; + +public class CookieUtils { + + public static Cookie invalidateCookie(String name) { + Cookie cookie = new Cookie(name, null); + cookie.setPath("/"); + cookie.setMaxAge(0); + cookie.setHttpOnly(true); + cookie.setSecure(true); + + return cookie; + } +} diff --git a/src/main/java/com/ftm/server/infrastructure/security/SecurityConfig.java b/src/main/java/com/ftm/server/infrastructure/security/SecurityConfig.java index e8a2c47..8e6a081 100644 --- a/src/main/java/com/ftm/server/infrastructure/security/SecurityConfig.java +++ b/src/main/java/com/ftm/server/infrastructure/security/SecurityConfig.java @@ -63,6 +63,7 @@ public class SecurityConfig { "/api/users/email/authentication", "/api/users/email/authentication/code", "/api/auth/login/**", + "api/auth/logout", "/api/users", "/api/users/social", "/api/grooming/tests/submission", diff --git a/src/test/java/com/ftm/server/auth/LogoutTest.java b/src/test/java/com/ftm/server/auth/LogoutTest.java index 89f4c33..9543c72 100644 --- a/src/test/java/com/ftm/server/auth/LogoutTest.java +++ b/src/test/java/com/ftm/server/auth/LogoutTest.java @@ -1,7 +1,6 @@ package com.ftm.server.auth; import static com.epages.restdocs.apispec.ResourceDocumentation.resource; -import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; @@ -10,28 +9,20 @@ import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.epages.restdocs.apispec.ResourceSnippetParameters; import com.ftm.server.BaseTest; -import com.ftm.server.adapter.in.web.auth.dto.request.GeneralLoginRequest; -import com.ftm.server.application.command.user.GeneralUserCreationCommand; import com.ftm.server.application.port.out.persistence.user.SaveUserImagePort; import com.ftm.server.application.port.out.persistence.user.SaveUserPort; import com.ftm.server.application.port.out.security.SecurityAuthenticationPort; -import com.ftm.server.common.response.enums.ErrorResponseCode; -import com.ftm.server.domain.entity.User; -import com.ftm.server.domain.entity.UserImage; import java.util.List; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.mock.web.MockHttpSession; import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; import org.springframework.restdocs.payload.FieldDescriptor; -import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultActions; import org.springframework.transaction.annotation.Transactional; @@ -48,6 +39,11 @@ public class LogoutTest extends BaseTest { fieldWithPath("message").type(STRING).description("메시지"), fieldWithPath("data").type(OBJECT).optional().description("data")); + private ResultActions getResultActions(MockHttpSession session) throws Exception { + return mockMvc.perform( + RestDocumentationRequestBuilders.post("/api/auth/logout").session(session)); + } + private RestDocumentationResultHandler getDocument(Integer identifier) { return document( "logout/" + identifier, @@ -67,42 +63,14 @@ private RestDocumentationResultHandler getDocument(Integer identifier) { .build())); } - @BeforeEach - void setUp() { - GeneralUserCreationCommand command = - new GeneralUserCreationCommand( - "test@gmail.com", - securityAuthenticationPort.passwordEncode("test1234!"), - "test", - null, - null); - User testUser = saveUserPort.saveUser(User.createGeneralUser(command)); - saveUserImagePort.saveUserDefaultImage(UserImage.createUserImage(testUser.getId())); - } - @Test @Transactional void 로그아웃_성공() throws Exception { // given - GeneralLoginRequest request = new GeneralLoginRequest("test@gmail.com", "test1234!"); + MockHttpSession session = createUserAndLogin(); // when - MvcResult loginResult = - mockMvc.perform( - RestDocumentationRequestBuilders.post("/api/auth/login") - .contentType(APPLICATION_JSON_VALUE) - .content(mapper.writeValueAsString(request))) - .andExpect(status().isOk()) - .andReturn(); - MockHttpSession session = (MockHttpSession) loginResult.getRequest().getSession(false); - - ResultActions resultActions = - mockMvc.perform( - RestDocumentationRequestBuilders.post("/api/auth/logout").session(session)); - - // 요청 헤더에 세션 쿠키 수동 추가 (문서화 통과용) - MvcResult result = resultActions.andReturn(); - result.getRequest().addHeader("Cookie", "SESSION=mock-session-id; Path=/; HttpOnly"); + ResultActions resultActions = getResultActions(session); // then resultActions.andExpect(status().isOk()).andDo(print()); @@ -110,21 +78,4 @@ void setUp() { // documentation resultActions.andDo(getDocument(1)); } - - @Test - @Transactional - void 로그아웃_실패() throws Exception { - // when - ResultActions resultActions = - mockMvc.perform(RestDocumentationRequestBuilders.post("/api/auth/logout")); - - // then - resultActions - .andExpect(status().is(ErrorResponseCode.NOT_AUTHENTICATED.getHttpStatus().value())) - .andExpect(jsonPath("code").value(ErrorResponseCode.NOT_AUTHENTICATED.getCode())) - .andDo(print()); - - // documentation - resultActions.andDo(getDocument(2)); - } }