🔧 Add 보안점검에 공통 모듈 포함

This commit is contained in:
kimjaeyeol
2021-11-09 09:30:19 +09:00
parent 13f4a6b1e9
commit 13a6c6f0b1
40 changed files with 2568 additions and 0 deletions

View File

@@ -0,0 +1,38 @@
package org.egovframe.cloud.common.config;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.InitBinder;
/**
* org.egovframe.cloud.common.config.WebControllerAdvice
*
* 모든 컨트롤러에 적용되는 컨트롤러 어드바이스 클래스
* 예외 처리 (@ExceptionHandler), 바인딩 설정(@InitBinder), 모델 객체(@ModelAttributes)
*
* @author 표준프레임워크센터 jooho
* @version 1.0
* @since 2021/07/12
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/07/12 jooho 최초 생성
* </pre>
*/
@ControllerAdvice
public class ApiControllerAdvice {
/**
* 모든 컨트롤러로 들어오는 요청 초기화
*
* @param binder 웹 데이터 바인더
*/
@InitBinder
public void initBinder(WebDataBinder binder) {
binder.initDirectFieldAccess(); // Setter 구현 없이 DTO 클래스 필드에 접근
}
}

View File

@@ -0,0 +1,32 @@
package org.egovframe.cloud.common.config;
/**
* org.egovframe.cloud.common.config.Constants
*
* 공통 전역 상수 정의
*
* @author 표준프레임워크센터 jaeyeolkim
* @version 1.0
* @since 2021/07/19
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/07/19 jaeyeolkim 최초 생성
* </pre>
*/
public interface GlobalConstant {
final String HEADER_SITE_ID = "X-Site-Id"; // header에 어떤 사이트에서 보내는 요청인지 구분하기 위한 정보
final String AUTHORIZATION_URI = "/api/v1/authorizations/check";
final String REFRESH_TOKEN_URI = "/api/v1/users/token/refresh";
final String MESSAGES_URI = "/api/v1/messages/**";
final String LOGIN_URI = "/login";
final String[] SECURITY_PERMITALL_ANTPATTERNS = {AUTHORIZATION_URI, REFRESH_TOKEN_URI, MESSAGES_URI, LOGIN_URI, "/actuator/**", "/v3/api-docs/**", "/api/v1/images/**", "/swagger-ui.html"};
final String USER_SERVICE_URI = "/user-service";
//예약 신청 후 재고 변경 성공여부 exchange name
final String SUCCESS_OR_NOT_EX_NAME = "success-or-not.direct";
// 첨부파일 저장 후 entity 정보 update binding name
final String ATTACHMENT_ENTITY_BINDING_NAME = "attachmentEntity-out-0";
}

View File

@@ -0,0 +1,34 @@
package org.egovframe.cloud.common.config;
import lombok.extern.slf4j.Slf4j;
import org.egovframe.rte.fdl.cmmn.trace.LeaveaTrace;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* org.egovframe.cloud.common.config.LeaveaTraceConfig
* <p>
* LeaveaTrace Bean 설정
* EgovAbstractServiceImpl 클래스가 의존한다.
*
* @author 표준프레임워크센터 jaeyeolkim
* @version 1.0
* @since 2021/09/24
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/09/24 jaeyeolkim 최초 생성
* </pre>
*/
@Configuration
public class LeaveaTraceConfig {
@Bean
public LeaveaTrace leaveaTrace() {
return new LeaveaTrace();
}
}

View File

@@ -0,0 +1,63 @@
package org.egovframe.cloud.common.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.util.StringUtils;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.Paths;
/**
* org.egovframe.cloud.common.config.MessageSourceConfig
* <p>
* Spring MessageSource 설정
* Message Domain 이 있는 portal-service 에서 messages.properties 를 외부 위치에 생성한다.
* 각 서비스에서 해당 파일을 통해 다국어를 지원하도록 한다.
*
* @author 표준프레임워크센터 jaeyeolkim
* @version 1.0
* @since 2021/08/09
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/08/09 jaeyeolkim 최초 생성
* </pre>
*/
@Slf4j
@Configuration
public class MessageSourceConfig {
@Value("${messages.directory}")
private String messagesDirectory;
@Value("${spring.profiles.active:default}")
private String profile;
@Bean
public MessageSource messageSource() {
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
final String MESSAGES = "/messages";
if ("default".equals(profile)) {
Path fileStorageLocation = Paths.get(messagesDirectory).toAbsolutePath().normalize();
String dbMessages = StringUtils.cleanPath("file://" + fileStorageLocation + MESSAGES);
messageSource.setBasenames(dbMessages);
} else {
messageSource.setBasenames(messagesDirectory + MESSAGES);
}
messageSource.getBasenameSet().forEach(s -> log.info("messageSource getBasenameSet={}", s));
messageSource.setCacheSeconds(60); // 메세지 파일 변경 감지 간격
messageSource.setUseCodeAsDefaultMessage(true); // 메세지가 없으면 코드를 메세지로 한다
messageSource.setDefaultEncoding(StandardCharsets.UTF_8.name());
return messageSource;
}
}

View File

@@ -0,0 +1,36 @@
package org.egovframe.cloud.common.config;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.servers.Server;
@Configuration
public class OpenApiDocsConfig {
@Value("${spring.application.name}")
private String appName;
/**
* @TODO
* api info update 필요
*
*/
@Bean
public OpenAPI customOpenAPI() {
Server server = new Server();
server.url("/"+appName);
List<Server> servers = new ArrayList<>();
servers.add(server);
return new OpenAPI()
.components(new Components())
.servers(servers)
.info(new io.swagger.v3.oas.models.info.Info().title(appName+" API"));
}
}

View File

@@ -0,0 +1,47 @@
package org.egovframe.cloud.common.domain;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.Arrays;
/**
* org.egovframe.cloud.common.domain.Role
* <p>
* 사용자 권한
*
* @author 표준프레임워크센터 jaeyeolkim
* @version 1.0
* @since 2021/06/30
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/06/30 jaeyeolkim 최초 생성
* </pre>
*/
@Getter
@RequiredArgsConstructor
public enum Role {
// 스프링 시큐리티에서는 권한 코드에 항상 ROLE_ 이 앞에 있어야 한다.
ANONYMOUS("ROLE_ANONYMOUS", "손님"),
USER("ROLE_USER", "일반 사용자"),
EMPLOYEE("ROLE_EMPLOYEE", "내부 사용자"),
ADMIN("ROLE_ADMIN", "시스템 관리자");
private final String key;
private final String title;
/**
* 권한 id로 상수 검색
*
* @param key 권한 id
* @return Role 권한 상수
*/
public static Role findByKey(String key) {
return Arrays.stream(Role.values()).filter(c -> c.getKey().equals(key)).findAny().orElse(null);
}
}

View File

@@ -0,0 +1,21 @@
package org.egovframe.cloud.common.dto;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@NoArgsConstructor
public class AttachmentEntityMessage {
private String attachmentCode;
private String entityName;
private String entityId;
@Builder
public AttachmentEntityMessage(String attachmentCode, String entityName, String entityId) {
this.attachmentCode = attachmentCode;
this.entityName = entityName;
this.entityId = entityId;
}
}

View File

