From d1c699778b80a373a66d980dd23d4c685f51f0ef Mon Sep 17 00:00:00 2001 From: AN-SOHYEON Date: Fri, 19 Jan 2024 17:44:10 +0900 Subject: [PATCH] =?UTF-8?q?Bugfix:=20OAuth2=20=EC=84=9C=EB=B2=84=EB=A1=9C?= =?UTF-8?q?=EB=B6=80=ED=84=B0=20=EC=9C=A0=EC=A0=80=EC=9D=98=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=EA=B0=80=20null=EB=A1=9C=20=EB=93=A4=EC=96=B4?= =?UTF-8?q?=EC=99=94=EC=9D=84=20=EB=95=8C=20=EC=97=90=EB=9F=AC=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=ED=95=98=EA=B8=B0=20(#172)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Feature: Add exception handling in security-filter logic * Fix: api-에러코드를 사용하는 패키지로 이동 * Refactor: securityConfig에 구현했던 handler 분리 * Fix: 변수명 수정 & 필요없는 예외 클래스 삭제 --- .../jdon/auth/dto/AuthExceptionResponse.java | 22 +++++++ .../kernel/jdon/auth/error/AuthErrorCode.java | 5 +- .../auth/service/JdonOAuth2UserService.java | 42 ++++++++----- .../config/auth/JdonAuthExceptionHandler.java | 54 +++++++++++++++++ ...donOAuth2AuthenticationSuccessHandler.java | 53 +++++++++++++++++ .../{ => auth}/OAuth2SecurityConfig.java | 59 ++++--------------- .../exception/UnAuthorizedException.java | 15 +++++ .../error}/JobCategoryErrorCode.java | 2 +- .../jdon/member/error}/MemberErrorCode.java | 2 +- .../jdon/member/service/MemberService.java | 6 +- .../jdon/skill/error}/SkillErrorCode.java | 2 +- .../java/kernel/jdon/util/DateParserUtil.java | 15 +++++ 12 files changed, 208 insertions(+), 69 deletions(-) create mode 100644 module-api/src/main/java/kernel/jdon/auth/dto/AuthExceptionResponse.java create mode 100644 module-api/src/main/java/kernel/jdon/config/auth/JdonAuthExceptionHandler.java create mode 100644 module-api/src/main/java/kernel/jdon/config/auth/JdonOAuth2AuthenticationSuccessHandler.java rename module-api/src/main/java/kernel/jdon/config/{ => auth}/OAuth2SecurityConfig.java (53%) create mode 100644 module-api/src/main/java/kernel/jdon/global/exception/UnAuthorizedException.java rename {module-common/src/main/java/kernel/jdon/error/code/api => module-api/src/main/java/kernel/jdon/jobcategory/error}/JobCategoryErrorCode.java (92%) rename {module-common/src/main/java/kernel/jdon/error/code/api => module-api/src/main/java/kernel/jdon/member/error}/MemberErrorCode.java (96%) rename {module-common/src/main/java/kernel/jdon/error/code/api => module-api/src/main/java/kernel/jdon/skill/error}/SkillErrorCode.java (93%) create mode 100644 module-common/src/main/java/kernel/jdon/util/DateParserUtil.java diff --git a/module-api/src/main/java/kernel/jdon/auth/dto/AuthExceptionResponse.java b/module-api/src/main/java/kernel/jdon/auth/dto/AuthExceptionResponse.java new file mode 100644 index 000000000..a1bfffff1 --- /dev/null +++ b/module-api/src/main/java/kernel/jdon/auth/dto/AuthExceptionResponse.java @@ -0,0 +1,22 @@ +package kernel.jdon.auth.dto; + +import java.time.LocalDateTime; + +import kernel.jdon.util.DateParserUtil; +import lombok.Getter; + +@Getter +public class AuthExceptionResponse { + + private String timestamp; + private int status; + private String message; + private String path; + + public AuthExceptionResponse(int status, String message, String path) { + this.timestamp = DateParserUtil.dateTimeToString(LocalDateTime.now()); + this.status = status; + this.message = message; + this.path = path; + } +} diff --git a/module-api/src/main/java/kernel/jdon/auth/error/AuthErrorCode.java b/module-api/src/main/java/kernel/jdon/auth/error/AuthErrorCode.java index d85ee5eb8..cec487ebd 100644 --- a/module-api/src/main/java/kernel/jdon/auth/error/AuthErrorCode.java +++ b/module-api/src/main/java/kernel/jdon/auth/error/AuthErrorCode.java @@ -7,7 +7,10 @@ @RequiredArgsConstructor public enum AuthErrorCode implements ErrorCode { - NOT_FOUND_NOT_MATCH_PROVIDER_TYPE(HttpStatus.NOT_FOUND, "다른 소셜 로그인으로 가입된 이메일입니다."),; + UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "인증되지 않은 사용자입니다."), + UNAUTHORIZED_NOT_MATCH_PROVIDER_TYPE(HttpStatus.UNAUTHORIZED, "다른 소셜 로그인으로 가입된 이메일입니다."), + UNAUTHORIZED_OAUTH_RETURN_NULL_EMAIL(HttpStatus.UNAUTHORIZED, "소셜 서비스에 이메일을 등록해야 서비스를 이용할 수 있습니다."), + FORBIDDEN(HttpStatus.FORBIDDEN, "권한이 없는 사용자입니다."); private final HttpStatus httpStatus; private final String message; diff --git a/module-api/src/main/java/kernel/jdon/auth/service/JdonOAuth2UserService.java b/module-api/src/main/java/kernel/jdon/auth/service/JdonOAuth2UserService.java index 38f3b8bb6..4a54ce969 100644 --- a/module-api/src/main/java/kernel/jdon/auth/service/JdonOAuth2UserService.java +++ b/module-api/src/main/java/kernel/jdon/auth/service/JdonOAuth2UserService.java @@ -1,27 +1,26 @@ package kernel.jdon.auth.service; -import java.util.List; -import java.util.Map; - -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; -import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; -import org.springframework.security.oauth2.core.OAuth2AuthenticationException; -import org.springframework.security.oauth2.core.user.DefaultOAuth2User; -import org.springframework.security.oauth2.core.user.OAuth2User; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - import jakarta.servlet.http.HttpSession; import kernel.jdon.auth.dto.JdonOAuth2User; import kernel.jdon.auth.dto.SessionUserInfo; import kernel.jdon.auth.error.AuthErrorCode; -import kernel.jdon.global.exception.ApiException; +import kernel.jdon.global.exception.UnAuthorizedException; import kernel.jdon.member.domain.Member; import kernel.jdon.member.domain.SocialProviderType; import kernel.jdon.member.repository.MemberRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.user.DefaultOAuth2User; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Map; @Slf4j @Service @@ -62,17 +61,28 @@ private DefaultOAuth2User getOAuth2UserFromOAuthServer(OAuth2UserRequest userReq } private String getEmailFromGithub(OAuth2User user) { - return (String)user.getAttributes().get("email"); + String email = (String)user.getAttributes().get("email"); + isEmailExist(email); + + return email; } private String getEmailFromKakao(OAuth2User user) { Map attributes = user.getAttributes(); - return ((Map)attributes.get("kakao_account")).get("email"); + String email = ((Map)attributes.get("kakao_account")).get("email"); + isEmailExist(email); + + return email; + } + + private void isEmailExist(String email) { + if (email == null) + throw new UnAuthorizedException(AuthErrorCode.UNAUTHORIZED_OAUTH_RETURN_NULL_EMAIL); } private void checkRightSocialProvider(Member findMember, SocialProviderType socialProvider) { if (!findMember.isRightSocialProvider(socialProvider)) - throw new ApiException(AuthErrorCode.NOT_FOUND_NOT_MATCH_PROVIDER_TYPE); + throw new UnAuthorizedException(AuthErrorCode.UNAUTHORIZED_NOT_MATCH_PROVIDER_TYPE); } private SocialProviderType getSocialProvider(OAuth2UserRequest userRequest) { diff --git a/module-api/src/main/java/kernel/jdon/config/auth/JdonAuthExceptionHandler.java b/module-api/src/main/java/kernel/jdon/config/auth/JdonAuthExceptionHandler.java new file mode 100644 index 000000000..40051e48d --- /dev/null +++ b/module-api/src/main/java/kernel/jdon/config/auth/JdonAuthExceptionHandler.java @@ -0,0 +1,54 @@ +package kernel.jdon.config.auth; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import kernel.jdon.auth.dto.AuthExceptionResponse; +import kernel.jdon.auth.error.AuthErrorCode; +import kernel.jdon.error.ErrorCode; +import kernel.jdon.global.exception.UnAuthorizedException; +import org.springframework.http.MediaType; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.io.PrintWriter; + +@Component +public class JdonAuthExceptionHandler implements AuthenticationEntryPoint, AccessDeniedHandler, AuthenticationFailureHandler { + + private void throwAuthException(HttpServletResponse response, ErrorCode authErrorCode, String redirectUri) throws + IOException { + AuthExceptionResponse exceptionResponse = new AuthExceptionResponse( + authErrorCode.getHttpStatus().value(), authErrorCode.getMessage(), redirectUri); + String exceptionResponseJson = new ObjectMapper().writeValueAsString(exceptionResponse); + response.setStatus(authErrorCode.getHttpStatus().value()); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding("utf-8"); + PrintWriter write = response.getWriter(); + write.write(exceptionResponseJson); + write.flush(); + } + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { + throwAuthException(response, AuthErrorCode.UNAUTHORIZED, "/"); + } + + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { + throwAuthException(response, AuthErrorCode.FORBIDDEN, "/"); + } + + @Override + public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { + if (exception instanceof UnAuthorizedException) { + throwAuthException(response, ((UnAuthorizedException) exception).getErrorCode(), "/"); + } + } +} diff --git a/module-api/src/main/java/kernel/jdon/config/auth/JdonOAuth2AuthenticationSuccessHandler.java b/module-api/src/main/java/kernel/jdon/config/auth/JdonOAuth2AuthenticationSuccessHandler.java new file mode 100644 index 000000000..99eba8ef4 --- /dev/null +++ b/module-api/src/main/java/kernel/jdon/config/auth/JdonOAuth2AuthenticationSuccessHandler.java @@ -0,0 +1,53 @@ +package kernel.jdon.config.auth; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import kernel.jdon.auth.dto.JdonOAuth2User; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +import static kernel.jdon.auth.encrypt.AesUtil.encryptAESCBC; +import static kernel.jdon.auth.encrypt.HmacUtil.generateHMAC; +import static kernel.jdon.util.StringUtil.createQueryString; +import static kernel.jdon.util.StringUtil.joinToString; + +@Component +@Slf4j +public class JdonOAuth2AuthenticationSuccessHandler implements AuthenticationSuccessHandler{ + + @Override + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { + JdonOAuth2User jdonOAuth2User = (JdonOAuth2User)authentication.getPrincipal(); + if (isTemporaryUser(jdonOAuth2User)) { + String query = createUserInfoString(jdonOAuth2User.getEmail(), jdonOAuth2User.getSocialProviderType()); + String encodedQueryString = createEncryptQueryString(query); + response.sendRedirect(joinToString("http://localhost:3000/oauth/info?", encodedQueryString)); + } + } + + private boolean isTemporaryUser(JdonOAuth2User jdonOAuth2User) { + return jdonOAuth2User.getAuthorities().contains(new SimpleGrantedAuthority("ROLE_TEMPORARY_USER")); + } + + private String createUserInfoString(String email, String provider) { + return joinToString(createQueryString("email", email), createQueryString("provider", provider)); + } + + private String createEncryptQueryString(String info) { + String encoded = null; + try { + encoded = encryptAESCBC(info); + encoded = joinToString(createQueryString("value", encoded), + createQueryString("hmac", generateHMAC(encoded))); + } catch (Exception e) { + log.warn(e.getMessage(), e); + } + return encoded; + } +} diff --git a/module-api/src/main/java/kernel/jdon/config/OAuth2SecurityConfig.java b/module-api/src/main/java/kernel/jdon/config/auth/OAuth2SecurityConfig.java similarity index 53% rename from module-api/src/main/java/kernel/jdon/config/OAuth2SecurityConfig.java rename to module-api/src/main/java/kernel/jdon/config/auth/OAuth2SecurityConfig.java index 4b34da8ec..54883d383 100644 --- a/module-api/src/main/java/kernel/jdon/config/OAuth2SecurityConfig.java +++ b/module-api/src/main/java/kernel/jdon/config/auth/OAuth2SecurityConfig.java @@ -1,24 +1,16 @@ -package kernel.jdon.config; - -import static kernel.jdon.auth.encrypt.AesUtil.*; -import static kernel.jdon.auth.encrypt.HmacUtil.*; -import static kernel.jdon.util.StringUtil.*; - -import java.util.List; +package kernel.jdon.config.auth; +import kernel.jdon.auth.service.JdonOAuth2UserService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.web.SecurityFilterChain; -import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.web.cors.CorsConfiguration; -import kernel.jdon.auth.dto.JdonOAuth2User; -import kernel.jdon.auth.service.JdonOAuth2UserService; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; +import java.util.List; @Slf4j @RequiredArgsConstructor @@ -27,6 +19,8 @@ @Configuration public class OAuth2SecurityConfig { private final JdonOAuth2UserService jdonOAuth2UserService; + private final JdonOAuth2AuthenticationSuccessHandler jdonOAuth2AuthenticationSuccessHandler; + private final JdonAuthExceptionHandler jdonAuthExceptionHandler; @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { @@ -40,12 +34,17 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { return config; })); + http.exceptionHandling(exceptionConfig -> exceptionConfig + .authenticationEntryPoint(jdonAuthExceptionHandler) + .accessDeniedHandler(jdonAuthExceptionHandler)); http.csrf().disable(); http.authorizeHttpRequests(config -> config + .requestMatchers("/api/v1/member").hasAnyRole("USER") .requestMatchers("api/**").permitAll() .anyRequest().permitAll()); http.oauth2Login(oauth2Configurer -> oauth2Configurer - .successHandler(oAuth2AuthenticationSuccessHandler()) + .successHandler(jdonOAuth2AuthenticationSuccessHandler) + .failureHandler(jdonAuthExceptionHandler) .userInfoEndpoint(userInfoEndpointConfigurer -> userInfoEndpointConfigurer .userService(jdonOAuth2UserService))); http.logout(logoutConfigurer -> logoutConfigurer @@ -56,36 +55,4 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { return http.build(); } - - @Bean - public AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler() { - return ((request, response, authentication) -> { - JdonOAuth2User jdonOAuth2User = (JdonOAuth2User)authentication.getPrincipal(); - if (isTemporaryUser(jdonOAuth2User)) { - String query = createUserInfoString(jdonOAuth2User.getEmail(), jdonOAuth2User.getSocialProviderType()); - String encodedQueryString = createEncryptQueryString(query); - response.sendRedirect(joinToString("http://localhost:3000/oauth/info?", encodedQueryString)); - } - }); - } - - private boolean isTemporaryUser(JdonOAuth2User jdonOAuth2User) { - return jdonOAuth2User.getAuthorities().contains(new SimpleGrantedAuthority("ROLE_TEMPORARY_USER")); - } - - private String createUserInfoString(String email, String provider) { - return joinToString(createQueryString("email", email), createQueryString("provider", provider)); - } - - private String createEncryptQueryString(String info) { - String encoded = null; - try { - encoded = encryptAESCBC(info); - encoded = joinToString(createQueryString("value", encoded), - createQueryString("hmac", generateHMAC(encoded))); - } catch (Exception e) { - log.warn(e.getMessage(), e); - } - return encoded; - } } diff --git a/module-api/src/main/java/kernel/jdon/global/exception/UnAuthorizedException.java b/module-api/src/main/java/kernel/jdon/global/exception/UnAuthorizedException.java new file mode 100644 index 000000000..257671c3a --- /dev/null +++ b/module-api/src/main/java/kernel/jdon/global/exception/UnAuthorizedException.java @@ -0,0 +1,15 @@ +package kernel.jdon.global.exception; + +import kernel.jdon.error.ErrorCode; +import lombok.Getter; +import org.springframework.security.core.AuthenticationException; + +@Getter +public class UnAuthorizedException extends AuthenticationException { + private final transient ErrorCode errorCode; + + public UnAuthorizedException(ErrorCode errorCode) { + super(errorCode.getMessage()); + this.errorCode = errorCode; + } +} diff --git a/module-common/src/main/java/kernel/jdon/error/code/api/JobCategoryErrorCode.java b/module-api/src/main/java/kernel/jdon/jobcategory/error/JobCategoryErrorCode.java similarity index 92% rename from module-common/src/main/java/kernel/jdon/error/code/api/JobCategoryErrorCode.java rename to module-api/src/main/java/kernel/jdon/jobcategory/error/JobCategoryErrorCode.java index 41801c878..68ca976ea 100644 --- a/module-common/src/main/java/kernel/jdon/error/code/api/JobCategoryErrorCode.java +++ b/module-api/src/main/java/kernel/jdon/jobcategory/error/JobCategoryErrorCode.java @@ -1,4 +1,4 @@ -package kernel.jdon.error.code.api; +package kernel.jdon.jobcategory.error; import org.springframework.http.HttpStatus; diff --git a/module-common/src/main/java/kernel/jdon/error/code/api/MemberErrorCode.java b/module-api/src/main/java/kernel/jdon/member/error/MemberErrorCode.java similarity index 96% rename from module-common/src/main/java/kernel/jdon/error/code/api/MemberErrorCode.java rename to module-api/src/main/java/kernel/jdon/member/error/MemberErrorCode.java index 77a9dcfef..3ddc08814 100644 --- a/module-common/src/main/java/kernel/jdon/error/code/api/MemberErrorCode.java +++ b/module-api/src/main/java/kernel/jdon/member/error/MemberErrorCode.java @@ -1,4 +1,4 @@ -package kernel.jdon.error.code.api; +package kernel.jdon.member.error; import org.springframework.http.HttpStatus; diff --git a/module-api/src/main/java/kernel/jdon/member/service/MemberService.java b/module-api/src/main/java/kernel/jdon/member/service/MemberService.java index 69cd9ba53..efac37f41 100644 --- a/module-api/src/main/java/kernel/jdon/member/service/MemberService.java +++ b/module-api/src/main/java/kernel/jdon/member/service/MemberService.java @@ -14,9 +14,9 @@ import kernel.jdon.auth.dto.object.RegisterMemberDto; import kernel.jdon.auth.dto.request.RegisterRequest; import kernel.jdon.auth.encrypt.AesUtil; -import kernel.jdon.error.code.api.JobCategoryErrorCode; -import kernel.jdon.error.code.api.MemberErrorCode; -import kernel.jdon.error.code.api.SkillErrorCode; +import kernel.jdon.jobcategory.error.JobCategoryErrorCode; +import kernel.jdon.member.error.MemberErrorCode; +import kernel.jdon.skill.error.SkillErrorCode; import kernel.jdon.global.exception.ApiException; import kernel.jdon.jobcategory.domain.JobCategory; import kernel.jdon.jobcategory.repository.JobCategoryRepository; diff --git a/module-common/src/main/java/kernel/jdon/error/code/api/SkillErrorCode.java b/module-api/src/main/java/kernel/jdon/skill/error/SkillErrorCode.java similarity index 93% rename from module-common/src/main/java/kernel/jdon/error/code/api/SkillErrorCode.java rename to module-api/src/main/java/kernel/jdon/skill/error/SkillErrorCode.java index 967a2e618..559dba9b6 100644 --- a/module-common/src/main/java/kernel/jdon/error/code/api/SkillErrorCode.java +++ b/module-api/src/main/java/kernel/jdon/skill/error/SkillErrorCode.java @@ -1,4 +1,4 @@ -package kernel.jdon.error.code.api; +package kernel.jdon.skill.error; import org.springframework.http.HttpStatus; diff --git a/module-common/src/main/java/kernel/jdon/util/DateParserUtil.java b/module-common/src/main/java/kernel/jdon/util/DateParserUtil.java new file mode 100644 index 000000000..51e993e6f --- /dev/null +++ b/module-common/src/main/java/kernel/jdon/util/DateParserUtil.java @@ -0,0 +1,15 @@ +package kernel.jdon.util; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class DateParserUtil { + + public static String dateTimeToString(LocalDateTime localDateTime) { + return localDateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); + } +} \ No newline at end of file