diff --git a/backend/baguni-api/src/main/java/baguni/api/application/user/controller/UserApiController.java b/backend/baguni-api/src/main/java/baguni/api/application/user/controller/UserApiController.java index db3fe95f2..b1cd8bb56 100644 --- a/backend/baguni-api/src/main/java/baguni/api/application/user/controller/UserApiController.java +++ b/backend/baguni-api/src/main/java/baguni/api/application/user/controller/UserApiController.java @@ -2,17 +2,19 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import baguni.api.application.user.controller.dto.UserApiMapper; +import baguni.api.application.user.controller.dto.UserInfoApiResponse; import baguni.api.service.user.service.UserService; import baguni.security.annotation.LoginUserId; -import baguni.security.handler.BaguniLogoutHandler; +import baguni.security.util.CookieUtil; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; @@ -23,7 +25,8 @@ public class UserApiController { private final UserService userService; - private final BaguniLogoutHandler logoutHandler; + private final CookieUtil cookieUtil; + private final UserApiMapper userApiMapper; @DeleteMapping @Operation(summary = "회원 탈퇴", description = "회원 탈퇴를 하면 모든 폴더, 픽, 태그가 삭제됩니다.") @@ -35,7 +38,20 @@ public ResponseEntity deleteUser( HttpServletResponse response ) { userService.deleteUser(userId); - logoutHandler.clearCookies(response); + cookieUtil.clearCookies(response); return ResponseEntity.noContent().build(); } + + @GetMapping + @Operation(summary = "로그인 회원의 정보 획득", description = "회원 식별자(ID_TOKEN) 및 이메일 등의 비민감성 정보를 획득합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "회원 정보 획득 성공") + }) + public ResponseEntity getUserInfo( + @LoginUserId Long userId + ) { + var userInfo = userService.getUserInfoById(userId); + var response = userApiMapper.toApiResponse(userInfo); + return ResponseEntity.ok(response); + } } \ No newline at end of file diff --git a/backend/baguni-api/src/main/java/baguni/api/application/user/controller/dto/UserApiMapper.java b/backend/baguni-api/src/main/java/baguni/api/application/user/controller/dto/UserApiMapper.java new file mode 100644 index 000000000..0019caba5 --- /dev/null +++ b/backend/baguni-api/src/main/java/baguni/api/application/user/controller/dto/UserApiMapper.java @@ -0,0 +1,19 @@ +package baguni.api.application.user.controller.dto; + +import org.mapstruct.InjectionStrategy; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.ReportingPolicy; + +import baguni.api.service.user.dto.UserInfo; + +@Mapper( + componentModel = "spring", + injectionStrategy = InjectionStrategy.CONSTRUCTOR, + unmappedTargetPolicy = ReportingPolicy.ERROR +) +public interface UserApiMapper { + + @Mapping(expression = "java(userInfo.idToken().toString())", target = "idToken") + UserInfoApiResponse toApiResponse(UserInfo userInfo); +} diff --git a/backend/baguni-api/src/main/java/baguni/api/application/user/controller/dto/UserInfoApiResponse.java b/backend/baguni-api/src/main/java/baguni/api/application/user/controller/dto/UserInfoApiResponse.java new file mode 100644 index 000000000..a5b3a4830 --- /dev/null +++ b/backend/baguni-api/src/main/java/baguni/api/application/user/controller/dto/UserInfoApiResponse.java @@ -0,0 +1,20 @@ +package baguni.api.application.user.controller.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; + +public record UserInfoApiResponse( + + @NotEmpty + @Schema(description = "사용자 식별 토큰") + String idToken, + + @NotEmpty + @Schema(description = "사용자 이메일") + String email, + + // Nullable + @Schema(description = "사용자 이름") + String name +) { +} diff --git a/backend/baguni-api/src/main/java/baguni/api/service/user/dto/UserInfo.java b/backend/baguni-api/src/main/java/baguni/api/service/user/dto/UserInfo.java index 63f9c8a96..e62e371b5 100644 --- a/backend/baguni-api/src/main/java/baguni/api/service/user/dto/UserInfo.java +++ b/backend/baguni-api/src/main/java/baguni/api/service/user/dto/UserInfo.java @@ -1,13 +1,15 @@ package baguni.api.service.user.dto; import baguni.entity.model.user.User; +import baguni.entity.model.util.IDToken; public record UserInfo( Long id, String name, + IDToken idToken, String email ) { public static UserInfo from(User user) { - return new UserInfo(user.getId(), user.getNickname(), user.getEmail()); + return new UserInfo(user.getId(), user.getNickname(), user.getIdToken(), user.getEmail()); } } diff --git a/backend/baguni-api/src/main/java/baguni/api/service/user/service/UserService.java b/backend/baguni-api/src/main/java/baguni/api/service/user/service/UserService.java index 289d659a4..8d46c0c1f 100644 --- a/backend/baguni-api/src/main/java/baguni/api/service/user/service/UserService.java +++ b/backend/baguni-api/src/main/java/baguni/api/service/user/service/UserService.java @@ -45,6 +45,12 @@ public UserInfo getUserInfoByToken(IDToken idToken) { return UserInfo.from(user); } + @Transactional(readOnly = true) + public UserInfo getUserInfoById(Long userId) { + var user = userDataHandler.getUser(userId); + return UserInfo.from(user); + } + @Transactional public void deleteUser(Long userId) { userDataHandler.deleteUser(userId); diff --git a/backend/baguni-api/src/main/java/baguni/security/exception/ApiAuthErrorCode.java b/backend/baguni-api/src/main/java/baguni/security/exception/ApiAuthErrorCode.java index abf1e1ae6..bef6391f1 100644 --- a/backend/baguni-api/src/main/java/baguni/security/exception/ApiAuthErrorCode.java +++ b/backend/baguni-api/src/main/java/baguni/security/exception/ApiAuthErrorCode.java @@ -22,6 +22,10 @@ public enum ApiAuthErrorCode implements ApiErrorCode { AUTH_SERVER_FAILURE ("AU-003", HttpStatus.SERVICE_UNAVAILABLE, "인증 처리 과정에 서버 오류가 발생했습니다! 개발자 문의 필요", ErrorLevel.MUST_NEVER_HAPPEN()), + + AUTH_INVALID_ID_TOKEN + ("AU-004", HttpStatus.UNAUTHORIZED, "사용자 식별 토큰 (ID TOKEN)이 유효한 값이 아닙니다.", + ErrorLevel.MUST_NEVER_HAPPEN()), ; // ------------------------------------------------------------ diff --git a/backend/baguni-api/src/main/java/baguni/security/exception/ApiAuthException.java b/backend/baguni-api/src/main/java/baguni/security/exception/ApiAuthException.java index 51fb6711c..73dc91faf 100644 --- a/backend/baguni-api/src/main/java/baguni/security/exception/ApiAuthException.java +++ b/backend/baguni-api/src/main/java/baguni/security/exception/ApiAuthException.java @@ -45,6 +45,10 @@ public static ApiAuthException INVALID_AUTHENTICATION() { return new ApiAuthException(ApiAuthErrorCode.AUTH_INVALID_AUTHENTICATION); } + public static ApiAuthException INVALID_USER_ID_TOKEN() { + return new ApiAuthException(ApiAuthErrorCode.AUTH_INVALID_ID_TOKEN); + } + public static ApiAuthException OAUTH_TOKEN_ATTRIBUTE_NOT_FOUND(String targetTokenKey) { return new ApiAuthException(ApiAuthErrorCode.AUTH_TOKEN_ATTRIBUTE_NOT_FOUND, targetTokenKey); } diff --git a/backend/baguni-api/src/main/java/baguni/security/handler/BaguniApiAuthExceptionEntrypoint.java b/backend/baguni-api/src/main/java/baguni/security/handler/BaguniApiAuthExceptionEntrypoint.java index c574aae3c..bde445467 100644 --- a/backend/baguni-api/src/main/java/baguni/security/handler/BaguniApiAuthExceptionEntrypoint.java +++ b/backend/baguni-api/src/main/java/baguni/security/handler/BaguniApiAuthExceptionEntrypoint.java @@ -9,13 +9,13 @@ import com.fasterxml.jackson.databind.ObjectMapper; -import baguni.api.service.user.service.UserService; import baguni.common.exception.base.ApiErrorResponse; import baguni.security.exception.ApiAuthErrorCode; import baguni.security.util.CookieUtil; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; /** * @author minkyeu kim @@ -27,10 +27,13 @@ * case 1. 토큰이 만료된 요청이 온 경우
* case 2. 변조된 JWT를 가진 요청인 경우 */ +@Slf4j @Component @RequiredArgsConstructor public class BaguniApiAuthExceptionEntrypoint implements AuthenticationEntryPoint { + private final CookieUtil cookieUtil; + /** * 시큐리티의 HttpServletResponse를 바구니 API ErrorResponse로 변환한다. * Security에서 서비스 에러 코드를 보내야 프론트가 UI를 처리할 수 있기 때문이다. @@ -39,12 +42,15 @@ public class BaguniApiAuthExceptionEntrypoint implements AuthenticationEntryPoin public void commence( HttpServletRequest request, HttpServletResponse response, - AuthenticationException arg2 + AuthenticationException exception ) throws IOException { + log.error(exception.getMessage(), exception); var errorResponse = ApiErrorResponse.of(ApiAuthErrorCode.AUTH_INVALID_AUTHENTICATION); var errorStatus = errorResponse.getStatusCode().value(); var body = errorResponse.getBody(); + cookieUtil.clearCookies(response); + response.setStatus(errorStatus); if (Objects.nonNull(body)) { var errorResponseJson = new ObjectMapper().writeValueAsString(body); diff --git a/backend/baguni-api/src/main/java/baguni/security/handler/BaguniLogoutHandler.java b/backend/baguni-api/src/main/java/baguni/security/handler/BaguniLogoutHandler.java index 7b3f7fd2f..f8c632a09 100644 --- a/backend/baguni-api/src/main/java/baguni/security/handler/BaguniLogoutHandler.java +++ b/backend/baguni-api/src/main/java/baguni/security/handler/BaguniLogoutHandler.java @@ -16,7 +16,6 @@ public class BaguniLogoutHandler implements LogoutHandler, LogoutSuccessHandler { private final CookieUtil cookieUtil; - private final SecurityProperties properties; @Override public void logout( @@ -24,7 +23,7 @@ public void logout( HttpServletResponse response, Authentication authentication ) { - clearCookies(response); + cookieUtil.clearCookies(response); } @Override @@ -35,14 +34,4 @@ public void onLogoutSuccess( ) { response.setStatus(HttpServletResponse.SC_OK); } - - /** - * @author sangwon - * 쿠키 삭제 메서드 분리 (공통으로 사용하기 위함) - * 시큐리티, 쿠키를 제거해주고 싶은 컨트롤러에서 사용하기 위해 분리 - */ - public void clearCookies(HttpServletResponse response) { - cookieUtil.deleteCookie(response, properties.ACCESS_TOKEN_KEY); - cookieUtil.deleteCookie(response, "JSESSIONID"); - } } \ No newline at end of file diff --git a/backend/baguni-api/src/main/java/baguni/security/util/AccessToken.java b/backend/baguni-api/src/main/java/baguni/security/util/AccessToken.java index 3fe9d0a78..aa4a9d3e4 100644 --- a/backend/baguni-api/src/main/java/baguni/security/util/AccessToken.java +++ b/backend/baguni-api/src/main/java/baguni/security/util/AccessToken.java @@ -9,7 +9,6 @@ import baguni.entity.model.user.Role; import baguni.entity.model.util.IDToken; -import baguni.entity.model.util.IdTokenConversionException; import baguni.security.config.JwtProperties; import baguni.security.exception.ApiAuthException; import io.jsonwebtoken.Claims; @@ -42,7 +41,7 @@ public IDToken getUserIdToken() { var raw = getClaims().get("id", String.class); return IDToken.fromString(raw); } catch (Exception e) { - throw ApiAuthException.INVALID_AUTHENTICATION(); + throw ApiAuthException.INVALID_USER_ID_TOKEN(); } } diff --git a/backend/baguni-api/src/main/java/baguni/security/util/CookieUtil.java b/backend/baguni-api/src/main/java/baguni/security/util/CookieUtil.java index fe267d136..543940573 100644 --- a/backend/baguni-api/src/main/java/baguni/security/util/CookieUtil.java +++ b/backend/baguni-api/src/main/java/baguni/security/util/CookieUtil.java @@ -67,6 +67,17 @@ public void deleteCookie(HttpServletResponse response, String name) { this.addCookie(response, name, "", 0, true); } + /** + * @author sangwon + * 쿠키 삭제 메서드 분리 (공통으로 사용하기 위함) + * 시큐리티, 쿠키를 제거해주고 싶은 컨트롤러에서 사용하기 위해 분리 + */ + public void clearCookies(HttpServletResponse response) { + deleteCookie(response, securityProps.ACCESS_TOKEN_KEY); + deleteCookie(response, securityProps.OAUTH_RETURN_URL_KEY); + deleteCookie(response, "JSESSIONID"); + } + public Optional findCookieValue(Cookie[] cookies, String name) { if (cookies == null) return Optional.empty(); diff --git a/backend/baguni-api/src/main/resources/logback-spring.xml b/backend/baguni-api/src/main/resources/logback-spring.xml index 7455f12e4..dc05a430a 100644 --- a/backend/baguni-api/src/main/resources/logback-spring.xml +++ b/backend/baguni-api/src/main/resources/logback-spring.xml @@ -74,11 +74,7 @@ - - - - - + diff --git a/backend/baguni-batch/src/main/resources/logback-spring.xml b/backend/baguni-batch/src/main/resources/logback-spring.xml index fab3204d7..7b1f40dd7 100644 --- a/backend/baguni-batch/src/main/resources/logback-spring.xml +++ b/backend/baguni-batch/src/main/resources/logback-spring.xml @@ -85,11 +85,7 @@ - - - - - + diff --git a/backend/baguni-ranking/src/main/resources/logback-spring.xml b/backend/baguni-ranking/src/main/resources/logback-spring.xml index ec25c5638..69bca661b 100644 --- a/backend/baguni-ranking/src/main/resources/logback-spring.xml +++ b/backend/baguni-ranking/src/main/resources/logback-spring.xml @@ -75,14 +75,8 @@ - + - - - - - - \ No newline at end of file