@@ -0,0 +1,30 @@
package org.egovframe.cloud.common.dto;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
/**
* org.egovframe.cloud.common.dto.RequestDto
* <p>
* 공통 조회조건 요청 파라미터 dto
*
* @author 표준프레임워크센터 jaeyeolkim
* @version 1.0
* @since 2021/07/15
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/07/15 jaeyeolkim 최초 생성
* </pre>
*/
@Getter
@NoArgsConstructor
@SuperBuilder
public class RequestDto {
private String keywordType; // 검색조건
private String keyword; // 검색어
}

View File

@@ -0,0 +1,70 @@
package org.egovframe.cloud.common.exception;
import org.egovframe.cloud.common.exception.dto.ErrorCode;
/**
* org.egovframe.cloud.common.exception.BusinessException
* <p>
* 런타임시 비즈니스 로직상 사용자에게 알려줄 오류 메시지를 만들어 던지는 처리를 담당한다
* 이 클래스를 상속하여 다양한 형태의 business exception 을 만들 수 있고,
* 그것들은 모두 ExceptionHandlerAdvice BusinessException 처리 메소드에서 잡아낸다.
* 상황에 맞게 에러 코드를 추가하고 이 클래스를 상속하여 사용할 수 있다.
*
* @author 표준프레임워크센터 jaeyeolkim
* @version 1.0
* @since 2021/07/16
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/07/16 jaeyeolkim 최초 생성
* </pre>
*/
public class BusinessException extends RuntimeException {
private String customMessage;
private ErrorCode errorCode;
/**
* 사용자 정의 메시지를 받아 처리하는 경우
*
* @param errorCode 400 에러
* @param customMessage 사용자에게 표시할 메시지
*/
public BusinessException(ErrorCode errorCode, String customMessage) {
super(customMessage);
this.errorCode = errorCode;
this.customMessage = customMessage;
}
/**
* 사전 정의된 에러코드 객체를 넘기는 경우
*
* @param message 서버에 남길 메시지
* @param errorCode 사전 정의된 에러코드
*/
public BusinessException(String message, ErrorCode errorCode) {
super(message);
this.errorCode = errorCode;
}
/**
* 사전 정의된 에러코드의 메시지를 서버에 남기고 에러코드 객체를 리턴한다
* @param errorCode 사전 정의된 에러코드
*/
public BusinessException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
}
public ErrorCode getErrorCode() {
return errorCode;
}
public String getCustomMessage() {
return customMessage;
}
}

View File

@@ -0,0 +1,33 @@
package org.egovframe.cloud.common.exception;
import org.egovframe.cloud.common.exception.dto.ErrorCode;
/**
* org.egovframe.cloud.common.exception.BusinessMessageException
* <p>
* 런타임시 비즈니스 로직상 사용자에게 알려줄 오류 메시지를 만들어 던지는 처리를 담당한다
*
* @author 표준프레임워크센터 jaeyeolkim
* @version 1.0
* @since 2021/07/28
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/07/28 jaeyeolkim 최초 생성
* </pre>
*/
public class BusinessMessageException extends BusinessException {
/**
* 사용자에게 표시될 메시지와 상태코드 400 을 넘긴다
*
* @param customMessage
*/
public BusinessMessageException(String customMessage) {
super(ErrorCode.BUSINESS_CUSTOM_MESSAGE, customMessage);
}
}

View File

@@ -0,0 +1,28 @@
package org.egovframe.cloud.common.exception;
import org.egovframe.cloud.common.exception.dto.ErrorCode;
/**
* org.egovframe.cloud.common.exception.EntityNotFoundException
* <p>
* 요청한 엔티티를 찾을 수 없을 경우 사용자에게 알려준다.
* ExceptionHandlerAdvice BusinessException 처리 메소드에서 잡아낸다.
*
* @author 표준프레임워크센터 jaeyeolkim
* @version 1.0
* @since 2021/07/16
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/07/16 jaeyeolkim 최초 생성
* </pre>
*/
public class EntityNotFoundException extends BusinessException {
public EntityNotFoundException(String message) {
super(message, ErrorCode.ENTITY_NOT_FOUND);
}
}

View File

@@ -0,0 +1,32 @@
package org.egovframe.cloud.common.exception;
import org.egovframe.cloud.common.exception.dto.ErrorCode;
/**
* org.egovframe.cloud.common.exception.InvalidValueException
* <p>
* 입력 받은 값이 잘못된 경우 사용자에게 알려준다.
* ExceptionHandlerAdvice BusinessException 처리 메소드에서 잡아낸다.
*
* @author 표준프레임워크센터 jaeyeolkim
* @version 1.0
* @since 2021/07/16
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/07/16 jaeyeolkim 최초 생성
* </pre>
*/
public class InvalidValueException extends BusinessException {
public InvalidValueException(String value) {
super(value, ErrorCode.INVALID_INPUT_VALUE);
}
public InvalidValueException(String value, ErrorCode errorCode) {
super(value, errorCode);
}
}

View File

@@ -0,0 +1,65 @@
package org.egovframe.cloud.common.exception.dto;
/**
* org.egovframe.cloud.common.exception.dto.ErrorCode
* <p>
* REST API 요청에 대한 오류 반환값을 정의
* ErrorResponse 클래스에서 status, code, message 세 가지 속성을 의존한다
* message 는 MessageSource 의 키 값을 정의하여 다국어 처리를 지원한다
*
* @author 표준프레임워크센터 jaeyeolkim
* @version 1.0
* @since 2021/07/16
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/07/16 jaeyeolkim 최초 생성
* </pre>
*/
public enum ErrorCode {
INVALID_INPUT_VALUE(400, "E001", "err.invalid.input.value"), // Bad Request
INVALID_TYPE_VALUE(400, "E002", "err.invalid.type.value"), // Bad Request
ENTITY_NOT_FOUND(400, "E003", "err.entity.not.found"), // Bad Request
UNAUTHORIZED(401, "E004", "err.unauthorized"), // The request requires an user authentication
JWT_EXPIRED(401, "E005", "err.unauthorized"), // The request requires an user authentication
ACCESS_DENIED(403, "E006", "err.access.denied"), // Forbidden, Access is Denied
NOT_FOUND(404, "E010", "err.page.not.found"), // Not found
METHOD_NOT_ALLOWED(405, "E011", "err.method.not.allowed"), // 요청 방법이 서버에 의해 알려졌으나, 사용 불가능한 상태
REQUIRE_USER_JOIN(412, "E012", "err.user.notexists"), // Server Error
UNPROCESSABLE_ENTITY(422, "E020", "err.unprocessable.entity"), // Unprocessable Entity
INTERNAL_SERVER_ERROR(500, "E999", "err.internal.server"), // Server Error
// business error code
BUSINESS_CUSTOM_MESSAGE(400, "B001", ""), // 사용자 정의 메시지를 넘기는 business exception
DUPLICATE_INPUT_INVALID(400, "B002", "err.duplicate.input.value"), // 중복된 값을 입력하였습니다
DB_CONSTRAINT_DELETE(400, "B003", "err.duplicate.input.value") // 참조하는 데이터가 있어서 삭제할 수 없습니다
;
private final int status;
private final String code;
private final String message;
ErrorCode(final int status, final String code, final String message) {
this.status = status;
this.code = code;
this.message = message;
}
public int getStatus() {
return status;
}
public String getCode() {
return code;
}
public String getMessage() {
return message;
}
}

View File

