Skip to content
Open
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
29 changes: 29 additions & 0 deletions src/main/java/com/server/auth/config/AuthCookieProperties.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.server.auth.config;

import jakarta.annotation.PostConstruct;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Slf4j
@Getter
@Setter
@Component
@ConfigurationProperties(prefix = "app.cookie.auth")
public class AuthCookieProperties {

private boolean secure;
private String sameSite;
private String path;
private long accessMaxAgeSeconds;
private long refreshMaxAgeSeconds;

@PostConstruct // 앱 뜰 때 딱 1번 찍힘 (등록+바인딩 검증)
public void logLoadedValues() {
log.info("[AuthCookieProperties] loaded. secure={}, sameSite={}, path={}, accessMaxAgeSeconds={}, refreshMaxAgeSeconds={}",
secure, sameSite, path, accessMaxAgeSeconds, refreshMaxAgeSeconds);
}
}

119 changes: 66 additions & 53 deletions src/main/java/com/server/auth/controller/AuthController.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.server.auth.controller;


import com.server.auth.config.AuthCookieProperties;
import com.server.auth.dto.*;
import com.server.auth.service.AuthService;
import com.server.auth.service.PasswordResetService;
Expand Down Expand Up @@ -28,6 +30,8 @@ public class AuthController {
private final AuthService authService;
private final PasswordResetService passwordResetService;

private final AuthCookieProperties authCookieProperties;

// 비밀번호 재설정 이메일 발송

@Operation(
Expand Down Expand Up @@ -58,54 +62,46 @@ public CommonResponse<PasswordResetResponseDto> confirmPasswordReset(
}


//로그인
@Operation(
summary = "로그인 및 토큰 발급")
//로그인
@Operation(
summary = "로그인 및 토큰 발급")
@PostMapping("/token")
public CommonResponse<UserLoginResponseDto> login(
@Valid @RequestBody UserLoginRequestDto request,
HttpServletResponse httpServletResponse
) {
UserLoginResponseDto response = authService.login(
request.email(),
request.password()
);

// Access Token 쿠키
ResponseCookie accessCookie = ResponseCookie.from("accessToken", response.getAccessToken())
.httpOnly(true)
.secure(true)
.sameSite("None")
.path("/")
.maxAge(60 * 60) // 1시간
.build();

// Refresh Token 쿠키
ResponseCookie refreshCookie = ResponseCookie.from("refreshToken", response.getRefreshToken())
.httpOnly(true)
.secure(true)
.sameSite("None")
.path("/")
.maxAge(60L * 60 * 24 * 7) // 7일
.build();
UserLoginResponseDto issued = authService.login(request.email(), request.password());

httpServletResponse.addHeader("Set-Cookie", accessCookie.toString());
httpServletResponse.addHeader("Set-Cookie", refreshCookie.toString());
// 쿠키 세팅 로직
setAuthCookies(httpServletResponse, issued.getAccessToken(), issued.getRefreshToken());

return CommonResponse.success(response);
// refreshToken 바디 제거
UserLoginResponseDto body = UserLoginResponseDto.of(issued.getAccessToken(), null);
return CommonResponse.success(body);
}
@Operation(
summary = "Access Token 재발급")
@PostMapping("/token/refresh")
public CommonResponse<UserLoginResponseDto> refresh(
@Valid @RequestBody TokenRefreshRequestDto request
// 쿠키 기반으로 바꾸기 위해 Request/Response를 받음
HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse
) {
UserLoginResponseDto response = authService.reissueAccessToken(
request.refreshToken()
);
return CommonResponse.success(response);
// refreshToken을 요청 바디가 아니라 쿠키에서 꺼냄
String refreshToken = extractCookie(httpServletRequest, "refreshToken");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요 부분 refreshToken이 null 이여도 호출되지 않나요?? 만약 그런거라면 컨트롤러 혹은 서비스 로직에서 관련 예외처리가 필요할 것 같아요

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

감사합니다 수정해보겠습니다~


// 쿠키에서 꺼낸 refreshToken으로 재발급
UserLoginResponseDto reissued = authService.reissueAccessToken(refreshToken);

// 쿠키 세팅 로직
setAuthCookies(httpServletResponse, reissued.getAccessToken(), reissued.getRefreshToken());

// refreshToken 바디 제거
UserLoginResponseDto body = UserLoginResponseDto.of(reissued.getAccessToken(), null);
return CommonResponse.success(body);
}


//로그아웃
@Operation(
summary = "로그아웃")
Expand All @@ -120,32 +116,49 @@ public CommonResponse<String> logout(
// 실제 쿠키 삭제 등의 로그아웃 처리 로직은 서비스에 위임
authService.logout(refreshToken);

// 쿠키 삭제 (accessToken / refreshToken 둘 다 만료시킴)
ResponseCookie clearAccess = ResponseCookie.from("accessToken", "")
.httpOnly(true)
.secure(true)
.sameSite("None")
.path("/")
.maxAge(0)// 즉시 만료
.build();
// 쿠키 삭제 로직 통일
clearAuthCookies(response);

ResponseCookie clearRefresh = ResponseCookie.from("refreshToken", "")
.httpOnly(true)
.secure(true)
.sameSite("None")
.path("/")
.maxAge(0)
.build();
// 로그아웃은 별도의 데이터가 필요 없으므로 data = null로 응답
return CommonResponse.success("로그아웃 되었습니다.");
}

response.addHeader("Set-Cookie", clearAccess.toString());
response.addHeader("Set-Cookie", clearRefresh.toString());
// 헬퍼 메서드: 인증 쿠키 처리 공통화(중복 제거)

private void setAuthCookies(HttpServletResponse response, String accessToken, String refreshToken) {
ResponseCookie accessCookie = buildCookie(
"accessToken",
accessToken,
authCookieProperties.getAccessMaxAgeSeconds()
);

// 로그아웃은 별도의 데이터가 필요 없으므로 data = null로 응답
return CommonResponse.success("로그아웃 되었습니다.");
ResponseCookie refreshCookie = buildCookie(
"refreshToken",
refreshToken,
authCookieProperties.getRefreshMaxAgeSeconds()
);

response.addHeader("Set-Cookie", accessCookie.toString());
response.addHeader("Set-Cookie", refreshCookie.toString());
}

private void clearAuthCookies(HttpServletResponse response) {
ResponseCookie clearAccess = buildCookie("accessToken", "", 0);
ResponseCookie clearRefresh = buildCookie("refreshToken", "", 0);

response.addHeader("Set-Cookie", clearAccess.toString());
response.addHeader("Set-Cookie", clearRefresh.toString());
}

// 요청 쿠키에서 name 에 해당하는 쿠키 값을 꺼내는 헬퍼 메서드
private ResponseCookie buildCookie(String name, String value, long maxAgeSeconds) {
return ResponseCookie.from(name, value)
.httpOnly(true)
.secure(authCookieProperties.isSecure())
.sameSite(authCookieProperties.getSameSite())
.path(authCookieProperties.getPath())
.maxAge(maxAgeSeconds)
.build();
}

private String extractCookie(HttpServletRequest request, String name) {
Cookie[] cookies = request.getCookies();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.server.user.domain;
package com.server.auth.domain;

import com.server.global.entity.BaseEntity;
import com.server.user.domain.User;
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Getter;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
package com.server.auth.repository;

import com.server.user.domain.PasswordResetToken;
import org.springframework.data.domain.Pageable;
import com.server.auth.domain.PasswordResetToken;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;

// 비밀번호 재설정 토큰 저장소
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import com.server.auth.exception.AuthErrorCase;
import com.server.auth.repository.PasswordResetTokenRepository;
import com.server.global.exception.ApplicationException;
import com.server.user.domain.PasswordResetToken;
import com.server.auth.domain.PasswordResetToken;
import com.server.user.domain.User;
import com.server.user.repository.UserRepository;
import com.server.user.util.PasswordValidator;
Expand All @@ -15,7 +15,6 @@
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.domain.PageRequest;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.security.crypto.password.PasswordEncoder;
Expand All @@ -29,7 +28,6 @@
import java.time.LocalDateTime;
import java.time.Year;
import java.util.Base64;
import java.util.List;

@Slf4j
@Service
Expand Down
30 changes: 26 additions & 4 deletions src/test/java/com/server/auth/controller/AuthControllerTest.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.server.auth.controller;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.server.auth.config.AuthCookieProperties;
import com.server.auth.dto.PasswordResetConfirmRequestDto;
import com.server.auth.dto.PasswordResetRequestDto;
import com.server.auth.dto.PasswordResetResponseDto;
Expand All @@ -9,22 +10,26 @@
import com.server.auth.service.PasswordResetService;
import com.server.user.dto.UserLoginRequestDto;
import com.server.user.dto.UserLoginResponseDto;
import jakarta.servlet.http.Cookie;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;

import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.hasItem;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@WebMvcTest(AuthController.class)
@AutoConfigureMockMvc(addFilters = false)
Expand All @@ -42,6 +47,20 @@ class AuthControllerTest {
@MockitoBean
PasswordResetService passwordResetService;

@MockitoBean
AuthCookieProperties authCookieProperties;

@BeforeEach
void setUp() {
//AuthCookieProperties mock 값 세팅
when(authCookieProperties.isSecure()).thenReturn(false);
when(authCookieProperties.getSameSite()).thenReturn("Lax");
when(authCookieProperties.getPath()).thenReturn("/");
when(authCookieProperties.getAccessMaxAgeSeconds()).thenReturn(3600L);
when(authCookieProperties.getRefreshMaxAgeSeconds()).thenReturn(604800L);

}

@Test
@DisplayName("로그인 성공")
void login_success() throws Exception {
Expand All @@ -65,7 +84,8 @@ void login_success() throws Exception {
)
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.accessToken").value("access-token"))
.andExpect(jsonPath("$.data.refreshToken").value("refresh-token"));
.andExpect(header().stringValues(HttpHeaders.SET_COOKIE,
hasItem(containsString("refreshToken="))));
}

@Test
Expand All @@ -83,12 +103,14 @@ void refresh_success() throws Exception {

mockMvc.perform(
post("/api/v1/auth/token/refresh")
.cookie(new Cookie("refreshToken", "old-refresh-token"))
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(requestDto))
)
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.accessToken").value("new-access-token"))
.andExpect(jsonPath("$.data.refreshToken").value("new-refresh-token"));
.andExpect(header().stringValues(HttpHeaders.SET_COOKIE,
hasItem(containsString("refreshToken="))));
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import com.server.auth.exception.AuthErrorCase;
import com.server.auth.repository.PasswordResetTokenRepository;
import com.server.global.exception.ApplicationException;
import com.server.user.domain.PasswordResetToken;
import com.server.auth.domain.PasswordResetToken;
import com.server.user.domain.User;
import com.server.user.repository.UserRepository;
import com.server.user.util.PasswordValidator;
Expand Down