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,12 +1,12 @@
package org.umc.valuedi.domain.auth.controller;

import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.umc.valuedi.global.external.kakao.config.KakaoProperties;
import org.umc.valuedi.domain.auth.converter.AuthConverter;
import org.umc.valuedi.domain.auth.dto.req.AuthReqDTO;
import org.umc.valuedi.domain.auth.dto.res.AuthResDTO;
import org.umc.valuedi.domain.auth.exception.AuthException;
Expand All @@ -16,6 +16,7 @@
import org.umc.valuedi.domain.auth.service.query.AuthQueryService;
import org.umc.valuedi.global.apiPayload.ApiResponse;
import org.umc.valuedi.global.security.annotation.CurrentMember;
import org.umc.valuedi.global.security.jwt.JwtUtil;
import org.umc.valuedi.global.security.util.CookieUtil;

import java.util.UUID;
Expand All @@ -30,27 +31,43 @@ public class AuthController implements AuthControllerDocs {
private final AuthQueryService authQueryService;
private final KakaoProperties kakaoProperties;
private final CookieUtil cookieUtil;
private final JwtUtil jwtUtil;

@Override
@GetMapping("/oauth/kakao/login")
public ApiResponse<AuthResDTO.LoginUrlDTO> kakaoLogin() {
public ApiResponse<String> kakaoLogin(HttpServletResponse response) {
String state = UUID.randomUUID().toString();
cookieUtil.addCookie(response, "oauth_state", state, 600, "/auth/oauth/kakao/callback");

String loginUrl = kakaoProperties.getKakaoAuthUrl(state);
return ApiResponse.onSuccess(AuthSuccessCode.KAKAO_AUTH_URL_SUCCESS, AuthConverter.toLoginUrlDTO(loginUrl, state));
return ApiResponse.onSuccess(AuthSuccessCode.KAKAO_AUTH_URL_SUCCESS, loginUrl);
}

@Override
@GetMapping("/oauth/kakao/callback")
public ApiResponse<AuthResDTO.LoginResultDTO> kakaoCallback(
@RequestParam("code") String code,
@RequestParam("state") String state,
@RequestParam("originalState") String originalState
@CookieValue(name = "oauth_state", required = false) String oauthState,
HttpServletResponse response
) {
if (originalState == null || !originalState.equals(state)) {
cookieUtil.deleteCookie(response, "oauth_state", "/auth/oauth/kakao/callback");

if (oauthState == null || !oauthState.equals(state)) {
throw new AuthException(AuthErrorCode.INVALID_STATE);
}

AuthResDTO.LoginResultDTO result = authCommandService.loginKakao(code);

// 리프레시 토큰은 쿠키에 저장
cookieUtil.addCookie(
response,
"refreshToken",
result.refreshToken(),
(int) jwtUtil.getRefreshTokenExpiration() / 1000,
"/auth/token/refresh"
);

return ApiResponse.onSuccess(AuthSuccessCode.LOGIN_OK, result);
}

Expand Down Expand Up @@ -91,31 +108,54 @@ public ResponseEntity<ApiResponse<AuthResDTO.RegisterResDTO>> signUp(
@Override
@PostMapping("/login")
public ApiResponse<AuthResDTO.LoginResultDTO> localLogin(
@RequestBody AuthReqDTO.LocalLoginDTO dto
@RequestBody AuthReqDTO.LocalLoginDTO dto,
HttpServletResponse response
) {

AuthResDTO.LoginResultDTO result = authCommandService.loginLocal(dto);

// 리프레시 토큰은 쿠키에 저장
cookieUtil.addCookie(
response,
"refreshToken",
result.refreshToken(),
(int) jwtUtil.getRefreshTokenExpiration() / 1000,
"/auth/token/refresh"
);

return ApiResponse.onSuccess(AuthSuccessCode.LOGIN_OK, result);
}

@Override
@PostMapping("/token/refresh")
public ApiResponse<AuthResDTO.LoginResultDTO> tokenReissue(
@RequestHeader(value = "Authorization", required = false) String accessToken,
@RequestParam String refreshToken
@CookieValue(name = "refreshToken") String refreshToken,
HttpServletResponse response
) {
AuthResDTO.LoginResultDTO result = authCommandService.tokenReissue(accessToken, refreshToken);

// 리프레시 토큰은 쿠키에 저장
cookieUtil.addCookie(
response,
"refreshToken",
result.refreshToken(),
(int) jwtUtil.getRefreshTokenExpiration() / 1000,
"/auth/token/refresh"
);

return ApiResponse.onSuccess(AuthSuccessCode.TOKEN_REISSUE_SUCCESS, result);
}

@Override
@PostMapping("/logout")
public ApiResponse<Void> logout(
@CurrentMember Long memberId,
@RequestHeader("Authorization") String accessToken
@RequestHeader("Authorization") String accessToken,
HttpServletResponse response
) {
authCommandService.logout(memberId, accessToken);

cookieUtil.deleteCookie(response, "refreshToken", "/auth/token/refresh");
return ApiResponse.onSuccess(AuthSuccessCode.LOGOUT_OK, null);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,16 @@
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestHeader;
import org.umc.valuedi.domain.auth.dto.req.AuthReqDTO;
import org.umc.valuedi.domain.auth.dto.res.AuthResDTO;
import org.umc.valuedi.global.apiPayload.ApiResponse;
import org.umc.valuedi.global.security.annotation.CurrentMember;
import org.umc.valuedi.global.security.principal.CustomUserDetails;

@Tag(name = "Auth", description = "Auth 관련 API (로그인, 회원가입 등)")
public interface AuthControllerDocs {

@Operation(
summary = "카카오 로그인 URL 생성 API",
description = "카카오 로그인 페이지로 이동하기 위한 URL을 생성하고, 보안을 위한 state 값을 함께 응답합니다.")
description = "카카오 로그인 페이지로 이동하기 위한 URL을 생성하고, 보안을 위한 state 값을 쿠키에 저장합니다.")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "200",
Expand All @@ -32,26 +29,24 @@ public interface AuthControllerDocs {
schema = @Schema(implementation = ApiResponse.class),
examples = @ExampleObject(
name = "카카오 로그인 URL 생성 예시",
description = "생성된 URL에는 보안을 위한 state 파라미터가 추가되어 있습니다.",
value = """
{
"isSuccess": true,
"code": "AUTH200_1",
"message": "카카오 로그인 URL이 성공적으로 생성되었습니다.",
"result": {
"url": "https://kauth.kakao.com/oauth/authorize?response_type=code&client_id={clientId}&redirect_uri={redirectUri}&state=6af5e726-...",
"state": "6af5e726-7df6-4d03-9829-4b7d4ad792ec"
}
"result": "https://kauth.kakao.com/oauth/authorize?response_type=code&client_id={clientId}&redirect_uri={redirectUri}&state=bba27165-..."
}
"""
)
)
)
})
ApiResponse<AuthResDTO.LoginUrlDTO> kakaoLogin();
ApiResponse<String> kakaoLogin(HttpServletResponse response);

@Operation(
summary = "카카오 로그인 콜백 API",
description = "카카오로부터 인가 코드를 받아 로그인을 완료하고 JWT를 발급합니다. \n기존에 카카오로 로그인한 적 없는 경우, 회원가입 처리 후 JWT를 발급합니다.")
description = "카카오로부터 인가 코드를 받아 로그인을 완료하고 JWT를 발급합니다. \n기존에 카카오로 로그인한 적 없는 경우, 회원가입 처리 후 JWT를 발급합니다. \n리프레시 토큰은 쿠키에 저장됩니다.")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "200",
Expand All @@ -67,7 +62,6 @@ public interface AuthControllerDocs {
"message": "로그인에 성공했습니다.",
"result": {
"accessToken": "eyJhbGciOiJIUzI1NiJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiJ9...",
"memberId": 1
}
}
Expand Down Expand Up @@ -130,8 +124,9 @@ ApiResponse<AuthResDTO.LoginResultDTO> kakaoCallback(
String code,
@Parameter(description = "카카오에서 전달한 state 값")
String state,
@Parameter(description = "클라이언트가 저장해둔 원본 state 값")
String originalState
@Parameter(hidden = true)
String oauthState,
HttpServletResponse response
);

@Operation(summary = "아이디 중복 확인 API", description = "사용자가 입력한 아이디의 중복 여부를 확인합니다.")
Expand Down Expand Up @@ -364,7 +359,7 @@ public ResponseEntity<ApiResponse<AuthResDTO.RegisterResDTO>> signUp(

@Operation(
summary = "로컬 계정 로그인 API",
description = "로컬 계정으로 로그인을 시도합니다. 로그인이 완료되면 JWT를 발급합니다.")
description = "로컬 계정으로 로그인을 시도합니다. 로그인이 완료되면 JWT를 발급합니다. \n리프레시 토큰은 쿠키에 저장됩니다.")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "200",
Expand All @@ -380,7 +375,6 @@ public ResponseEntity<ApiResponse<AuthResDTO.RegisterResDTO>> signUp(
"message": "로그인에 성공했습니다.",
"result": {
"accessToken": "eyJhbGciOiJIUzI1NiJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiJ9...",
"memberId": 1
}
}
Expand Down Expand Up @@ -439,12 +433,13 @@ public ResponseEntity<ApiResponse<AuthResDTO.RegisterResDTO>> signUp(
)
})
public ApiResponse<AuthResDTO.LoginResultDTO> localLogin(
@Valid AuthReqDTO.LocalLoginDTO dto
@Valid AuthReqDTO.LocalLoginDTO dto,
HttpServletResponse response
);