@@ -0,0 +1,172 @@
package org.egovframe.cloud.common.exception.dto;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.validation.BindingResult;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import static lombok.AccessLevel.PROTECTED;
/**
* org.egovframe.cloud.common.exception.ErrorResponse
* <p>
* 일관된 예외처리를 제공하는 클래스
* https://github.com/cheese10yun/spring-guide
*
* @author 표준프레임워크센터 jooho
* @version 1.0
* @since 2021/07/16
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/07/16 jaeyeolkim 최초 생성
* </pre>
*/
@Getter
@NoArgsConstructor(access = PROTECTED)
public class ErrorResponse {
private LocalDateTime timestamp;
private String message;
private int status;
private String code;
private List<FieldError> errors;
private static final String DEFAULT_ERROR_MESSAGE = "ERROR";
private ErrorResponse(final ErrorCode code, final List<FieldError> errors, MessageSource messageSource) {
this.timestamp = LocalDateTime.now();
this.message = messageSource.getMessage(code.getMessage(), new Object[]{}, DEFAULT_ERROR_MESSAGE, LocaleContextHolder.getLocale());
this.status = code.getStatus();
this.code = code.getCode();
this.errors = errors;
}
private ErrorResponse(final ErrorCode code, MessageSource messageSource) {
this.timestamp = LocalDateTime.now();
this.message = messageSource.getMessage(code.getMessage(), new Object[]{}, DEFAULT_ERROR_MESSAGE, LocaleContextHolder.getLocale());
this.status = code.getStatus();
this.code = code.getCode();
this.errors = new ArrayList<>();
}
private ErrorResponse(final ErrorCode code, String customMessage) {
this.timestamp = LocalDateTime.now();
this.message = customMessage;
this.status = code.getStatus();
this.code = code.getCode();
this.errors = new ArrayList<>();
}
/**
* 사용자 정의 메시지를 받아 넘기는 경우
*
* @param code
* @param customMessage
* @return
*/
public static ErrorResponse of(final ErrorCode code, final String customMessage) {
return new ErrorResponse(code, customMessage);
}
/**
* ErrorResponse 는 protected 를 선언하여 new 생성할 수 없도록 막아두고 static 메소드를 통해 생성할 수 있도록 하였다
* ExceptionHandlerAdvice 에서 인자를 받아 ErrorResponse 객체를 생성한다
*
* @param code
* @param bindingResult
* @param messageSource
* @return
*/
public static ErrorResponse of(final ErrorCode code, final BindingResult bindingResult, MessageSource messageSource) {
return new ErrorResponse(code, FieldError.of(bindingResult), messageSource);
}
/**
* ErrorResponse 는 protected 를 선언하여 new 생성할 수 없도록 막아두고 static 메소드를 통해 생성할 수 있도록 하였다
* ExceptionHandlerAdvice 에서 인자를 받아 ErrorResponse 객체를 생성한다
*
* @param code
* @param messageSource
* @return
*/
public static ErrorResponse of(final ErrorCode code, MessageSource messageSource) {
return new ErrorResponse(code, messageSource);
}
/**
* ErrorResponse 는 protected 를 선언하여 new 생성할 수 없도록 막아두고 static 메소드를 통해 생성할 수 있도록 하였다
* ExceptionHandlerAdvice 에서 인자를 받아 ErrorResponse 객체를 생성한다
*
* @param code
* @param errors
* @param messageSource
* @return
*/
public static ErrorResponse of(final ErrorCode code, final List<FieldError> errors, MessageSource messageSource) {
return new ErrorResponse(code, errors, messageSource);
}
/**
* ErrorResponse 는 protected 를 선언하여 new 생성할 수 없도록 막아두고 static 메소드를 통해 생성할 수 있도록 하였다
* ExceptionHandlerAdvice 에서 인자를 받아 ErrorResponse 객체를 생성한다
* java validator 에러 발생 시 에러 정보 중 필요한 내용만 FieldError 로 반환한다
*
* @param e
* @return
*/
public static ErrorResponse of(MethodArgumentTypeMismatchException e, MessageSource messageSource) {
final String value = e.getValue() == null ? "" : e.getValue().toString();
final List<ErrorResponse.FieldError> errors = ErrorResponse.FieldError.of(e.getName(), value, e.getErrorCode());
return new ErrorResponse(ErrorCode.INVALID_TYPE_VALUE, errors, messageSource);
}
/**
* java validator 에러 발생 시 에러 정보 중 필요한 내용만 반환한다
*/
@Getter
@NoArgsConstructor(access = PROTECTED)
public static class FieldError {
private String message;
private String field;
private String rejectedValue;
private FieldError(final String field, final String rejectedValue, final String message) {
this.field = field;
this.rejectedValue = rejectedValue;
this.message = message;
}
public static List<FieldError> of(final String field, final String rejectedValue, final String message) {
List<FieldError> fieldErrors = new ArrayList<>();
fieldErrors.add(new FieldError(field, rejectedValue, message));
return fieldErrors;
}
/**
* BindingResult to FieldError
*
* @param bindingResult
* @return
*/
private static List<FieldError> of(final BindingResult bindingResult) {
final List<org.springframework.validation.FieldError> fieldErrors = bindingResult.getFieldErrors();
return fieldErrors.stream()
.map(error -> new FieldError(
error.getField(),
error.getRejectedValue() == null ? "" : error.getRejectedValue().toString(),
error.getDefaultMessage()))
.collect(Collectors.toList());
}
}
}

View File

@@ -0,0 +1,69 @@
package org.egovframe.cloud.common.service;
import static org.egovframe.cloud.common.config.GlobalConstant.*;
import org.egovframe.cloud.common.dto.AttachmentEntityMessage;
import org.egovframe.cloud.common.util.MessageUtil;
import org.egovframe.rte.fdl.cmmn.EgovAbstractServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.stream.function.StreamBridge;
import org.springframework.messaging.support.MessageBuilder;
import javax.annotation.Resource;
/**
* org.egovframe.cloud.common.service.AbstractService
* <p>
* 표준프레임워크 EgovAbstractServiceImpl 을 상속하는 공통 추상 클래스이다.
* 각 @Service 클래스는 이 클래스를 반드시 상속하여야 한다.(표준프레임워크 준수사항)
*
* @author 표준프레임워크센터 jaeyeolkim
* @version 1.0
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/07/28 jaeyeolkim 최초 생성
* </pre>
* @since 2021/07/28
*/
public abstract class AbstractService extends EgovAbstractServiceImpl {
@Resource(name = "messageUtil")
protected MessageUtil messageUtil;
/**
* messageSource 에 코드값을 넘겨 메시지를 찾아 리턴한다.
*
* @param code
* @return
*/
protected String getMessage(String code) {
return messageUtil.getMessage(code);
}
/**
* messageSource 에 코드값과 인자를 넘겨 메시지를 찾아 리턴한다.
*
* @param code
* @param args
* @return
*/
protected String getMessage(String code, Object[] args) {
return messageUtil.getMessage(code, args);
}
/**
* 게시물 저장 후 해당 정보를 첨부파일 entity에 입력하기 위해
* 이벤트 메세지 발행
*
* @param entityMessage
*/
protected void sendAttachmentEntityInfo(StreamBridge streamBridge, AttachmentEntityMessage entityMessage) {
streamBridge.send(ATTACHMENT_ENTITY_BINDING_NAME,
MessageBuilder.withPayload(entityMessage).build());
}
}

View File

