Skip to content

Commit

Permalink
refactor: security 모듈 CustomException 정의 및 ErrorCode 정리 (#159)
Browse files Browse the repository at this point in the history
* refactor: ErrorCode 정리

* refactor: HttpResponseUtil 적용

* refactor: SecurityCustomException 생성, 적용

* refactor: GlobalExceptionHandler 리팩터링

---------

Co-authored-by: Kim Dae Hwi <[email protected]>
  • Loading branch information
2 people authored and park0jae committed Jan 8, 2024
1 parent b48d932 commit 21a59ac
Show file tree
Hide file tree
Showing 14 changed files with 116 additions and 67 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.pgms.apimember.exception;

import static com.pgms.coredomain.domain.common.GlobalErrorCode.*;

import java.util.List;

import org.springframework.http.HttpStatus;
Expand All @@ -11,8 +13,8 @@
import org.springframework.web.bind.annotation.RestControllerAdvice;

import com.pgms.coredomain.domain.common.BaseErrorCode;
import com.pgms.coredomain.domain.common.MemberErrorCode;
import com.pgms.coredomain.response.ErrorResponse;
import com.pgms.coresecurity.security.exception.SecurityCustomException;

import lombok.extern.slf4j.Slf4j;

Expand All @@ -23,13 +25,19 @@ public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
protected ResponseEntity<ErrorResponse> handleGlobalException(Exception ex) {
log.error(">>>>> Internal Server Error : {}", ex);
ErrorResponse errorResponse = new ErrorResponse("INTERNAL SERVER ERROR", ex.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(INTERNAL_SERVER_ERROR.getErrorResponse());
}

@ExceptionHandler(CustomException.class)
protected ResponseEntity<ErrorResponse> handleEventCustomException(CustomException ex) {
log.warn(">>>>> Custom Exception : {}", ex);
protected ResponseEntity<ErrorResponse> handleMemberCustomException(CustomException ex) {
log.warn(">>>>> MemberCustomException : {}", ex);
BaseErrorCode errorCode = ex.getErrorCode();
return ResponseEntity.status(errorCode.getStatus()).body(errorCode.getErrorResponse());
}

@ExceptionHandler(SecurityCustomException.class)
protected ResponseEntity<ErrorResponse> handleSecurityCustomException(SecurityCustomException ex) {
log.warn(">>>>> SecurityCustomException : {}", ex);
BaseErrorCode errorCode = ex.getErrorCode();
return ResponseEntity.status(errorCode.getStatus()).body(errorCode.getErrorResponse());
}
Expand All @@ -40,7 +48,7 @@ protected ResponseEntity<ErrorResponse> handleMethodArgumentNotValidException(Me
BindingResult bindingResult = ex.getBindingResult();

List<FieldError> fieldErrors = bindingResult.getFieldErrors();
ErrorResponse errorResponse = MemberErrorCode.VALIDATION_FAILED.getErrorResponse();
ErrorResponse errorResponse = VALIDATION_FAILED.getErrorResponse();
fieldErrors.forEach(error -> errorResponse.addValidation(error.getField(), error.getDefaultMessage()));
return ResponseEntity.status(ex.getStatusCode()).body(errorResponse);
}
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@
import com.pgms.apimember.dto.request.LoginRequest;
import com.pgms.apimember.dto.request.RefreshTokenRequest;
import com.pgms.apimember.dto.response.AuthResponse;
import com.pgms.apimember.exception.SecurityException;
import com.pgms.apimember.exception.CustomException;
import com.pgms.apimember.redis.RefreshToken;
import com.pgms.apimember.redis.RefreshTokenRepository;
import com.pgms.coredomain.domain.common.MemberErrorCode;
import com.pgms.coredomain.domain.common.SecurityErrorCode;
import com.pgms.coresecurity.security.jwt.JwtTokenProvider;
import com.pgms.coresecurity.security.service.AdminUserDetailsService;
import com.pgms.coresecurity.security.service.MemberUserDetailsService;
Expand Down Expand Up @@ -57,7 +57,7 @@ public AuthResponse login(LoginRequest request, String accountType) {
public AuthResponse refresh(RefreshTokenRequest request) {
// refresh token이 만료됐는지 확인
RefreshToken refreshToken = refreshTokenRepository.findById(request.refreshToken())
.orElseThrow(() -> new SecurityException(MemberErrorCode.REFRESH_TOKEN_EXPIRED));
.orElseThrow(() -> new CustomException(SecurityErrorCode.REFRESH_TOKEN_EXPIRED));

// 회원 정보 로드
UserDetailsImpl userDetails = loadUserDetails(refreshToken.getAccountType(), refreshToken.getEmail());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.pgms.coredomain.domain.common;

import org.springframework.http.HttpStatus;

import com.pgms.coredomain.response.ErrorResponse;

import lombok.Getter;

@Getter
public enum GlobalErrorCode implements BaseErrorCode {
INTERNAL_SERVER_ERROR("INTERNAL SERVER ERROR", HttpStatus.INTERNAL_SERVER_ERROR, "서버에 문제가 발생했습니다. 잠시 후 다시 시도해주세요."),
VALIDATION_FAILED("VALIDATION FAILED", HttpStatus.BAD_REQUEST, "입력값에 대한 검증에 실패했습니다.");

private final String errorCode;
private final HttpStatus status;
private final String message;

GlobalErrorCode(String errorCode, HttpStatus status, String message) {
this.errorCode = errorCode;
this.status = status;
this.message = message;
}

@Override
public ErrorResponse getErrorResponse() {
return new ErrorResponse(errorCode, message);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,26 +11,13 @@ public enum MemberErrorCode implements BaseErrorCode {
// ADMIN
ADMIN_NOT_FOUND("NOT FOUND", HttpStatus.NOT_FOUND, "존재하지 않는 관리자입니다."),
DUPLICATED_ADMIN_EMAIL("DUPLICATED ADMIN EMAIL", HttpStatus.BAD_REQUEST, "이미 존재하는 관리자 이메일입니다."),
NOT_AUTHORIZED("NOT AUTHORIZED", HttpStatus.UNAUTHORIZED, "접근 권한이 없습니다."),
ADMIN_ROLE_NOT_FOUND("ADMIN ROLE NOT FOUND", HttpStatus.NOT_FOUND, "존재하지 않는 관리자 역할입니다."),
DUPLICATED_ROLE("DUPLICATED ROLE", HttpStatus.BAD_REQUEST, "이미 존재하는 관리자 역할입니다."),
ROLE_IN_USE("ROLE IN USE", HttpStatus.BAD_REQUEST, "사용중인 역할입니다."),
ADMIN_PERMISSION_NOT_FOUND("ADMIN PERMISSION NOT FOUND", HttpStatus.NOT_FOUND, "존재하지 않는 관리자 권한입니다."),
DUPLICATED_PERMISSION("DUPLICATED PERMISSION", HttpStatus.BAD_REQUEST, "이미 존재하는 권한입니다"),
PERMISSION_ASSIGNED("PERMISSION ASSIGNED", HttpStatus.BAD_REQUEST, "이미 할당된 권한은 삭제할 수 없습니다."),
ROLE_PERMISSION_NOT_FOUND("ROLE PERMISSION NOT FOUND", HttpStatus.BAD_REQUEST, "존재하지 않는 역할 권한입니다."),

// MEMBER
MEMBER_NOT_FOUND("NOT FOUND", HttpStatus.NOT_FOUND, "존재하지 않는 회원입니다."),
DUPLICATED_MEMBER_EMAIL("DUPLICATED MEMBER EMAIL", HttpStatus.BAD_REQUEST, "이미 존재하는 회원 이메일입니다."),
MEMBER_ALREADY_DELETED("MEMBER ALREADY DELETED", HttpStatus.BAD_REQUEST, "이미 탈퇴한 회원입니다."),
PASSWORD_NOT_MATCHED("PASSWORD NOT MATCHED", HttpStatus.BAD_REQUEST, "비밀번호가 일치하지 않습니다."),
PASSWORD_CONFIRM_NOT_MATCHED("PASSWORD CONFIRM NOT MATCHED", HttpStatus.BAD_REQUEST, "비밀번호 확인이 일치하지 않습니다."),
VALIDATION_FAILED("VALIDATION FAILED", HttpStatus.BAD_REQUEST, "입력값에 대한 검증에 실패했습니다."),

// security
UNAUTHORIZED("UNAUTHORIZED", HttpStatus.UNAUTHORIZED, "로그인 해주세요."),
REFRESH_TOKEN_EXPIRED("REFRESH TOKEN EXPIRED", HttpStatus.UNAUTHORIZED, "다시 로그인 해주세요.");
PASSWORD_CONFIRM_NOT_MATCHED("PASSWORD CONFIRM NOT MATCHED", HttpStatus.BAD_REQUEST, "비밀번호 확인이 일치하지 않습니다.");

private final String errorCode;
private final HttpStatus status;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.pgms.coredomain.domain.common;

import org.springframework.http.HttpStatus;

import com.pgms.coredomain.response.ErrorResponse;

import lombok.Getter;

@Getter
public enum SecurityErrorCode implements BaseErrorCode {
UNAUTHORIZED("UNAUTHORIZED", HttpStatus.UNAUTHORIZED, "로그인 해주세요."),
ACCESS_TOKEN_EXPIRED("ACCESS TOKEN EXPIRED", HttpStatus.UNAUTHORIZED, "토큰이 만료되었습니다"),
REFRESH_TOKEN_EXPIRED("REFRESH TOKEN EXPIRED", HttpStatus.UNAUTHORIZED, "다시 로그인 해주세요."),
FORBIDDEN("FORBIDDEN", HttpStatus.FORBIDDEN, "권한이 없습니다");

private final String errorCode;
private final HttpStatus status;
private final String message;

SecurityErrorCode(String errorCode, HttpStatus status, String message) {
this.errorCode = errorCode;
this.status = status;
this.message = message;
}

@Override
public ErrorResponse getErrorResponse() {
return new ErrorResponse(errorCode, message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.pgms.coresecurity.security.exception;

import com.pgms.coredomain.domain.common.BaseErrorCode;

import lombok.Getter;

@Getter
public class SecurityCustomException extends RuntimeException {

private final BaseErrorCode errorCode;

public SecurityCustomException(BaseErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
}
}
Original file line number Diff line number Diff line change
@@ -1,41 +1,32 @@
package com.pgms.coresecurity.security.jwt;

import static com.pgms.coredomain.domain.common.SecurityErrorCode.*;

import java.io.IOException;

import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.pgms.coredomain.response.ErrorResponse;
import com.pgms.coresecurity.security.util.HttpResponseUtil;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

/**
* 인증된 사용자가 필요한 권한없이 접근하려고 할 때 발생하는 예외 처리
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtAccessDeniedHandler implements AccessDeniedHandler {

private final ObjectMapper objectMapper;

@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException {
log.warn("Access Denied: ", accessDeniedException);

ErrorResponse errorResponse = new ErrorResponse("FORBIDDEN", "권한이 없습니다.");

response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setStatus(HttpStatus.FORBIDDEN.value());
response.setCharacterEncoding("UTF-8");
objectMapper.writeValue(response.getOutputStream(), errorResponse);
HttpResponseUtil.setErrorResponse(response, HttpStatus.FORBIDDEN, FORBIDDEN.getErrorResponse());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,25 @@
import java.io.IOException;

import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.pgms.coredomain.domain.common.SecurityErrorCode;
import com.pgms.coredomain.response.ErrorResponse;
import com.pgms.coresecurity.security.util.HttpResponseUtil;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

/**
* 사용자가 인증되지 않은 상태에서 접근하려고 할 때 발생하는 예외 처리
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {

private final ObjectMapper objectMapper;

@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException)
Expand All @@ -34,15 +30,11 @@ public void commence(HttpServletRequest request, HttpServletResponse response,

ErrorResponse errorResponse;
if (request.getAttribute("expired") != null) {
errorResponse = new ErrorResponse("ACCESS_TOKEN_EXPIRED", "토큰이 만료되었습니다.");
errorResponse = SecurityErrorCode.ACCESS_TOKEN_EXPIRED.getErrorResponse();
} else {
errorResponse = new ErrorResponse("UNAUTHORIZED", "로그인 해주세요.");
errorResponse = SecurityErrorCode.UNAUTHORIZED.getErrorResponse();
}

response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setCharacterEncoding("UTF-8");
objectMapper.writeValue(response.getOutputStream(), errorResponse);
HttpResponseUtil.setErrorResponse(response, HttpStatus.UNAUTHORIZED, errorResponse);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

import com.pgms.coredomain.domain.common.SecurityErrorCode;
import com.pgms.coresecurity.security.exception.SecurityCustomException;
import com.pgms.coresecurity.security.service.UserDetailsImpl;

public class CurrentAccountArgumentResolver implements HandlerMethodArgumentResolver {
Expand All @@ -32,7 +34,7 @@ public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer m

private void checkAuthenticated(Authentication authentication) {
if (Objects.isNull(authentication)) {
throw new RuntimeException("인증되지 않은 요청입니다.");
throw new SecurityCustomException(SecurityErrorCode.UNAUTHORIZED);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import com.pgms.coredomain.domain.common.MemberErrorCode;
import com.pgms.coredomain.domain.member.Admin;
import com.pgms.coredomain.domain.member.repository.AdminRepository;
import com.pgms.coresecurity.security.exception.SecurityCustomException;

import lombok.RequiredArgsConstructor;

Expand All @@ -21,7 +23,7 @@ public class AdminUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
Admin admin = adminRepository.findByEmail(email)
.orElseThrow(() -> new RuntimeException("해당 어드민이 존재하지 않습니다."));
.orElseThrow(() -> new SecurityCustomException(MemberErrorCode.ADMIN_NOT_FOUND));

return UserDetailsImpl.from(admin);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

import com.pgms.coredomain.domain.common.MemberErrorCode;
import com.pgms.coresecurity.security.exception.SecurityCustomException;

@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {

Expand Down Expand Up @@ -39,7 +42,7 @@ public Authentication authenticate(Authentication authentication) throws Authent
}

if (!passwordEncoder.matches(password, userDetails.getPassword())) {
throw new RuntimeException("비밀번호가 일치하지 않습니다.");
throw new SecurityCustomException(MemberErrorCode.PASSWORD_NOT_MATCHED);
}

return new UsernamePasswordAuthenticationToken(userDetails, password, userDetails.getAuthorities());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import com.pgms.coredomain.domain.common.MemberErrorCode;
import com.pgms.coredomain.domain.member.Member;
import com.pgms.coredomain.domain.member.repository.MemberRepository;
import com.pgms.coresecurity.security.exception.SecurityCustomException;

import lombok.RequiredArgsConstructor;

Expand All @@ -21,7 +23,7 @@ public class MemberUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
Member member = memberRepository.findByEmail(email)
.orElseThrow(() -> new RuntimeException("해당 멤버가 존재하지 않습니다."));
.orElseThrow(() -> new SecurityCustomException(MemberErrorCode.MEMBER_NOT_FOUND));

return UserDetailsImpl.from(member);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@

import com.fasterxml.jackson.databind.ObjectMapper;
import com.pgms.coredomain.response.ApiResponse;
import com.pgms.coredomain.response.ErrorResponse;

import jakarta.servlet.http.HttpServletResponse;
import lombok.NoArgsConstructor;
Expand All @@ -28,13 +27,11 @@ public static void setSuccessResponse(HttpServletResponse response, HttpStatus h
response.getWriter().write(responseBody);
}

public static void setErrorResponse(HttpServletResponse response, HttpStatus httpStatus)
public static void setErrorResponse(HttpServletResponse response, HttpStatus httpStatus, Object body)
throws IOException {
String responseBody = objectMapper.writeValueAsString(
new ErrorResponse(httpStatus.name(), httpStatus.getReasonPhrase()));
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setStatus(httpStatus.value());
response.setCharacterEncoding("UTF-8");
response.getWriter().write(responseBody);
objectMapper.writeValue(response.getOutputStream(), body);
}
}

0 comments on commit 21a59ac

Please sign in to comment.