Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -40,11 +41,11 @@ public ResponseEntity<RsData<UserResponse>> register(

// 로그인
@PostMapping("/login")
public ResponseEntity<RsData<UserResponse>> login(
public ResponseEntity<RsData<LoginResponse>> login(
@Valid @RequestBody LoginRequest request,
HttpServletResponse response
) {
UserResponse loginResponse = userService.login(request, response);
LoginResponse loginResponse = userService.login(request, response);
return ResponseEntity
.ok(RsData.success(
"로그인에 성공했습니다.",
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -130,8 +131,7 @@ ResponseEntity<RsData<UserResponse>> register(

@Operation(
summary = "로그인",
description = "username + password로 로그인합니다. " +
"로그인 성공 시 Access Token은 `Authorization` 헤더에, Refresh Token은 HttpOnly 쿠키로 발급됩니다."
description = "username + password로 로그인합니다. "
)
@ApiResponses({
@ApiResponse(
Expand All @@ -145,13 +145,16 @@ ResponseEntity<RsData<UserResponse>> register(
"code": "SUCCESS_200",
"message": "로그인에 성공했습니다.",
"data": {
"userId": 1,
"username": "testuser",
"email": "[email protected]",
"nickname": "홍길동",
"role": "USER",
"status": "ACTIVE",
"createdAt": "2025-09-19T15:00:00"
"accessToken": "{accessToken}",
"user": {
"userId": 1,
"username": "testuser",
"email": "[email protected]",
"nickname": "홍길동",
"role": "USER",
"status": "ACTIVE",
"createdAt": "2025-09-19T15:00:00"
}
}
}
""")
Expand Down Expand Up @@ -228,7 +231,7 @@ ResponseEntity<RsData<UserResponse>> register(
)
)
})
ResponseEntity<RsData<UserResponse>> login(
ResponseEntity<RsData<LoginResponse>> login(
@Valid @RequestBody LoginRequest request,
HttpServletResponse response
);
Expand Down
6 changes: 6 additions & 0 deletions src/main/java/com/back/domain/user/dto/LoginResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.back.domain.user.dto;

public record LoginResponse(
String accessToken,
UserResponse user
) {}
15 changes: 8 additions & 7 deletions src/main/java/com/back/domain/user/service/UserService.java
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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())
);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.*;
Expand Down Expand Up @@ -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!";
Expand Down Expand Up @@ -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"));
}

Expand Down Expand Up @@ -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)))
Expand All @@ -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);
}

Expand Down
13 changes: 8 additions & 5 deletions src/test/java/com/back/domain/user/service/UserServiceTest.java
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -332,8 +335,8 @@ void refreshToken_success() throws InterruptedException {
User user = setupUser("refreshuser", "[email protected]", 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();

Expand Down