@@ -0,0 +1,84 @@
package org.egovframe.cloud.common.util;
import org.egovframe.cloud.common.config.GlobalConstant;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
/**
* org.egovframe.cloud.common.util.LogUtil
* <p>
* 로그인, 접속 로그 입력 시 필요한 정보 제공
*
* @author 표준프레임워크센터 jaeyeolkim
* @version 1.0
* @since 2021/09/02
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/09/02 jaeyeolkim 최초 생성
* </pre>
*/
public class LogUtil {
/**
* 클라이언트 사용자의 IP 가져오기
*
* @return
*/
public static String getUserIp() {
String ip = null;
HttpServletRequest request = ((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest();
ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Real-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-RealIP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("REMOTE_ADDR");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}
/**
* 접속 사이트 정보를 넘긴다.
*
* @param request
* @return
*/
public static Long getSiteId(HttpServletRequest request) {
String header = request.getHeader(GlobalConstant.HEADER_SITE_ID);
if (!StringUtils.hasLength(header)) {
return null;
}
return Long.valueOf(header);
}
}

View File

@@ -0,0 +1,66 @@
package org.egovframe.cloud.common.util;
import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.Locale;
/**
* org.egovframe.cloud.common.util.MessageUtil
* <p>
* MessageSource 값을 읽을 수 있는 메소드를 제공한다.
*
* @author 표준프레임워크센터 jaeyeolkim
* @version 1.0
* @since 2021/09/08
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/09/08 jaeyeolkim 최초 생성
* </pre>
*/
@Component
public class MessageUtil {
@Resource(name = "messageSource")
private MessageSource messageSource;
/**
* messageSource 에 코드값을 넘겨 메시지를 찾아 리턴한다.
*
* @param code
* @return
*/
public String getMessage(String code) {
return this.getMessage(code, new Object[]{});
}
/**
* messageSource 에 코드값과 인자를 넘겨 메시지를 찾아 리턴한다.
*
* @param code
* @param args
* @return
*/
public String getMessage(String code, Object[] args) {
return this.getMessage(code, args, LocaleContextHolder.getLocale());
}
/**
* messageSource 에 코드값, 인자, 지역정보를 넘겨 메시지를 찾아 리턴한다.
*
* @param code
* @param args
* @param locale
* @return
*/
public String getMessage(String code, Object[] args, Locale locale) {
return messageSource.getMessage(code, args, locale);
}
}

View File

@@ -0,0 +1,99 @@
package org.egovframe.cloud.reactive.config;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.ArrayList;
import java.util.List;
/**
* org.egovframe.cloud.reserveitemservice.config.AuthenticationConverter
*
* 요청을 authentiation으로 변환하는 클래스
* AuthenticationWebFilter에서 호출됨.
*
* @author 표준프레임워크센터 shinmj
* @version 1.0
* @since 2021/09/06
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/09/06 shinmj 최초 생성
* </pre>
*/
@Slf4j
@Component
public class AuthenticationConverter implements ServerAuthenticationConverter {
@Value("${token.secret}")
private String TOKEN_SECRET;
final String TOKEN_CLAIM_NAME = "authorities";
/**
* 요청에 담긴 토큰을 조회하여 Authentication 정보를 설정한다.
*
* @param exchange
* @return
*/
@Override
public Mono<Authentication> convert(ServerWebExchange exchange) {
return Mono.justOrEmpty(exchange)
.flatMap(e -> Mono.justOrEmpty(e.getRequest().getHeaders().get(HttpHeaders.AUTHORIZATION)))
.flatMap(auth -> {
if (auth == null) {
return Mono.empty();
}
String token = auth.get(0);
if (!StringUtils.hasText(token) || "undefined".equals(token)) {
return Mono.empty();
}
Claims claims = getClaimsFromToken(token);
String authorities = claims.get(TOKEN_CLAIM_NAME, String.class);
List<SimpleGrantedAuthority> roleList = new ArrayList<>();
roleList.add(new SimpleGrantedAuthority(authorities));
String username = claims.getSubject();
if (username == null) {
ReactiveSecurityContextHolder.withAuthentication(null);
return Mono.empty();
}
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(username, null, roleList);
ReactiveSecurityContextHolder.withAuthentication(authenticationToken);
return Mono.just(authenticationToken);
});
}
/**
* AuthenticationFilter.doFilter 메소드에서 UsernamePasswordAuthenticationToken 정보를 세팅할 때 호출된다.
*
* @param token
* @return
*/
public Claims getClaimsFromToken(String token) {
return Jwts.parser()
.setSigningKey(TOKEN_SECRET)
.parseClaimsJws(token)
.getBody();
}
}

View File

@@ -0,0 +1,47 @@
package org.egovframe.cloud.reactive.config;
import io.r2dbc.spi.ConnectionFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.r2dbc.config.EnableR2dbcAuditing;
import org.springframework.r2dbc.connection.init.CompositeDatabasePopulator;
import org.springframework.r2dbc.connection.init.ConnectionFactoryInitializer;
import org.springframework.r2dbc.connection.init.ResourceDatabasePopulator;
/**
* org.egovframe.cloud.reserveitemservice.config.R2dbcConfig
*
* R2DBC configuration class
*
* @author 표준프레임워크센터 shinmj
* @version 1.0
* @since 2021/09/06
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/09/06 shinmj 최초 생성
* </pre>
*/
@Profile("!test")
@Configuration
@EnableR2dbcAuditing(auditorAwareRef = "userAuditAware") //auditing
public class R2dbcConfig {
@Bean
public ConnectionFactoryInitializer initializer(ConnectionFactory connectionFactory) {
ConnectionFactoryInitializer initializer = new ConnectionFactoryInitializer();
initializer.setConnectionFactory(connectionFactory);
CompositeDatabasePopulator populator = new CompositeDatabasePopulator();
populator.addPopulators(new ResourceDatabasePopulator(new ClassPathResource("schema.sql")));
initializer.setDatabasePopulator(populator);
return initializer;
}
}

View File

@@ -0,0 +1,82 @@
package org.egovframe.cloud.reactive.config;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.SecurityWebFiltersOrder;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.authentication.AuthenticationWebFilter;
import reactor.core.publisher.Mono;
/**
* org.egovframe.cloud.reserveitemservice.config.SecurityConfig
*
* Spring Security Config 클래스
* AuthenticationFilter 를 추가하고 토큰으로 setAuthentication 인증처리를 한다
*
* @author 표준프레임워크센터 shinmj
* @version 1.0
* @since 2021/09/06
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/09/06 shinmj 최초 생성
* </pre>
*/
@RequiredArgsConstructor
@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {
private final AuthenticationConverter authenticationConverter;
/**
* Reactive Security 설정
*
* @param http
* @return
* @throws Exception
*/
@Bean
public SecurityWebFilterChain configure(ServerHttpSecurity http) throws Exception {
return http
.csrf().disable()
.headers().frameOptions().disable()
.and()
.formLogin().disable()
.httpBasic().disable()
.logout().disable()
.addFilterAt(authenticationWebFilter(), SecurityWebFiltersOrder.AUTHENTICATION)
.build();
}
/**
* AuthenticationManager
* Api Gateway에서 인증되기 때문에 따로 확인하지 않는다.
*
* @return
*/
@Bean
public ReactiveAuthenticationManager authenticationManager() {
return authentication -> Mono.just(authentication);
}
/**
* 인증 요청 필터
* AuthenticationConverter 를 적용하여 Authentication 정보를 설정한다.
*
* @return
* @throws Exception
*/
public AuthenticationWebFilter authenticationWebFilter() throws Exception {
AuthenticationWebFilter filter = new AuthenticationWebFilter(authenticationManager());
filter.setServerAuthenticationConverter(authenticationConverter);
return filter;
}
}

View File

@@ -0,0 +1,20 @@
package org.egovframe.cloud.reactive.config;
import org.springframework.data.domain.ReactiveAuditorAware;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
@Component
public class UserAuditAware implements ReactiveAuditorAware<String> {
@Override
public Mono<String> getCurrentAuditor() {
return ReactiveSecurityContextHolder.getContext()
.map(SecurityContext::getAuthentication)
.filter(Authentication::isAuthenticated)
.map(Authentication::getPrincipal)
.map(String.class::cast);
}
}

View File

@@ -0,0 +1,32 @@
package org.egovframe.cloud.reactive.domain;
import lombok.Getter;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.LastModifiedBy;
/**
* org.egovframe.cloud.servlet.domain.BaseEntity
* <p>
* JPA Entity 클래스들이 BaseEntity 를 상속할 경우 createdBy, lastModifiedBy 필드들과
* BaseTimeEntity 필드들(createdDate, modifiedDate)까지 컬럼으로 인식된다.
*
* @author 표준프레임워크센터 jaeyeolkim
* @version 1.0
* @since 2021/06/30
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/06/30 jaeyeolkim 최초 생성
* </pre>
*/
@Getter
public abstract class BaseEntity extends BaseTimeEntity{
@CreatedBy
protected String createdBy;
@LastModifiedBy
protected String lastModifiedBy;
}

View File

@@ -0,0 +1,34 @@
package org.egovframe.cloud.reactive.domain;
import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import java.time.LocalDateTime;
/**
* org.egovframe.cloud.servlet.domain.BaseTimeEntity
* <p>
* JPA Entity 클래스들이 BaseTimeEntity을 상속할 경우 필드들(createdDate, modifiedDate)도 컬럼으로 인식된다.
*
* @author 표준프레임워크센터 jaeyeolkim
* @version 1.0
* @since 2021/06/30
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/06/30 jaeyeolkim 최초 생성
* </pre>
*/
@Getter
public abstract class BaseTimeEntity {
@CreatedDate
protected LocalDateTime createDate;
@LastModifiedDate
protected LocalDateTime modifiedDate;
}

View File

@@ -0,0 +1,209 @@
package org.egovframe.cloud.reactive.exception;
import io.jsonwebtoken.ExpiredJwtException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.egovframe.cloud.common.exception.BusinessException;
import org.egovframe.cloud.common.exception.BusinessMessageException;
import org.egovframe.cloud.common.exception.dto.ErrorCode;
import org.egovframe.cloud.common.exception.dto.ErrorResponse;
import org.springframework.context.MessageSource;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.validation.BindException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.bind.support.WebExchangeBindException;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.webjars.NotFoundException;
import reactor.core.publisher.Mono;
@Slf4j
@RequiredArgsConstructor
@RestControllerAdvice
public class ExceptionHandlerAdvice {
private final MessageSource messageSource;
/**
* javax.validation.Valid or @Validated 으로 binding error 발생시 발생한다.
* HttpMessageConverter 에서 등록한 HttpMessageConverter binding 못할경우 발생
* 주로 @RequestBody, @RequestPart 어노테이션에서 발생
*
* @param e
* @return ResponseEntity<ErrorResponse>
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
protected Mono<ErrorResponse> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
log.error("handleMethodArgumentNotValidException", e);
final ErrorResponse response = ErrorResponse.of(ErrorCode.INVALID_INPUT_VALUE, e.getBindingResult(), messageSource);
return Mono.just(response);
}
/**
* 바인딩 객체 @ModelAttribute 으로 binding error 발생시 BindException 발생한다.
* ref https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#mvc-ann-modelattrib-method-args
*
* @param e
* @return ResponseEntity<ErrorResponse>
*/
@ExceptionHandler(BindException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
protected Mono<ErrorResponse> handleBindException(BindException e) {
log.error("handleBindException", e);
final ErrorResponse response = ErrorResponse.of(ErrorCode.INVALID_INPUT_VALUE, e.getBindingResult(), messageSource);
return Mono.just(response);
}
/**
* 요청은 잘 만들어졌지만, 문법 오류로 인하여 따를 수 없습니다
*
* @param e
* @return ResponseEntity<ErrorResponse>
*/
@ExceptionHandler(HttpClientErrorException.UnprocessableEntity.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
protected Mono<ErrorResponse> handleUnprocessableEntityException(HttpClientErrorException.UnprocessableEntity e) {
log.error("handleUnprocessableEntityException", e);
final ErrorResponse response = ErrorResponse.of(ErrorCode.UNPROCESSABLE_ENTITY, messageSource);
return Mono.just(response);
}
/**
* enum type 일치하지 않아 binding 못할 경우 발생
* 주로 @RequestParam enum으로 binding 못했을 경우 발생
*
* @param e
* @return ResponseEntity<ErrorResponse>
*/
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
protected Mono<ErrorResponse> handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e) {
log.error("handleMethodArgumentTypeMismatchException", e);
final ErrorResponse response = ErrorResponse.of(e, messageSource);
return Mono.just(response);
}
/**
* 요청한 페이지가 존재하지 않는 경우
*
* @param e
* @return ResponseEntity<ErrorResponse>
*/
@ExceptionHandler(NotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
protected Mono<ErrorResponse> handleNotFoundException(NotFoundException e) {
log.error("handleNotFoundException", e);
final ErrorResponse response = ErrorResponse.of(ErrorCode.NOT_FOUND, messageSource);
return Mono.just(response);
}
/**
* Authentication 객체가 필요한 권한을 보유하지 않은 경우 발생
*
* @param e
* @return ResponseEntity<ErrorResponse>
*/
@ExceptionHandler(AccessDeniedException.class)
protected Mono<ResponseEntity<ErrorResponse>> handleAccessDeniedException(AccessDeniedException e) {
log.error("handleAccessDeniedException", e);
final ErrorResponse response = ErrorResponse.of(ErrorCode.ACCESS_DENIED, messageSource);
return Mono.just(ResponseEntity.status(HttpStatus.valueOf(ErrorCode.ACCESS_DENIED.getStatus()))
.body(response));
}
/**
* 사용자 인증되지 않은 경우 발생
*
* @param e
* @return ResponseEntity<ErrorResponse>
*/
@ExceptionHandler(HttpClientErrorException.Unauthorized.class)
protected Mono<ResponseEntity<ErrorResponse>> handleUnauthorizedException(HttpClientErrorException.Unauthorized e) {
log.error("handleUnauthorizedException", e);
final ErrorResponse response = ErrorResponse.of(ErrorCode.UNAUTHORIZED, messageSource);
return Mono.just(ResponseEntity.status(HttpStatus.valueOf(ErrorCode.ACCESS_DENIED.getStatus()))
.body(response));
}
/**
* JWT 인증 만료
*
* @param e
* @return ResponseEntity<ErrorResponse>
*/
@ExceptionHandler(ExpiredJwtException.class)
protected Mono<ResponseEntity<ErrorResponse>> handleExpiredJwtException(ExpiredJwtException e) {
log.error("handleExpiredJwtException", e);
final ErrorResponse response = ErrorResponse.of(ErrorCode.JWT_EXPIRED, messageSource);
return Mono.just(ResponseEntity.status(HttpStatus.valueOf(ErrorCode.ACCESS_DENIED.getStatus()))
.body(response));
}
/**
* 사용자에게 표시할 다양한 메시지를 직접 정의하여 처리하는 Business RuntimeException Handler
* 개발자가 만들어 던지는 런타임 오류를 처리
*
* @param e
* @return ResponseEntity<ErrorResponse>
*/
@ExceptionHandler(BusinessMessageException.class)
protected Mono<ResponseEntity<ErrorResponse>> handleBusinessMessageException(BusinessMessageException e) {
log.error("handleBusinessMessageException", e);
final ErrorCode errorCode = e.getErrorCode();
final String customMessage = e.getCustomMessage();
final ErrorResponse response = ErrorResponse.of(errorCode, customMessage);
return Mono.just(ResponseEntity.status(HttpStatus.valueOf(errorCode.getStatus()))
.body(response));
}
/**
* 개발자 정의 ErrorCode 를 처리하는 Business RuntimeException Handler
* 개발자가 만들어 던지는 런타임 오류를 처리
*
* @param e
* @return ResponseEntity<ErrorResponse>
*/
@ExceptionHandler(BusinessException.class)
protected Mono<ResponseEntity<ErrorResponse>> handleBusinessException(BusinessException e) {
log.error("handleBusinessException", e);
final ErrorCode errorCode = e.getErrorCode();
final ErrorResponse response = ErrorResponse.of(errorCode, messageSource);
return Mono.just(ResponseEntity.status(HttpStatus.valueOf(errorCode.getStatus()))
.body(response));
}
/**
* default exception
*
* @param e
* @return ResponseEntity<ErrorResponse>
*/
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
protected Mono<ErrorResponse> handleException(Exception e) {
log.error("handleException", e);
final ErrorResponse response = ErrorResponse.of(ErrorCode.INTERNAL_SERVER_ERROR, messageSource);
return Mono.just(response);
}
/**
* javax.validation.Valid or @Validated 으로 binding error 발생시 발생한다.
*
* @param e
* @return ResponseEntity<ErrorResponse>
*/
@ExceptionHandler(WebExchangeBindException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
protected Mono<ErrorResponse> handleWebExchangeBindException(WebExchangeBindException e) {
log.error("handleWebExchangeBindException", e);
final ErrorResponse response = ErrorResponse.of(ErrorCode.INVALID_INPUT_VALUE, e.getBindingResult(), messageSource);
return Mono.just(response);
}
}

View File

@@ -0,0 +1,19 @@
package org.egovframe.cloud.reactive.service;
import org.egovframe.cloud.common.exception.EntityNotFoundException;
import org.egovframe.cloud.common.service.AbstractService;
import reactor.core.publisher.Mono;
public class ReactiveAbstractService extends AbstractService {
/**
* mono error entity not found exception
*
* @param id
* @param <T>
* @return
*/
protected <T> Mono<T> monoResponseStatusEntityNotFoundException(Object id) {
return Mono.error( new EntityNotFoundException("해당 데이터가 존재하지 않습니다. ID =" + String.valueOf(id)));
}
}

View File

@@ -0,0 +1,101 @@
package org.egovframe.cloud.servlet.config;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* org.egovframe.cloud.servlet.config.AuthenticationFilter
* <p>
* Spring Security AuthenticationFilter 처리
* 로그인 인증정보를 받아 토큰을 발급한다
*
* @author 표준프레임워크센터 jaeyeolkim
* @version 1.0
* @since 2021/06/30
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/06/30 jaeyeolkim 최초 생성
* </pre>
*/
public class AuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final String TOKEN_SECRET;
final String TOKEN_CLAIM_NAME = "authorities";
public AuthenticationFilter(AuthenticationManager authenticationManager, String tokenSecret) {
super.setAuthenticationManager(authenticationManager);
this.TOKEN_SECRET = tokenSecret;
}
/**
* AuthenticationFilter.doFilter 메소드에서 UsernamePasswordAuthenticationToken 정보를 세팅할 때 호출된다.
*
* @param token
* @return
*/
public Claims getClaimsFromToken(String token) {
return Jwts.parser()
.setSigningKey(TOKEN_SECRET)
.parseClaimsJws(token)
.getBody();
}
/**
* 로그인 요청 뿐만 아니라 모든 요청시마다 호출된다.
* 토큰에 담긴 정보로 Authentication 정보를 설정한다.
* 이 처리를 하지 않으면 AnonymousAuthenticationToken 으로 처리된다.
*
* @param request
* @param response
* @param chain
* @throws IOException
* @throws ServletException
*/
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String token = httpRequest.getHeader(HttpHeaders.AUTHORIZATION);
if (token == null || "undefined".equals(token) || "".equals(token)) {
super.doFilter(request, response, chain);
} else {
Claims claims = getClaimsFromToken(token);
String authorities = claims.get(TOKEN_CLAIM_NAME, String.class);
List<SimpleGrantedAuthority> roleList = new ArrayList<>();
roleList.add(new SimpleGrantedAuthority(authorities));
String username = claims.getSubject();
if (username != null) {
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(username, null, roleList));
chain.doFilter(request, response);
} else {
SecurityContextHolder.getContext().setAuthentication(null);
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
httpServletResponse.setStatus(HttpStatus.UNAUTHORIZED.value());
}
}
}
}

View File

@@ -0,0 +1,44 @@
package org.egovframe.cloud.servlet.config;
import com.querydsl.jpa.impl.JPAQueryFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import javax.persistence.EntityManager;
/**
* org.egovframe.cloud.servlet.config.JpaConfig
*
* JPA 설정 클래스
*
* @author 표준프레임워크센터 jooho
* @version 1.0
* @since 2021/07/07
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/07/07 jooho 최초 생성
* </pre>
*/
@Configuration
@EnableJpaAuditing(auditorAwareRef = "userAuditAware") // JPA Auditing 활성화
@EnableJpaRepositories(basePackages = "org.egovframe.cloud.*.domain")
public class JpaConfig {
/**
* JpaQueryFactory 빈 등록
*
* @param entityManager 엔티티 매니저
* @return JPAQueryFactory 쿼리 및 DML 절 생성을 위한 팩토리 클래스
*/
@Bean
public JPAQueryFactory jpaQueryFactory(EntityManager entityManager) {
return new JPAQueryFactory(entityManager);
}
}

View File

@@ -0,0 +1,48 @@
package org.egovframe.cloud.servlet.config;
import org.springframework.data.domain.AuditorAware;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import java.util.Optional;
/**
* org.egovframe.cloud.servlet.config.UserAuditAware
* <p>
* JPA Entity 생성자/수정자 정보를 자동 입력한다.
* AuthenticationFilter.doFilter 메소드에서 UsernamePasswordAuthenticationToken 정보를 세팅해주기 때문에
* Authentication 에서 userId 값을 받아올 수 있게 된다.
*
* @author 표준프레임워크센터 jaeyeolkim
* @version 1.0
* @since 2021/07/08
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/07/08 jaeyeolkim 최초 생성
* </pre>
*/
@Component
public class UserAuditAware implements AuditorAware<String> {
/**
* Auditing 기능이 활성화된 엔티티에 변경이 감지되면 호출되어 생성자/수정자 정보를 반환한다.
*
* @return
*/
@Override
public Optional<String> getCurrentAuditor() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !authentication.isAuthenticated() || authentication instanceof AnonymousAuthenticationToken) {
return Optional.empty();
}
String userId = authentication.getPrincipal() == null ? null : authentication.getPrincipal().toString();
return Optional.of(userId);
}
}