@Operation(
summary = "토큰 재발급 API",
description = "리프레시 토큰으로 새로운 액세스 토큰과 리프레시 토큰을 발급합니다. \n요청 헤더에 만료되지 않은 액세스 토큰이 있다면 이를 무효화합니다.")
description = "쿠키에 저장된 리프레시 토큰으로 새로운 액세스 토큰과 리프레시 토큰을 발급합니다. \n요청 헤더에 만료되지 않은 액세스 토큰이 있다면 이를 무효화하며, 새로 발급된 리프레시 토큰은 쿠키에 저장됩니다.")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "200",
Expand All @@ -460,7 +455,6 @@ public ApiResponse<AuthResDTO.LoginResultDTO> localLogin(
"message": "토큰 재발급에 성공했습니다.",
"result": {
"accessToken": "eyJhbGciOiJIUzI1NiJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiJ9...",
"memberId": 1
}
}
Expand All @@ -472,7 +466,9 @@ public ApiResponse<AuthResDTO.LoginResultDTO> localLogin(
public ApiResponse<AuthResDTO.LoginResultDTO> tokenReissue(
@Parameter(hidden = true)
String accessToken,
String refreshToken
@Parameter(hidden = true)
String refreshToken,
HttpServletResponse response
);

@Operation(
Expand Down Expand Up @@ -501,7 +497,8 @@ public ApiResponse<AuthResDTO.LoginResultDTO> tokenReissue(
public ApiResponse<Void> logout(
Long memberId,
@Parameter(hidden = true)
String accessToken
String accessToken,
HttpServletResponse response
);

@Operation(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,6 @@

public class AuthConverter {

// 카카오 로그인 URL과 state 값을 하나의 DTO로 변환
public static AuthResDTO.LoginUrlDTO toLoginUrlDTO(String loginUrl, String state) {
return new AuthResDTO.LoginUrlDTO(loginUrl, state);
}

// 로컬 회원가입 정보 바탕으로 Member 엔티티 생성
public static Member toGeneralMember(AuthReqDTO.RegisterReqDTO dto, String encodedPassword) {
return Member.builder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ public class AuthResDTO {
@Builder
public record LoginResultDTO (
String accessToken,

// 리프레시 토큰은 쿠키로 전달하고 JSON body에는 저장하지 않음
@JsonIgnore
String refreshToken,

Long memberId
) {}

Expand All @@ -22,10 +26,4 @@ public record AuthStatusDTO (
Boolean isLogin,
Long memberId
) {}

@Builder
public record LoginUrlDTO(
String url,
String state
) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,10 @@ public JwtAuthFilter jwtAuthFilter() {
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();

configuration.setAllowedOriginPatterns(List.of("*"));
configuration.setAllowedOrigins(Arrays.asList(
"https://valuedi-web.vercel.app",
"http://localhost:5173"
));

configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"));
configuration.setAllowedHeaders(Arrays.asList("*"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ public class CookieUtil {
public void addCookie(HttpServletResponse response, String name, String value, int maxAge, String path) {
ResponseCookie cookie = ResponseCookie.from(name, value)
.httpOnly(true)
.secure(false) // 개발 서버 테스트를 위해 임시로 HTTP 허용. 실제 배포 시에는 true로 변경
.secure(true)
.path(path)
.maxAge(maxAge)
.sameSite("Lax")
.sameSite("None")
.build();

response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
Expand Down