diff --git a/src/main/java/com/back/domain/user/controller/AuthController.java b/src/main/java/com/back/domain/user/controller/AuthController.java index 2323366c..7f0e49c9 100644 --- a/src/main/java/com/back/domain/user/controller/AuthController.java +++ b/src/main/java/com/back/domain/user/controller/AuthController.java @@ -1,6 +1,7 @@ package com.back.domain.user.controller; import com.back.domain.user.dto.LoginRequest; +import com.back.domain.user.dto.LoginResponse; import com.back.domain.user.dto.UserRegisterRequest; import com.back.domain.user.dto.UserResponse; import com.back.domain.user.service.UserService; @@ -40,11 +41,11 @@ public ResponseEntity> register( // 로그인 @PostMapping("/login") - public ResponseEntity> login( + public ResponseEntity> login( @Valid @RequestBody LoginRequest request, HttpServletResponse response ) { - UserResponse loginResponse = userService.login(request, response); + LoginResponse loginResponse = userService.login(request, response); return ResponseEntity .ok(RsData.success( "로그인에 성공했습니다.", diff --git a/src/main/java/com/back/domain/user/controller/AuthControllerDocs.java b/src/main/java/com/back/domain/user/controller/AuthControllerDocs.java index dbcd5423..ed441a5a 100644 --- a/src/main/java/com/back/domain/user/controller/AuthControllerDocs.java +++ b/src/main/java/com/back/domain/user/controller/AuthControllerDocs.java @@ -1,6 +1,7 @@ package com.back.domain.user.controller; import com.back.domain.user.dto.LoginRequest; +import com.back.domain.user.dto.LoginResponse; import com.back.domain.user.dto.UserRegisterRequest; import com.back.domain.user.dto.UserResponse; import com.back.global.common.dto.RsData; @@ -130,8 +131,7 @@ ResponseEntity> register( @Operation( summary = "로그인", - description = "username + password로 로그인합니다. " + - "로그인 성공 시 Access Token은 `Authorization` 헤더에, Refresh Token은 HttpOnly 쿠키로 발급됩니다." + description = "username + password로 로그인합니다. " ) @ApiResponses({ @ApiResponse( @@ -145,13 +145,16 @@ ResponseEntity> register( "code": "SUCCESS_200", "message": "로그인에 성공했습니다.", "data": { - "userId": 1, - "username": "testuser", - "email": "test@example.com", - "nickname": "홍길동", - "role": "USER", - "status": "ACTIVE", - "createdAt": "2025-09-19T15:00:00" + "accessToken": "{accessToken}", + "user": { + "userId": 1, + "username": "testuser", + "email": "test@example.com", + "nickname": "홍길동", + "role": "USER", + "status": "ACTIVE", + "createdAt": "2025-09-19T15:00:00" + } } } """) @@ -228,7 +231,7 @@ ResponseEntity> register( ) ) }) - ResponseEntity> login( + ResponseEntity> login( @Valid @RequestBody LoginRequest request, HttpServletResponse response ); diff --git a/src/main/java/com/back/domain/user/dto/LoginResponse.java b/src/main/java/com/back/domain/user/dto/LoginResponse.java new file mode 100644 index 00000000..a92b45e7 --- /dev/null +++ b/src/main/java/com/back/domain/user/dto/LoginResponse.java @@ -0,0 +1,6 @@ +package com.back.domain.user.dto; + +public record LoginResponse( + String accessToken, + UserResponse user +) {} diff --git a/src/main/java/com/back/domain/user/service/UserService.java b/src/main/java/com/back/domain/user/service/UserService.java index bfc6dd93..7a9dc80e 100644 --- a/src/main/java/com/back/domain/user/service/UserService.java +++ b/src/main/java/com/back/domain/user/service/UserService.java @@ -1,6 +1,7 @@ package com.back.domain.user.service; import com.back.domain.user.dto.LoginRequest; +import com.back.domain.user.dto.LoginResponse; import com.back.domain.user.dto.UserRegisterRequest; import com.back.domain.user.dto.UserResponse; import com.back.domain.user.entity.User; @@ -87,9 +88,9 @@ public UserResponse register(UserRegisterRequest request) { * 2. 사용자 상태 검증 (PENDING, SUSPENDED, DELETED) * 3. Access/Refresh Token 발급 * 4. Refresh Token을 HttpOnly 쿠키로, Access Token은 헤더로 설정 - * 5. UserResponse 반환 + * 5. LoginResponse 반환 */ - public UserResponse login(LoginRequest request, HttpServletResponse response) { + public LoginResponse login(LoginRequest request, HttpServletResponse response) { // 사용자 조회 User user = userRepository.findByUsername(request.username()) .orElseThrow(() -> new CustomException(ErrorCode.INVALID_CREDENTIALS)); @@ -127,11 +128,11 @@ public UserResponse login(LoginRequest request, HttpServletResponse response) { "/api/auth" ); - // Access Token을 응답 헤더에 설정 - response.setHeader("Authorization", "Bearer " + accessToken); - - // UserResponse 반환 - return UserResponse.from(user, user.getUserProfile()); + // LoginResponse 반환 + return new LoginResponse( + accessToken, + UserResponse.from(user, user.getUserProfile()) + ); } /** diff --git a/src/test/java/com/back/domain/user/controller/AuthControllerTest.java b/src/test/java/com/back/domain/user/controller/AuthControllerTest.java index 3961d6be..d75af6c9 100644 --- a/src/test/java/com/back/domain/user/controller/AuthControllerTest.java +++ b/src/test/java/com/back/domain/user/controller/AuthControllerTest.java @@ -5,8 +5,6 @@ import com.back.domain.user.entity.UserStatus; import com.back.domain.user.repository.UserRepository; import com.back.fixture.TestJwtTokenProvider; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.SignatureAlgorithm; import jakarta.servlet.http.Cookie; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -20,9 +18,6 @@ import org.springframework.test.web.servlet.ResultActions; import org.springframework.transaction.annotation.Transactional; -import java.util.Date; - -import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @@ -202,7 +197,7 @@ void register_invalidRequest_missingField() throws Exception { } @Test - @DisplayName("정상 로그인 → 200 OK + Authorization 헤더 + refreshToken 쿠키") + @DisplayName("정상 로그인 → 200 OK + accessToken + refreshToken 쿠키") void login_success() throws Exception { // given: 회원가입 요청으로 DB에 정상 유저 저장 String rawPassword = "P@ssw0rd!"; @@ -233,12 +228,12 @@ void login_success() throws Exception { .content(loginBody)) .andDo(print()); - // then: 200 OK 응답 + username/Authorization 헤더/refreshToken 쿠키 확인 + // then: 200 OK 응답 + username/accessToken/refreshToken 쿠키 확인 resultActions .andExpect(status().isOk()) .andExpect(jsonPath("$.success").value(true)) - .andExpect(jsonPath("$.data.username").value("loginuser")) - .andExpect(header().exists("Authorization")) + .andExpect(jsonPath("$.data.user.username").value("loginuser")) + .andExpect(jsonPath("$.data.accessToken").exists()) .andExpect(cookie().exists("refreshToken")); } @@ -484,16 +479,13 @@ void refreshToken_success() throws Exception { // 기존 AccessToken, RefreshToken 확보 String oldAccessToken = loginResult.andReturn() .getResponse() - .getHeader("Authorization") - .substring(7); // "Bearer " 제거 + .getContentAsString(); // body에서 accessToken 꺼낼 수 있도록 JSON 파싱 필요 + String refreshCookie = loginResult.andReturn() .getResponse() .getCookie("refreshToken") .getValue(); - // Issued At(발급 시간) 분리를 위해 1초 대기 -// Thread.sleep(1000); - // when: 재발급 요청 (RefreshToken 쿠키 포함) ResultActions refreshResult = mvc.perform(post("/api/auth/refresh") .cookie(new Cookie("refreshToken", refreshCookie))) @@ -503,15 +495,11 @@ void refreshToken_success() throws Exception { refreshResult .andExpect(status().isOk()) .andExpect(jsonPath("$.success").value(true)) - .andExpect(jsonPath("$.data.accessToken").exists()) - .andExpect(header().exists("Authorization")); - - String newAccessToken = refreshResult.andReturn() - .getResponse() - .getHeader("Authorization") - .substring(7); + .andExpect(jsonPath("$.data.accessToken").exists()); - // 새 토큰은 기존 토큰과 달라야 함 + // 새 토큰은 기존 토큰과 달라야 함 (파싱 후 비교) +// String newAccessToken = JsonPath.read(refreshResult.andReturn() +// .getResponse().getContentAsString(), "$.data.accessToken"); // assertThat(newAccessToken).isNotEqualTo(oldAccessToken); } diff --git a/src/test/java/com/back/domain/user/service/UserServiceTest.java b/src/test/java/com/back/domain/user/service/UserServiceTest.java index 7ee8d18f..a35ee38a 100644 --- a/src/test/java/com/back/domain/user/service/UserServiceTest.java +++ b/src/test/java/com/back/domain/user/service/UserServiceTest.java @@ -1,6 +1,7 @@ package com.back.domain.user.service; import com.back.domain.user.dto.LoginRequest; +import com.back.domain.user.dto.LoginResponse; import com.back.domain.user.dto.UserRegisterRequest; import com.back.domain.user.dto.UserResponse; import com.back.domain.user.entity.User; @@ -184,17 +185,19 @@ void login_success() { MockHttpServletResponse response = new MockHttpServletResponse(); // when: 로그인 요청 실행 - UserResponse userResponse = userService.login( + LoginResponse loginResponse = userService.login( new LoginRequest("loginuser", rawPassword), response); // then: 응답에 username과 토큰/쿠키가 포함됨 - assertThat(userResponse.username()).isEqualTo("loginuser"); - assertThat(response.getHeader("Authorization")).startsWith("Bearer "); + assertThat(loginResponse.user().username()).isEqualTo("loginuser"); + assertThat(loginResponse.accessToken()).isNotBlank(); + Cookie refreshCookie = response.getCookie("refreshToken"); assertThat(refreshCookie).isNotNull(); assertThat(refreshCookie.isHttpOnly()).isTrue(); } + @Test @DisplayName("잘못된 비밀번호 → INVALID_CREDENTIALS 예외 발생") void login_invalidPassword() { @@ -332,8 +335,8 @@ void refreshToken_success() throws InterruptedException { User user = setupUser("refreshuser", "refresh@example.com", rawPassword, "닉네임", UserStatus.ACTIVE); MockHttpServletResponse loginResponse = new MockHttpServletResponse(); - userService.login(new LoginRequest("refreshuser", rawPassword), loginResponse); - String oldAccessToken = loginResponse.getHeader("Authorization").substring(7); + LoginResponse loginRes = userService.login(new LoginRequest("refreshuser", rawPassword), loginResponse); + String oldAccessToken = loginRes.accessToken(); Cookie refreshCookie = loginResponse.getCookie("refreshToken"); assertThat(refreshCookie).isNotNull();