View File

@@ -0,0 +1,66 @@
package org.egovframe.cloud.servlet.config;
import org.egovframe.cloud.servlet.interceptor.ApiLogInterceptor;
import org.egovframe.cloud.servlet.service.ApiLogService;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* org.egovframe.cloud.userservice.domain.BaseTimeEntity
* <p>
* WebMvc 관련 Configuration 클래스
*
* @author 표준프레임워크센터 jaeyeolkim
* @version 1.0
* @since 2021/07/05
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/07/05 jaeyeolkim 최초 생성
* </pre>
*/
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
private final ApiLogService apiLogService;
public WebMvcConfig(ApiLogService apiLogService) {
this.apiLogService = apiLogService;
}
/**
* LocalValidatorFactoryBean 에 messageSource Bean 주입하여 validtor 메시지를 지원한다.
*
* @param messageSource messages.properties
* @return LocalValidatorFactoryBean
*/
@Bean
public LocalValidatorFactoryBean validator(MessageSource messageSource) {
LocalValidatorFactoryBean bean = new LocalValidatorFactoryBean();
bean.setValidationMessageSource(messageSource);
return bean;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry
.addInterceptor(new ApiLogInterceptor(apiLogService))
.excludePathPatterns(
"/",
"/error",
"/favicon.ico",
"/api/v1/authorizations/check",
"/api/v1/users/token/refresh",
"/api/v1/menu-roles/**",
"/api/v1/messages/**"
);
}
}

