diff --git a/src/main/java/com/deardream/deardream_be/domain/auth/controller/AuthController.java b/src/main/java/com/deardream/deardream_be/domain/auth/controller/AuthController.java index 64cfbda..7bd6747 100644 --- a/src/main/java/com/deardream/deardream_be/domain/auth/controller/AuthController.java +++ b/src/main/java/com/deardream/deardream_be/domain/auth/controller/AuthController.java @@ -6,7 +6,9 @@ import com.deardream.deardream_be.global.apiPayload.ApiResponse; import com.deardream.deardream_be.global.apiPayload.code.status.ErrorStatus; import com.deardream.deardream_be.global.apiPayload.exception.GeneralException; +import com.deardream.deardream_be.domain.cookie.CookieService; import io.swagger.v3.oas.annotations.Operation; +import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.*; @@ -19,21 +21,34 @@ public class AuthController { private final AuthService authService; + private final CookieService cookieService; @Operation(summary = "인가코드와 redirectUri를 통해 카카오 서버로부터 정보를 내려받습니다.") @GetMapping("/login/kakao") - public ApiResponse kakaoLogin(@RequestParam("code") String code, @RequestParam(value = "redirectUri") String redirectUri, @RequestParam(value = "state", required = false) Long familyId) { - - log.info("Received redirectUri={}", redirectUri); + public ApiResponse kakaoLogin( + @RequestParam("code") String code, + @RequestParam(value = "redirectUri") String redirectUri, + Long familyId, + HttpServletResponse response) { if (!WhiteRedirectUriList.getAllowedRedirectUris().contains(redirectUri)) { boolean allowed = WhiteRedirectUriList.getAllowedRedirectUris().contains(redirectUri); log.info("Is redirectUri allowed? {}", allowed); - throw new GeneralException(ErrorStatus._INVALID_REDIRECT_URI); } KakaoLoginResponseDto loginResponseDto = authService.loginWithKakao(code, redirectUri, familyId); + + // 쿠키 세팅 로직 + // tempToken은 가입 전용, 가입된 유저가 아니면 임시 토큰만 세팅 + if (!loginResponseDto.isRegistered()) { + if (loginResponseDto.getTempToken() != null) { + cookieService.setTempTokenCookie(response, loginResponseDto.getTempToken()); + } + } else { // 가입된 유저면 accessToken, refreshToken 쿠키 세팅 + cookieService.setTokenCookies(response, loginResponseDto.getNewAccessToken(), loginResponseDto.getNewRefreshToken()); + } + return ApiResponse.onSuccess(loginResponseDto); } @@ -41,10 +56,17 @@ public ApiResponse kakaoLogin(@RequestParam("code") Strin @Operation(summary = "토큰을 재발급 받습니다.") @PostMapping("/reissue") - public ApiResponse reissueToken(@RequestHeader("Authorization") String token) { + public ApiResponse reissueToken( + @RequestHeader("Authorization") String token, + HttpServletResponse response + ) { try { String refreshToken = token.replace("Bearer ", "").trim(); KakaoLoginResponseDto responseDto = authService.reissueToken(refreshToken); + + // 리프레시 후에는 accessToken/refreshToken 쿠키 모두 세팅 + cookieService.setTokenCookies(response, responseDto.getNewAccessToken(), responseDto.getNewRefreshToken()); + return ApiResponse.onSuccess(responseDto); } catch (Exception e) { @@ -55,31 +77,38 @@ public ApiResponse reissueToken(@RequestHeader("Authoriza @Operation(summary = "일반 로그아웃 : 로그아웃 시 재로그인이 필요하지 않습니다.") @PostMapping("/logout") - public ApiResponse logout(@RequestHeader("Authorization") String token) { + public ApiResponse logout( + @RequestHeader("Authorization") String token, + HttpServletResponse response + ) { String accessToken = token.replace("Bearer ", ""); authService.logout(accessToken); + + // accessToken, refreshToken, tempToken 쿠키 모두 삭제 + cookieService.deleteTokenCookies(response); + cookieService.deleteTempTokenCookie(response); + return ApiResponse.onSuccess("로그아웃에 성공했습니다."); } @Operation(summary = "카카오 계정과 함께 로그아웃 : 로그아웃 시 재로그인이 필요합니다.") @GetMapping("/logout/kakao-account") - public ApiResponse logoutKakaoAccount(@RequestHeader(value = "Authorization") String token, HttpServletRequest request) { + public ApiResponse logoutKakaoAccount( + @RequestHeader(value = "Authorization") String token, + HttpServletRequest request, + HttpServletResponse response + ) { if (token != null && !token.isBlank()) { String accessToken = token.replace("Bearer ", ""); authService.logout(accessToken); + + // accessToken, refreshToken, tempToken 쿠키 모두 삭제 + cookieService.deleteTokenCookies(response); + cookieService.deleteTempTokenCookie(response); } String logoutRedirectUri = authService.logoutWithKakaoAccount(request); return ApiResponse.onSuccess(logoutRedirectUri); } -// @GetMapping("/logout/callback") -// public ApiResponse kakaoAccountLogoutCallback(@RequestHeader(value = "Authorization", required = false) String token) { -// if (token != null && !token.isBlank()) { -// String accessToken = token.replace("Bearer ", ""); -// authService.logout(accessToken); -// } -// return ApiResponse.onSuccess("카카오 계정 및 서비스 로그아웃 완료"); -// } - } diff --git a/src/main/java/com/deardream/deardream_be/domain/cookie/CookieService.java b/src/main/java/com/deardream/deardream_be/domain/cookie/CookieService.java new file mode 100644 index 0000000..78bffbb --- /dev/null +++ b/src/main/java/com/deardream/deardream_be/domain/cookie/CookieService.java @@ -0,0 +1,55 @@ +package com.deardream.deardream_be.domain.cookie; + +import com.deardream.deardream_be.domain.cookie.CookieUtil; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.stereotype.Service; + +@Service +public class CookieService { + + public static final long ACCESS_TOKEN_EXPIRES_IN = 60 * 60 * 2; + public static final long REFRESH_TOKEN_EXPIRES_IN = 60 * 60 * 24 * 2; + public static final long TEMP_TOKEN_EXPIRES_IN = 60 * 10; + + public void setAccessTokenCookie(HttpServletResponse response, String token) { + response.addHeader("Set-Cookie", CookieUtil.createAccessTokenCookie(token, ACCESS_TOKEN_EXPIRES_IN).toString()); + } + + + public void setRefreshTokenCookie(HttpServletResponse response, String token) { + response.addHeader("Set-Cookie", CookieUtil.createRefreshTokenCookie(token, REFRESH_TOKEN_EXPIRES_IN).toString()); + } + + public void setTempTokenCookie(HttpServletResponse response, String token) { + response.addHeader("Set-Cookie", CookieUtil.createTempTokenCookie(token, TEMP_TOKEN_EXPIRES_IN).toString()); + } + + // 토큰 삭제(만료) + public void deleteAccessTokenCookie(HttpServletResponse response) { + response.addHeader("Set-Cookie", CookieUtil.deleteAccessTokenCookie().toString()); + } + + public void deleteRefreshTokenCookie(HttpServletResponse response) { + response.addHeader("Set-Cookie", CookieUtil.deleteRefreshTokenCookie().toString()); + } + + public void deleteTempTokenCookie(HttpServletResponse response) { + response.addHeader("Set-Cookie", CookieUtil.deleteTempTokenCookie().toString()); + } + + public void setTokenCookies(HttpServletResponse response, String accessToken, String refreshToken) { + if (accessToken != null) { + setAccessTokenCookie(response, accessToken); + } + if (refreshToken != null) { + setRefreshTokenCookie(response, refreshToken); + } + } + + public void deleteTokenCookies(HttpServletResponse response) { + deleteAccessTokenCookie(response); + deleteRefreshTokenCookie(response); + } + + +} diff --git a/src/main/java/com/deardream/deardream_be/domain/cookie/CookieUtil.java b/src/main/java/com/deardream/deardream_be/domain/cookie/CookieUtil.java new file mode 100644 index 0000000..690c4a1 --- /dev/null +++ b/src/main/java/com/deardream/deardream_be/domain/cookie/CookieUtil.java @@ -0,0 +1,80 @@ +package com.deardream.deardream_be.domain.cookie; + +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.http.ResponseCookie; +import org.springframework.http.ResponseEntity; + +public class CookieUtil { + + public static final String ACCESS_TOKEN = "accessToken"; + public static final String REFRESH_TOKEN = "refreshToken"; + public static final String TEMP_TOKEN = "tempToken"; + + // 필요하다면 SameSite 값을 "Strict"나 "Lax", "None" 등으로 변경 + private static final String SAME_SITE = "Strict"; + private static final String PATH = "/"; + + // 액세스 토큰 세팅 + public static ResponseCookie createAccessTokenCookie(String token, long maxAgeSeconds) { + return ResponseCookie.from(ACCESS_TOKEN, token) + .httpOnly(true) + .secure(true) + .path(PATH) + .maxAge(maxAgeSeconds) + .sameSite(SAME_SITE) + .build(); + } + + // 리프레시 토큰 세팅 + public static ResponseCookie createRefreshTokenCookie(String token, long maxAgeSeconds) { + return ResponseCookie.from(REFRESH_TOKEN, token) + .httpOnly(true) + .secure(true) + .path(PATH) + .maxAge(maxAgeSeconds) + .sameSite(SAME_SITE) + .build(); + } + + public static ResponseCookie createTempTokenCookie(String token, long maxAgeSeconds) { + return ResponseCookie.from(TEMP_TOKEN, token) + .httpOnly(true) + .secure(true) + .path(PATH) + .maxAge(maxAgeSeconds) + .sameSite(SAME_SITE) + .build(); + } + + // 액세스 토큰 쿠키 삭제 (로그아웃 등) + public static ResponseCookie deleteAccessTokenCookie() { + return ResponseCookie.from(ACCESS_TOKEN, "") + .httpOnly(true) + .secure(true) + .path(PATH) + .maxAge(0) + .sameSite(SAME_SITE) + .build(); + } + + // 리프레시 토큰 쿠키 삭제 + public static ResponseCookie deleteRefreshTokenCookie() { + return ResponseCookie.from(REFRESH_TOKEN, "") + .httpOnly(true) + .secure(true) + .path(PATH) + .maxAge(0) + .sameSite(SAME_SITE) + .build(); + } + + public static ResponseCookie deleteTempTokenCookie() { + return ResponseCookie.from(TEMP_TOKEN, "") + .httpOnly(true) + .secure(true) + .path(PATH) + .maxAge(0) + .sameSite(SAME_SITE) + .build(); + } +} diff --git a/src/main/java/com/deardream/deardream_be/domain/user/controller/UserController.java b/src/main/java/com/deardream/deardream_be/domain/user/controller/UserController.java index b5bec0d..13dfe31 100644 --- a/src/main/java/com/deardream/deardream_be/domain/user/controller/UserController.java +++ b/src/main/java/com/deardream/deardream_be/domain/user/controller/UserController.java @@ -1,6 +1,8 @@ package com.deardream.deardream_be.domain.user.controller; import com.deardream.deardream_be.domain.auth.service.AuthService; +import com.deardream.deardream_be.domain.cookie.CookieService; +import com.deardream.deardream_be.domain.cookie.CookieUtil; import com.deardream.deardream_be.domain.jwt.CustomUserDetails; import com.deardream.deardream_be.domain.jwt.JwtUtil; import com.deardream.deardream_be.domain.user.dto.RegisterResponseDto; @@ -12,6 +14,7 @@ import com.deardream.deardream_be.global.apiPayload.code.status.ErrorStatus; import com.deardream.deardream_be.global.apiPayload.code.status.SuccessStatus; import com.deardream.deardream_be.global.apiPayload.exception.GeneralException; +import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.Value; @@ -34,6 +37,7 @@ public class UserController { private final UserServiceWithdraw userServiceWithdraw; private final AuthService authService; private final JwtUtil jwtUtil; + private final CookieService cookieService; /** @@ -46,7 +50,8 @@ public ApiResponse registerUser( @RequestParam(value = "code", required = false) String inviteCode, @RequestHeader("Authorization") String authorization, @RequestPart("userRequestDto") @Valid UserRequestDto userRequestDto, - @RequestPart(value = "profileImage", required = false) MultipartFile profileImage + @RequestPart(value = "profileImage", required = false) MultipartFile profileImage, + HttpServletResponse response ) { @@ -62,6 +67,9 @@ public ApiResponse registerUser( // 3. 프로필 등록 RegisterResponseDto registerResponseDto = userService.register(kakaoId, userRequestDto, profileImage, inviteCode); + // 4. 쿠키 등록 + cookieService.setTokenCookies(response, registerResponseDto.getAccessToken(), registerResponseDto.getRefreshToken()); + return ApiResponse.onSuccess(registerResponseDto); } @@ -118,7 +126,9 @@ public ApiResponse updateMyInfo( @DeleteMapping("/me") public ApiResponse withdraw( Authentication authentication, - @RequestHeader("Authorization") String token) { + @RequestHeader("Authorization") String token, + HttpServletResponse response + ) { CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal(); Long kakaoId = userDetails.getKakaoId(); @@ -129,9 +139,11 @@ public ApiResponse withdraw( // redis에서 리프레시 토큰 삭제 -> 접근 불가하게 만듦 authService.logout(accessToken); - userServiceWithdraw.withdraw(kakaoId); + cookieService.deleteTokenCookies(response); + cookieService.deleteTempTokenCookie(response); + return ApiResponse.of(SuccessStatus._OK, null); } } \ No newline at end of file