diff --git a/src/main/java/org/capstone/maru/controller/LoginController.java b/src/main/java/org/capstone/maru/controller/LoginController.java new file mode 100644 index 0000000..9f95455 --- /dev/null +++ b/src/main/java/org/capstone/maru/controller/LoginController.java @@ -0,0 +1,27 @@ +package org.capstone.maru.controller; + +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping +public class LoginController { + + @GetMapping("/login") + public String socialLogin() { + return "카카오 로그인 url: login-kakao | 네이버 로그인 url: login-naver"; + } + + @GetMapping("/login-kakao") + public void loginKakao(HttpServletResponse response) throws IOException { + response.sendRedirect("oauth2/authorization/kakao"); + } + + @GetMapping(value = "/login-naver") + public void loginNaver(HttpServletResponse response) throws IOException { + response.sendRedirect("oauth2/authorization/naver"); + } +} diff --git a/src/main/java/org/capstone/maru/controller/MainController.java b/src/main/java/org/capstone/maru/controller/MainController.java index 1888aac..ba352eb 100644 --- a/src/main/java/org/capstone/maru/controller/MainController.java +++ b/src/main/java/org/capstone/maru/controller/MainController.java @@ -1,13 +1,19 @@ package org.capstone.maru.controller; + +import lombok.RequiredArgsConstructor; import org.capstone.maru.security.principal.SharedPostPrincipal; +import org.capstone.maru.service.MemberAccountService; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; +@RequiredArgsConstructor @RestController public class MainController { + private final MemberAccountService memberAccountService; + @GetMapping("/") public String root() { return "health check"; @@ -15,7 +21,7 @@ public String root() { @GetMapping("/test") public String test(@AuthenticationPrincipal SharedPostPrincipal sharedPostPrincipal) { + return sharedPostPrincipal.getName(); } - } diff --git a/src/main/java/org/capstone/maru/exception/GlobalExceptionHandler.java b/src/main/java/org/capstone/maru/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..6023272 --- /dev/null +++ b/src/main/java/org/capstone/maru/exception/GlobalExceptionHandler.java @@ -0,0 +1,24 @@ +package org.capstone.maru.exception; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.servlet.resource.NoResourceFoundException; + +@Slf4j +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler({NoResourceFoundException.class}) + public ResponseEntity handleNoResourceFoundException( + NoResourceFoundException ex + ) { + log.error("NoResourceFoundException occur!: {}", ex.getMessage()); + + return ResponseEntity.status(HttpStatus.NOT_FOUND) + .body(RestErrorResponse.of(RestErrorCode.NOT_FOUND)); + } +} diff --git a/src/main/java/org/capstone/maru/exception/RestErrorCode.java b/src/main/java/org/capstone/maru/exception/RestErrorCode.java new file mode 100644 index 0000000..0485942 --- /dev/null +++ b/src/main/java/org/capstone/maru/exception/RestErrorCode.java @@ -0,0 +1,27 @@ +package org.capstone.maru.exception; + +import lombok.Getter; + +@Getter +public enum RestErrorCode { + + DUPLICATE_VALUE(409, "C001", "이미 존재하는 값입니다."), + UNAUTHORIZED(401, "C001", "인증 되지 않은 사용자입니다."), + NOT_FOUND(404, "C001", "존재하지 않는 url 입니다."), + ; + + // 에러 코드의 '코드 상태'을 반환한다. + private final int status; + + // 에러 코드의 '코드간 구분 값'을 반환한다. + private final String code; + + // 에러 코드의 '코드 메시지'을 반환한다. + private final String message; + + RestErrorCode(final int status, final String code, final String message) { + this.status = status; + this.code = code; + this.message = message; + } +} diff --git a/src/main/java/org/capstone/maru/exception/RestErrorResponse.java b/src/main/java/org/capstone/maru/exception/RestErrorResponse.java new file mode 100644 index 0000000..710a20e --- /dev/null +++ b/src/main/java/org/capstone/maru/exception/RestErrorResponse.java @@ -0,0 +1,49 @@ +package org.capstone.maru.exception; + +import lombok.Builder; + +/** + * @param status 에러 상태 코드 + * @param code 에러 구분 코드 + * @param errorMsg 에러 메시지 + * @param reason 에러 이유 + */ +@Builder +public record RestErrorResponse( + int status, + String code, + String errorMsg, + String reason +) { + + /** + * Global Exception 전송 타입 + * + * @param code ErrorCode + * @return ErrorResponse + */ + public static RestErrorResponse of(final RestErrorCode code) { + return RestErrorResponse.builder() + .status(code.getStatus()) + .code(code.getCode()) + .errorMsg(code.getMessage()) + .build(); + } + + /** + * Global Exception 전송 타입 + * + * @param code ErrorCode + * @param reason String + * @return ErrorResponse + */ + public static RestErrorResponse of(final RestErrorCode code, final String reason) { + return RestErrorResponse.builder() + .status(code.getStatus()) + .code(code.getCode()) + .errorMsg(code.getMessage()) + .reason(reason) + .build(); + } + +} diff --git a/src/main/java/org/capstone/maru/repository/MemberAccountRepository.java b/src/main/java/org/capstone/maru/repository/MemberAccountRepository.java index 01e5743..dc5caea 100644 --- a/src/main/java/org/capstone/maru/repository/MemberAccountRepository.java +++ b/src/main/java/org/capstone/maru/repository/MemberAccountRepository.java @@ -1,8 +1,10 @@ package org.capstone.maru.repository; +import java.util.Optional; import org.capstone.maru.domain.MemberAccount; import org.springframework.data.jpa.repository.JpaRepository; public interface MemberAccountRepository extends JpaRepository { + Optional findByEmail(String email); } diff --git a/src/main/java/org/capstone/maru/config/SecurityConfig.java b/src/main/java/org/capstone/maru/security/config/SecurityConfig.java similarity index 57% rename from src/main/java/org/capstone/maru/config/SecurityConfig.java rename to src/main/java/org/capstone/maru/security/config/SecurityConfig.java index 3616a68..6dab6a7 100644 --- a/src/main/java/org/capstone/maru/config/SecurityConfig.java +++ b/src/main/java/org/capstone/maru/security/config/SecurityConfig.java @@ -1,26 +1,43 @@ -package org.capstone.maru.config; +package org.capstone.maru.security.config; import lombok.extern.slf4j.Slf4j; import org.capstone.maru.security.service.CustomOAuth2UserService; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.security.servlet.PathRequest; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; +import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; @Slf4j @Configuration +@EnableWebSecurity public class SecurityConfig { + private final AuthenticationEntryPoint authEntryPoint; + + private final AuthenticationFailureHandler authFailureHandler; + + public SecurityConfig( + @Qualifier("customAuthenticationEntryPoint") AuthenticationEntryPoint authEntryPoint, + @Qualifier("customAuthenticationFailureHandler") AuthenticationFailureHandler authFailureHandler + ) { + this.authEntryPoint = authEntryPoint; + this.authFailureHandler = authFailureHandler; + } + @Bean @ConditionalOnProperty(name = "spring.h2.console.enabled", havingValue = "true") public WebSecurityCustomizer configureH2ConsoleEnable() { return web -> web.ignoring() - .requestMatchers(PathRequest.toH2Console()); + .requestMatchers(PathRequest.toH2Console()); } @Bean @@ -33,7 +50,12 @@ public SecurityFilterChain securityFilterChain( .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() .requestMatchers( HttpMethod.GET, - "/" + "/", "/login", "login-kakao", "login-naver", "/oauth2/**", "/login/oauth2/**", + "/errorTest" + ).permitAll() + .requestMatchers( + HttpMethod.POST, + "/login" ).permitAll() .anyRequest().authenticated() ) @@ -41,6 +63,10 @@ public SecurityFilterChain securityFilterChain( .userInfoEndpoint(userInfo -> userInfo .userService(customOAuth2UserService) ) + .failureHandler(authFailureHandler) + ) + .exceptionHandling(hc -> hc + .authenticationEntryPoint(authEntryPoint) ) .csrf( csrf -> csrf diff --git a/src/main/java/org/capstone/maru/security/exception/CustomAuthenticationEntryPoint.java b/src/main/java/org/capstone/maru/security/exception/CustomAuthenticationEntryPoint.java new file mode 100644 index 0000000..c3a7724 --- /dev/null +++ b/src/main/java/org/capstone/maru/security/exception/CustomAuthenticationEntryPoint.java @@ -0,0 +1,33 @@ +package org.capstone.maru.security.exception; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerExceptionResolver; + +@Component("customAuthenticationEntryPoint") +public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint { + + private final HandlerExceptionResolver resolver; + + public CustomAuthenticationEntryPoint( + @Qualifier("handlerExceptionResolver") HandlerExceptionResolver resolver + ) { + this.resolver = resolver; + } + + @Override + public void commence( + HttpServletRequest request, + HttpServletResponse response, + AuthenticationException authException + ) throws IOException, ServletException { + //-- 인증 처리가 안된 사용자가 인증이 필요한 URL에 접근했을 때 작동되는 로직 입력 --// + resolver.resolveException(request, response, null, authException); + } +} diff --git a/src/main/java/org/capstone/maru/security/exception/CustomAuthenticationFailureHandler.java b/src/main/java/org/capstone/maru/security/exception/CustomAuthenticationFailureHandler.java new file mode 100644 index 0000000..16a67f1 --- /dev/null +++ b/src/main/java/org/capstone/maru/security/exception/CustomAuthenticationFailureHandler.java @@ -0,0 +1,35 @@ +package org.capstone.maru.security.exception; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerExceptionResolver; + +@Slf4j +@Component("customAuthenticationFailureHandler") +public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler { + + private final HandlerExceptionResolver resolver; + + public CustomAuthenticationFailureHandler( + @Qualifier("handlerExceptionResolver") HandlerExceptionResolver resolver + ) { + this.resolver = resolver; + } + + @Override + public void onAuthenticationFailure( + HttpServletRequest request, + HttpServletResponse response, + AuthenticationException exception + ) throws IOException, ServletException { + //-- 로그인이 실패했을 때 작동되는 로직 입력 --// + resolver.resolveException(request, response, null, exception); + } +} diff --git a/src/main/java/org/capstone/maru/security/exception/GlobalSecurityExceptionHandler.java b/src/main/java/org/capstone/maru/security/exception/GlobalSecurityExceptionHandler.java new file mode 100644 index 0000000..b9266d8 --- /dev/null +++ b/src/main/java/org/capstone/maru/security/exception/GlobalSecurityExceptionHandler.java @@ -0,0 +1,38 @@ +package org.capstone.maru.security.exception; + +import lombok.extern.slf4j.Slf4j; +import org.capstone.maru.exception.RestErrorCode; +import org.capstone.maru.exception.RestErrorResponse; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.AuthenticationException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@Slf4j +@RestControllerAdvice +public class GlobalSecurityExceptionHandler { + + @ExceptionHandler({AuthenticationException.class}) + @ResponseBody + public ResponseEntity handleAuthenticationException( + AuthenticationException ex + ) { + log.error("AuthenticationException occur!: {}", ex.getMessage()); + + return ResponseEntity.status(HttpStatus.UNAUTHORIZED) + .body(RestErrorResponse.of(RestErrorCode.UNAUTHORIZED)); + } + + @ExceptionHandler({MemberAccountExistentException.class}) + @ResponseBody + public ResponseEntity handleAlreadyHaveMemberAccountException( + MemberAccountExistentException ex + ) { + log.error("MemberAccountExistentException occur!: {}", ex.getMessage()); + + return ResponseEntity.status(ex.getErrorCode().getStatus()) + .body(RestErrorResponse.of(ex.getErrorCode(), ex.getReason())); + } +} diff --git a/src/main/java/org/capstone/maru/security/exception/MemberAccountExistentException.java b/src/main/java/org/capstone/maru/security/exception/MemberAccountExistentException.java new file mode 100644 index 0000000..eb8095a --- /dev/null +++ b/src/main/java/org/capstone/maru/security/exception/MemberAccountExistentException.java @@ -0,0 +1,33 @@ +package org.capstone.maru.security.exception; + +import lombok.Getter; +import org.capstone.maru.exception.RestErrorCode; +import org.springframework.security.core.AuthenticationException; + +@Getter +public class MemberAccountExistentException extends AuthenticationException { + + private final RestErrorCode errorCode; + private final String reason; + + public MemberAccountExistentException(String msg, Throwable cause) { + this(RestErrorCode.DUPLICATE_VALUE, msg); + cause.fillInStackTrace(); + } + + public MemberAccountExistentException(String msg) { + this(RestErrorCode.DUPLICATE_VALUE, msg); + } + + public MemberAccountExistentException(RestErrorCode errorCode) { + super(errorCode.getMessage()); + this.errorCode = errorCode; + this.reason = "이미 가입한 회원 계정이 존재합니다."; + } + + public MemberAccountExistentException(RestErrorCode errorCode, String reason) { + super(errorCode.getMessage()); + this.errorCode = errorCode; + this.reason = reason; + } +} diff --git a/src/main/java/org/capstone/maru/security/service/CustomOAuth2UserService.java b/src/main/java/org/capstone/maru/security/service/CustomOAuth2UserService.java index df4566e..7231b52 100644 --- a/src/main/java/org/capstone/maru/security/service/CustomOAuth2UserService.java +++ b/src/main/java/org/capstone/maru/security/service/CustomOAuth2UserService.java @@ -54,4 +54,6 @@ private SocialType getSocialType(String registrationId) { private String getMemberId(String registrationId, OAuth2Response oAuth2Response) { return registrationId + "_" + oAuth2Response.id(); } + + } diff --git a/src/main/java/org/capstone/maru/service/MemberAccountService.java b/src/main/java/org/capstone/maru/service/MemberAccountService.java index 92760a9..34df1fc 100644 --- a/src/main/java/org/capstone/maru/service/MemberAccountService.java +++ b/src/main/java/org/capstone/maru/service/MemberAccountService.java @@ -2,6 +2,9 @@ import java.util.Optional; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.capstone.maru.exception.RestErrorCode; +import org.capstone.maru.security.exception.MemberAccountExistentException; import org.capstone.maru.domain.MemberAccount; import org.capstone.maru.dto.MemberAccountDto; import org.capstone.maru.repository.MemberAccountRepository; @@ -10,6 +13,7 @@ @RequiredArgsConstructor @Transactional +@Slf4j @Service public class MemberAccountService { @@ -21,11 +25,18 @@ public Optional searchMember(String memberId) { .map(MemberAccountDto::from); } + @Transactional public MemberAccountDto saveUser( String memberId, String email, String nickname ) { + if (memberAccountRepository.findByEmail(email).isPresent()) { + throw new MemberAccountExistentException( + RestErrorCode.DUPLICATE_VALUE + ); + } + return MemberAccountDto.from( memberAccountRepository.save( MemberAccount.of(