View File

@@ -0,0 +1,41 @@
package org.egovframe.cloud.servlet.domain;
import lombok.Getter;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import javax.persistence.Column;
import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
/**
* org.egovframe.cloud.servlet.domain.BaseTimeEntity
* <p>
* JPA Entity 클래스들이 BaseEntity 를 상속할 경우 createdBy, lastModifiedBy 필드들과
* BaseTimeEntity 필드들(createdDate, modifiedDate)까지 컬럼으로 인식된다.
*
* @author 표준프레임워크센터 jaeyeolkim
* @version 1.0
* @since 2021/06/30
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/06/30 jaeyeolkim 최초 생성
* </pre>
*/
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class) // Auditing 기능 포함
public abstract class BaseEntity extends BaseTimeEntity {
@CreatedBy
@Column(updatable = false)
private String createdBy;
@LastModifiedBy
private String lastModifiedBy;
}

View File

@@ -0,0 +1,39 @@
package org.egovframe.cloud.servlet.domain;
import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import java.time.LocalDateTime;
/**
* org.egovframe.cloud.servlet.domain.BaseTimeEntity
* <p>
* JPA Entity 클래스들이 BaseTimeEntity을 상속할 경우 필드들(createdDate, modifiedDate)도 컬럼으로 인식된다.
*
* @author 표준프레임워크센터 jaeyeolkim
* @version 1.0
* @since 2021/06/30
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/06/30 jaeyeolkim 최초 생성
* </pre>
*/
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class) // Auditing 기능 포함
public abstract class BaseTimeEntity {
@CreatedDate
private LocalDateTime createdDate;
@LastModifiedDate
private LocalDateTime modifiedDate;
}

View File

@@ -0,0 +1,60 @@
package org.egovframe.cloud.servlet.domain.log;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.egovframe.cloud.servlet.domain.BaseTimeEntity;
import javax.persistence.*;
import static javax.persistence.GenerationType.IDENTITY;
/**
* org.egovframe.cloud.servlet.domain.log.LoginLog
* <p>
* 로그인 로그 엔티티
*
* @author 표준프레임워크센터 jaeyeolkim
* @version 1.0
* @since 2021/09/01
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/09/01 jaeyeolkim 최초 생성
* </pre>
*/
@Getter
@NoArgsConstructor
@Entity
public class ApiLog extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = IDENTITY)
@Column(name = "log_id")
private Long id;
private Long siteId;
private String userId;
@Column(length = 10)
private String httpMethod;
@Column(length = 500)
private String requestUrl;
@Column(name = "ip_addr", length = 100)
private String remoteIp;
@Builder
public ApiLog(Long siteId, String userId, String httpMethod, String requestUrl, String remoteIp) {
this.siteId = siteId;
this.userId = userId;
this.httpMethod = httpMethod;
this.requestUrl = requestUrl;
this.remoteIp = remoteIp;
}
}

View File

@@ -0,0 +1,23 @@
package org.egovframe.cloud.servlet.domain.log;
import org.springframework.data.jpa.repository.JpaRepository;
/**
* org.egovframe.cloud.servlet.domain.log.ApiLogRepository
* <p>
* Spring Data JPA 에서 제공되는 JpaRepository 를 상속하는 인터페이스
*
* @author 표준프레임워크센터 jaeyeolkim
* @version 1.0
* @since 2021/09/01
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/09/01 jaeyeolkim 최초 생성
* </pre>
*/
public interface ApiLogRepository extends JpaRepository<ApiLog, Long> {
}

View File

@@ -0,0 +1,213 @@
package org.egovframe.cloud.servlet.exception;
import io.jsonwebtoken.ExpiredJwtException;
import javassist.NotFoundException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.egovframe.cloud.common.exception.BusinessException;
import org.egovframe.cloud.common.exception.BusinessMessageException;
import org.egovframe.cloud.common.exception.dto.ErrorCode;
import org.egovframe.cloud.common.exception.dto.ErrorResponse;
import org.springframework.context.MessageSource;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.validation.BindException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
/**
* org.egovframe.cloud.common.exception.WebControllerAdvice
* <p>
* 모든 컨트롤러에 적용되는 컨트롤러 어드바이스 클래스
* 예외 처리 (@ExceptionHandler), 바인딩 설정(@InitBinder), 모델 객체(@ModelAttributes)
*
* @author 표준프레임워크센터 jaeyeolkim
* @version 1.0
* @since 2021/07/15
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/07/15 jaeyeolkim 최초 생성
* </pre>
*/
@Slf4j
@RestControllerAdvice
@RequiredArgsConstructor
public class ExceptionHandlerAdvice {
protected final MessageSource messageSource;
/**
* javax.validation.Valid or @Validated 으로 binding error 발생시 발생한다.
* HttpMessageConverter 에서 등록한 HttpMessageConverter binding 못할경우 발생
* 주로 @RequestBody, @RequestPart 어노테이션에서 발생
*
* @param e
* @return ResponseEntity<ErrorResponse>
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
protected ResponseEntity<ErrorResponse> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
log.error("handleMethodArgumentNotValidException", e);
final ErrorResponse response = ErrorResponse.of(ErrorCode.INVALID_INPUT_VALUE, e.getBindingResult(), messageSource);
return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
}
/**
* 바인딩 객체 @ModelAttribute 으로 binding error 발생시 BindException 발생한다.
* ref https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#mvc-ann-modelattrib-method-args
*
* @param e
* @return ResponseEntity<ErrorResponse>
*/
@ExceptionHandler(BindException.class)
protected ResponseEntity<ErrorResponse> handleBindException(BindException e) {
log.error("handleBindException", e);
final ErrorResponse response = ErrorResponse.of(ErrorCode.INVALID_INPUT_VALUE, e.getBindingResult(), messageSource);
return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
}
/**
* 요청은 잘 만들어졌지만, 문법 오류로 인하여 따를 수 없습니다
*
* @param e
* @return ResponseEntity<ErrorResponse>
*/
@ExceptionHandler(HttpClientErrorException.UnprocessableEntity.class)
protected ResponseEntity<ErrorResponse> handleUnprocessableEntityException(HttpClientErrorException.UnprocessableEntity e) {
log.error("handleUnprocessableEntityException", e);
final ErrorResponse response = ErrorResponse.of(ErrorCode.UNPROCESSABLE_ENTITY, messageSource);
return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
}
/**
* 지원하지 않은 HTTP method 호출 할 경우 발생
*
* @param e
* @return ResponseEntity<ErrorResponse>
*/
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
protected ResponseEntity<ErrorResponse> handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) {
log.error("handleHttpRequestMethodNotSupportedException", e);
final ErrorResponse response = ErrorResponse.of(ErrorCode.METHOD_NOT_ALLOWED, messageSource);
return new ResponseEntity<>(response, HttpStatus.METHOD_NOT_ALLOWED);
}
/**
* enum type 일치하지 않아 binding 못할 경우 발생
* 주로 @RequestParam enum으로 binding 못했을 경우 발생
*
* @param e
* @return ResponseEntity<ErrorResponse>
*/
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
protected ResponseEntity<ErrorResponse> handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e) {
log.error("handleMethodArgumentTypeMismatchException", e);
final ErrorResponse response = ErrorResponse.of(e, messageSource);
return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
}
/**
* 요청한 페이지가 존재하지 않는 경우
*
* @param e
* @return ResponseEntity<ErrorResponse>
*/
@ExceptionHandler(NotFoundException.class)
protected ResponseEntity<ErrorResponse> handleNotFoundException(NotFoundException e) {
log.error("handleNotFoundException", e);
final ErrorResponse response = ErrorResponse.of(ErrorCode.NOT_FOUND, messageSource);
return new ResponseEntity<>(response, HttpStatus.NOT_FOUND);
}
/**
* Authentication 객체가 필요한 권한을 보유하지 않은 경우 발생
*
* @param e
* @return ResponseEntity<ErrorResponse>
*/
@ExceptionHandler(AccessDeniedException.class)
protected ResponseEntity<ErrorResponse> handleAccessDeniedException(AccessDeniedException e) {
log.error("handleAccessDeniedException", e);
final ErrorResponse response = ErrorResponse.of(ErrorCode.ACCESS_DENIED, messageSource);
return new ResponseEntity<>(response, HttpStatus.valueOf(ErrorCode.ACCESS_DENIED.getStatus()));
}
/**
* 사용자 인증되지 않은 경우 발생
*
* @param e
* @return ResponseEntity<ErrorResponse>
*/
@ExceptionHandler(HttpClientErrorException.Unauthorized.class)
protected ResponseEntity<ErrorResponse> handleUnauthorizedException(HttpClientErrorException.Unauthorized e) {
log.error("handleUnauthorizedException", e);
final ErrorResponse response = ErrorResponse.of(ErrorCode.UNAUTHORIZED, messageSource);
return new ResponseEntity<>(response, HttpStatus.valueOf(ErrorCode.ACCESS_DENIED.getStatus()));
}
/**
* JWT 인증 만료
*
* @param e
* @return ResponseEntity<ErrorResponse>
*/
@ExceptionHandler(ExpiredJwtException.class)
protected ResponseEntity<ErrorResponse> handleExpiredJwtException(ExpiredJwtException e) {
log.error("handleExpiredJwtException", e);
final ErrorResponse response = ErrorResponse.of(ErrorCode.JWT_EXPIRED, messageSource);
return new ResponseEntity<>(response, HttpStatus.valueOf(ErrorCode.ACCESS_DENIED.getStatus()));
}
/**
* 사용자에게 표시할 다양한 메시지를 직접 정의하여 처리하는 Business RuntimeException Handler
* 개발자가 만들어 던지는 런타임 오류를 처리
*
* @param e
* @return ResponseEntity<ErrorResponse>
*/
@ExceptionHandler(BusinessMessageException.class)
protected ResponseEntity<ErrorResponse> handleBusinessMessageException(BusinessMessageException e) {
log.error("handleBusinessMessageException", e);
final ErrorCode errorCode = e.getErrorCode();
final String customMessage = e.getCustomMessage();
final ErrorResponse response = ErrorResponse.of(errorCode, customMessage);
return new ResponseEntity<>(response, HttpStatus.valueOf(errorCode.getStatus()));
}
/**
* 개발자 정의 ErrorCode 를 처리하는 Business RuntimeException Handler
* 개발자가 만들어 던지는 런타임 오류를 처리
*
* @param e
* @return ResponseEntity<ErrorResponse>
*/
@ExceptionHandler(BusinessException.class)
protected ResponseEntity<ErrorResponse> handleBusinessException(BusinessException e) {
log.error("handleBusinessException", e);
final ErrorCode errorCode = e.getErrorCode();
final ErrorResponse response = ErrorResponse.of(errorCode, messageSource);
return new ResponseEntity<>(response, HttpStatus.valueOf(errorCode.getStatus()));
}
/**
* default exception
*
* @param e
* @return ResponseEntity<ErrorResponse>
*/
@ExceptionHandler(Exception.class)
protected ResponseEntity<ErrorResponse> handleException(Exception e) {
log.error("handleException", e);
final ErrorResponse response = ErrorResponse.of(ErrorCode.INTERNAL_SERVER_ERROR, messageSource);
return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR);
}
}

View File

@@ -0,0 +1,38 @@
package org.egovframe.cloud.servlet.interceptor;
import lombok.extern.slf4j.Slf4j;
import org.egovframe.cloud.servlet.service.ApiLogService;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Slf4j
public class ApiLogInterceptor implements HandlerInterceptor {
private final ApiLogService apiLogService;
public ApiLogInterceptor(ApiLogService apiLogService) {
this.apiLogService = apiLogService;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 접근 로그 입력
apiLogService.saveApiLog(request);
log.info("[ApiLogInterceptor preHandle] {}, {}, {}", request.getMethod(), request.getRequestURI(), response.getStatus());
return HandlerInterceptor.super.preHandle(request, response, handler);
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("[ApiLogInterceptor postHandle] {}, {}, {}", request.getMethod(), request.getRequestURI(), response.getStatus());
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
}
}

View File

@@ -0,0 +1,63 @@
package org.egovframe.cloud.servlet.service;
import lombok.extern.slf4j.Slf4j;
import org.egovframe.cloud.servlet.domain.log.ApiLog;
import org.egovframe.cloud.common.service.AbstractService;
import org.egovframe.cloud.servlet.domain.log.ApiLogRepository;
import org.egovframe.cloud.common.util.LogUtil;
import org.egovframe.cloud.servlet.interceptor.ApiLogInterceptor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.servlet.http.HttpServletRequest;
/**
* org.egovframe.cloud.servlet.service.ApiLogService
* <p>
* API Log 처리 서비스
*
* @author 표준프레임워크센터 jaeyeolkim
* @version 1.0
* @since 2021/09/01
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/09/01 jaeyeolkim 최초 생성
* </pre>
*/
@Slf4j
@Service
public class ApiLogService extends AbstractService {
private final ApiLogRepository apiLogRepository;
public ApiLogService(ApiLogRepository apiLogRepository) {
this.apiLogRepository = apiLogRepository;
}
/**
* API log 입력
* LogInterceptor 에서 호출된다
*
* @param request
* @see ApiLogInterceptor
*/
@Transactional
public void saveApiLog(HttpServletRequest request) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
apiLogRepository.save(
ApiLog.builder()
.siteId(LogUtil.getSiteId(request))
.httpMethod(request.getMethod())
.requestUrl(request.getRequestURI())
.userId(authentication.getName())
.remoteIp(LogUtil.getUserIp())
.build()
);
}
}