본문 바로가기
포트폴리오/Java rest api

rest api + jpa(jpql) + replica set

by NaHyungMin 2022. 8. 2.

지인의 요청으로 간단한 서버 구현.

* 보안 및 민감한 부분은 코드가 없을 수 있습니다. 이 포스팅은 누군가에게 프로그램 구현을 위한 공유가 아닙니다.

* 220811 멀티프로젝트 배포 시 설정을 각 모듈로 나눠진 내용을 최상단으로 변경

환경 : 오라클 클라우드 free... ram 1gb, vcpu2

OS : Linux centos7

DB : mariadb 10.5.8

Tool : intellij, Java 1.8

Spring boot 2.7.1 Gradle

프로젝트 구조

 

멀트 프로젝트 Gradle 설정

buildscript {
    ext {
        springBootVersion = '2.7.1'
    }

    repositories {
        mavenCentral()
    }

    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
        classpath "io.spring.gradle:dependency-management-plugin:1.0.11.RELEASE"
    }
}

allprojects {
}

subprojects {
    apply plugin: 'java'
    apply plugin: 'idea'
    apply plugin: 'org.springframework.boot'
    apply plugin: 'io.spring.dependency-management'

    group = 'org.program'
    version = '1.0'
    sourceCompatibility = '11'

    repositories {
        mavenCentral()
    }

    dependencies {
        compileOnly 'org.projectlombok:lombok:1.18.16'
        annotationProcessor 'org.projectlombok:lombok:1.18.16'

        implementation group: 'commons-io', name: 'commons-io', version: '2.11.0'

        // https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind
        implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.13.3'

        implementation group: 'commons-codec', name: 'commons-codec', version: '1.15'
        implementation group: 'org.apache.commons', name: 'commons-pool2', version: '2.11.1'

        testImplementation 'org.springframework.boot:spring-boot-starter-test'
        testImplementation('org.springframework.boot:spring-boot-starter-test') {
            exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
        }
    }

    test {
        useJUnitPlatform()
    }
}

project(':apps:log') {
    bootJar.enabled = false
    jar.enabled = true

    dependencies {
        implementation group: 'org.slf4j', name: 'slf4j-api', version: '1.7.36'
        implementation group: 'ch.qos.logback', name: 'logback-classic', version: '1.2.11'

        // https://mvnrepository.com/artifact/org.springframework/spring-context
        /*implementation group: 'org.springframework', name: 'spring-context', version: '5.3.22'*/
    }
}

project(':apps:commons') {
    bootJar.enabled = false
    jar.enabled = true

    dependencies {
        compileOnly project(':apps:log')
        compileOnly group: 'javax.servlet', name: 'javax.servlet-api', version: '4.0.1'

        implementation group: 'org.springframework', name: 'spring-webmvc', version: '5.3.21'
        implementation group: 'com.google.code.gson', name: 'gson', version: '2.9.0'

        implementation group: 'org.slf4j', name: 'slf4j-api', version: '1.7.36'
        implementation group: 'jakarta.annotation', name: 'jakarta.annotation-api', version: '2.1.1'
        implementation group: 'org.springframework.boot', name: 'spring-boot-starter-data-jpa', version: '2.7.1'
    }
}

project(':apps:application') {
    dependencies {
        // 컴파일시 commons 모듈을 가져온다.
        compileOnly project(':apps:log')
        compileOnly project(':apps:commons')
        compileOnly group: 'org.projectlombok', name: 'lombok', version: '1.18.22'

        implementation 'org.springframework.boot:spring-boot-starter-web'
        implementation 'org.springframework.boot:spring-boot-starter-jdbc'
        implementation group: 'org.springframework.boot', name: 'spring-boot-starter-data-jpa', version: '2.7.1'
        implementation group: 'org.mariadb.jdbc', name: 'mariadb-java-client', version: '3.0.6'
        implementation group: 'com.h2database', name: 'h2', version: '2.1.214'

        testCompile project(':apps:commons')
        testCompile project(':apps:log')
        testImplementation 'org.springframework.boot:spring-boot-starter-test'
    }
}

 

Log 프로젝트

Log

package com.program.log;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

//@Component
public class Log {
    /*private static final Logger log = LoggerFactory.getLogger(Log.class);*/
    private static final Logger debugLog = LoggerFactory.getLogger(LogTypes.DEBUG_LOGGER.name());
    private static final Logger detailLog = LoggerFactory.getLogger(LogTypes.DETAIL_LOGGER.name());
    private static final Logger errorLog = LoggerFactory.getLogger(LogTypes.ERROR_LOGGER.name());
    private static final Logger batchLog = LoggerFactory.getLogger(LogTypes.BATCH_LOGGER.name());
    private static final Logger batchErrorLog = LoggerFactory.getLogger(LogTypes.BATCH_ERROR_LOGGER.name());

    public enum LogLevel {
        Debug(0),
        Info(1),
        Warn(2),
        Error(3),
        Trace(4);

        private int value;
        LogLevel(int value) {
            this.value = value;
        }
    }

    public static void debug(String pattern, Object... args) {
        debugLog.debug(getLogPattern(pattern), args);
    }

    public static void info(String pattern, Object... args){
        debugLog.debug(getLogPattern(pattern), args);
        detailLog.info(getLogPattern(pattern), args);
    }

    public static void warn(String pattern, Object... args) {
        detailLog.warn(getLogPattern(pattern), args);
    }

    public static void error(String pattern, Object... args){
        debugLog.debug(getLogPattern(pattern), args);
        errorLog.error(getLogPattern(pattern), args);
    }

    public static void trace(String pattern, Object... args){
        debugLog.trace(getLogPattern(pattern), args);
    }

    public static void batchInfo(String pattern, Object... args){
        batchLog.info(getLogPattern(pattern), args);
    }

    public static void batchErrorInfo(String pattern, Object... args){
        batchErrorLog.info(getLogPattern(pattern), args);
    }

    public static void write(LogLevel level, String pattern, Object... args){
        switch (level){
            case Debug: debug(pattern, args); break;
            case Info: info(pattern, args); break;
            case Warn: warn(pattern, args); break;
            case Error: error(pattern, args); break;
            case Trace:
            default: trace(pattern, args); break;
        }
    }

    private static String getClassName(){
        return Thread.currentThread().getStackTrace()[2].getClassName();
    }

    private static String getMethodName() {
        Thread.currentThread().getStackTrace()[2].getLineNumber();
        return Thread.currentThread().getStackTrace()[2].getMethodName();
    }

    private static String getLogPattern(String pattern) {
        StackTraceElement[] stackList = new Throwable().getStackTrace();

        if (stackList.length > 3) {
            String head = stackList[2].getClassName() + "." + stackList[2].getMethodName() + "():" + stackList[2].getLineNumber();
            pattern = head + ", " + pattern;
        }

        return pattern;
    }
}

LogTypes

package com.program.log;

public enum LogTypes {
   DEBUG_LOGGER, DETAIL_LOGGER, ERROR_LOGGER, BATCH_LOGGER, BATCH_ERROR_LOGGER
}

 

logback-dev.xml

<?xml version="1.0" encoding="UTF-8" ?>
<configuration scan="true" scanPeriod="5 seconds">
    <property name="LOG_PATH" value="/var/log/server" />
    <property name="DATE" value="%d{yyyyMMdd, Asia/Seoul}"/>
    <property name="DATETIME" value="%d{yyyy-MM-dd HH:mm:ss z, Asia/Seoul}"/>
    <!--<property name="CONSOLE_PATTERN" value="[${DATETIME}] [%-5level] : %msg%n" />-->

    <property name="CONSOLE_PATTERN" value="[${DATETIME}] [%-5level] %X{access.uri}: %msg %X{remote.address} %X{user.device}%n" />

    <property name="ACCESS_LOG_FILE_PATTERN" value="${CONSOLE_PATTERN}" />
    <property name="DETAIL_LOG_FILE_PATTERN" value="${CONSOLE_PATTERN}" />
    <property name="BATCH_LOG_FILE_PATTERN" value="[${DATETIME}][%-5level] %msg%n" />

    <appender name="detail-file" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <filter class="com.program.log.filter.SqlFilter" />
        <file>${LOG_PATH}/detail.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${LOG_PATH}/detail/detail-${DATE}.log</fileNamePattern>
            <maxHistory>30</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>${DETAIL_LOG_FILE_PATTERN}</pattern>
        </encoder>
    </appender>

    <appender name="error-file" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>error</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>

        <file>${LOG_PATH}/error.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${LOG_PATH}/error/error-${DATE}.log</fileNamePattern>
            <maxHistory>30</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>${DETAIL_LOG_FILE_PATTERN}</pattern>
        </encoder>
    </appender>

    <appender name="batch-file" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_PATH}/batch.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${LOG_PATH}/batch/batch-${DATE}.log</fileNamePattern>
            <maxHistory>30</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>${BATCH_LOG_FILE_PATTERN}</pattern>
        </encoder>
    </appender>

    <appender name="batch-error-file" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>error</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>

        <file>${LOG_PATH}/batch-error.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${LOG_PATH}/batch-error/batch-error-${DATE}.log</fileNamePattern>
            <maxHistory>30</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>${BATCH_LOG_FILE_PATTERN}</pattern>
        </encoder>
    </appender>

    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>${CONSOLE_PATTERN}</pattern>
        </encoder>
    </appender>

     <logger name="DEBUG_LOGGER" additivity="false" level="debug">
        <appender-ref ref="console" />
    </logger>

    <logger name="DETAIL_LOGGER" additivity="false" level="info">
        <appender-ref ref="detail-file" />
    </logger>

    <logger name="ERROR_LOGGER" additivity="false" level="error">
        <appender-ref ref="error-file" />
    </logger>

     <logger name="BATCH_LOGGER" additivity="false" level="info">
        <appender-ref ref="batch-file" />
    </logger>

     <logger name="BATCH_ERROR_LOGGER" additivity="false" level="error">
        <appender-ref ref="batch-error-file" />
    </logger>

    <root level="info">
        <appender-ref ref="console" />
        <!--<appender-ref ref="error-file" />
        <appender-ref ref="detail-file" />
        <appender-ref ref="batch-file" />
        <appender-ref ref="batch-error-file" />-->
    </root>

</configuration>

SqlFillter

package com.program.log.filter;

import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.filter.Filter;
import ch.qos.logback.core.spi.FilterReply;

public class SqlFilter extends Filter<ILoggingEvent> {
  @Override
  public FilterReply decide(ILoggingEvent event) {

      String[] containsDenyArray = { "Connection", "encrypt" };

      for (String deny : containsDenyArray) {
        if(event.getMessage().contains(deny)) {
          return FilterReply.DENY;
        }
      }

      return FilterReply.ACCEPT;
  }
}

 

Commons 프로젝트

AbstractRestapiBaseException

package com.program.commons.exception;

import org.springframework.http.HttpStatus;

public abstract class AbstractRestapiBaseException extends RuntimeException {
  private static final long serialVersionUID = 1L;

  public AbstractRestapiBaseException() {
    super();
  }

  public AbstractRestapiBaseException(String msg) {
    super(msg);
  }

  public AbstractRestapiBaseException(Throwable e) {
    super(e);
  }

  public AbstractRestapiBaseException(String errorMessge, Throwable e) {
    super(errorMessge, e);
  }

  public abstract HttpStatus getHttpStatus();
}

 

RestapiUnSupportSqlException

package com.program.commons.exception;

import lombok.Getter;
import org.springframework.http.HttpStatus;

public class RestapiUnSupportSqlException extends AbstractRestapiBaseException {

  private static final long serialVersionUID = 1L;

  @Getter
  private int errorCode;

  public int getErrorCode() { return errorCode; }

  public RestapiUnSupportSqlException() {
    super();
  }

  public RestapiUnSupportSqlException(Throwable e) {
    super(e);
  }

  public RestapiUnSupportSqlException(String errorMessge) {
    super(errorMessge);
  }

  public RestapiUnSupportSqlException(String errorMessge, Throwable e) {
    super(errorMessge, e);
  }

  public RestapiUnSupportSqlException(int errorCode, String errorMessge, Throwable e) {
    super(errorMessge, e);
    this.errorCode = errorCode;
  }

  public RestapiUnSupportSqlException(int errorCode, String errorMessge) {
    super(errorMessge);
    this.errorCode = errorCode;
  }

  public HttpStatus getHttpStatus() {
    return HttpStatus.OK;
  }
}

 

HttpFilter

package com.program.commons.filter;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.program.commons.exception.RestapiInvalidRequestException;
import com.program.commons.response.ResponseResult;
import com.program.commons.response.ResultCode;
import com.program.commons.security.SecurityUtil;
import com.program.log.Log;
import org.apache.commons.io.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Objects;

public class HttpFilter extends OncePerRequestFilter {
  private final String REQUEST_TIME = "request-time";
  private final String TRANSACTION_NUMBER = "transaction-number";
  private final String RESPONSE_TIME = "response-time";
  private final String RESPONSE = "response";
  private ObjectMapper objectMapper = new ObjectMapper();

  public List<String> excludePathPatterns = new ArrayList<>();

  @Override
  protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
    ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(request);
    ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);

    HttpHeaders requestHeaders = new HttpHeaders();
    Enumeration headerNames = requestWrapper.getHeaderNames();

    String requestUrl = requestWrapper.getRequestURL().toString();
    Log.info("request={}, user agent={}", requestUrl, request.getHeader("User-Agent"));

    while (headerNames.hasMoreElements()) {
      String headerName = (String) headerNames.nextElement();
      requestHeaders.add(headerName, requestWrapper.getHeader(headerName));
    }

    if(!isExcludePathPatterns(requestUrl)) {
      //header 검사.
      if(!requestHeaders.containsKey("access-token")) {
        Log.error("Filter Not Found access-token request url = {}", requestUrl);
        errorMessageFlush(response, requestUrl);
        return;
        //return new ResponseResult(exception.getErrorCode(), new RestapiInvalidRequestException(exception.getMessage(), exception), req.getRequestURL().toString());
      }
    }

    String[] headerCheckArray = { REQUEST_TIME, TRANSACTION_NUMBER }; //, TRANSACTION_ID};

    for (String headerCheck : headerCheckArray) {
      if (!requestHeaders.containsKey(headerCheck)) {
        Log.error("Filter Not Found Header = {}", headerCheck);
        errorMessageFlush(response, requestUrl);
        return;
      }
    }

    filterChain.doFilter(requestWrapper, responseWrapper);
    //HttpStatus responseStatus = HttpStatus.valueOf(responseWrapper.getStatus());
    String responseBody = IOUtils.toString(responseWrapper.getContentInputStream(), StandardCharsets.UTF_8);
    JsonNode responseJson = objectMapper.readTree(responseBody);

    responseWrapper.copyBodyToResponse();
  }

  private boolean isExcludePathPatterns(String request) {
    return excludePathPatterns.stream().anyMatch(request::contains);
  }

  private void errorMessageFlush(HttpServletResponse response, String requestUrl) throws IOException {
    ResponseResult responseResult = new ResponseResult(ResultCode.InvalidRequest.getCode()
            , new RestapiInvalidRequestException(), requestUrl);

    ObjectMapper mapper = new ObjectMapper();
    PrintWriter out = response.getWriter();
    out.print(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(responseResult));
    out.flush();
  }
}

AbstractBaseRestResponse

 

package com.program.commons.response;

import com.program.commons.exception.*;
import com.program.log.Log;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@EnableWebMvc
@ControllerAdvice
@RestControllerAdvice
public class AbstractBaseRestResponse {

  @ExceptionHandler(AbstractRestapiBaseException.class)
  public ResponseResult abstractBaseException(HttpServletRequest req, HttpServletResponse res, final AbstractRestapiBaseException exception){
    Log.error("abstractBaseException : ", exception.getMessage());
    res.setStatus(exception.getHttpStatus().value());
    return new ResponseResult(ResultCode.Base_Error.getCode(), exception, req.getRequestURL().toString());
  }

  @ExceptionHandler(IllegalArgumentException.class)
  @ResponseStatus(value= HttpStatus.BAD_REQUEST)
  public ResponseResult illegalArgumentException(HttpServletRequest req, final IllegalArgumentException exception){
    Log.error("illegalArgumentException : ", exception.getMessage());
    return new ResponseResult(ResultCode.IllegalArgument.getCode(), new RestapiInvalidRequestException(exception.getMessage(), exception), req.getRequestURL().toString());
  }

  @ExceptionHandler({Throwable.class, RuntimeException.class, RestapiUnknownException.class})
  @ResponseStatus(value= HttpStatus.INTERNAL_SERVER_ERROR)
  ResponseResult handleControllerException(HttpServletRequest req, Throwable ex) {
    Log.error("handleControllerException : "+ ex.getMessage());
    String internalMessage = "서버에 오류가 발생했습니다.";
    //ResponseResult message = new ResponseMessage(ErrorCode.Throwable.getCode(), new RestapiUnknownException(internalMessage, ex), req.getRequestURL().toString());
    return new ResponseResult(ResultCode.INTERNAL_SERVER_ERROR.getCode(), new RestapiUnknownException(internalMessage, ex), req.getRequestURL().toString());
    //return new ResponseEntity<Object>(message, HttpStatus.INTERNAL_SERVER_ERROR);
  }
}

 

ErrorMessage

package com.program.commons.response;

import lombok.Getter;
import lombok.Setter;
import org.apache.commons.pool2.BaseObject;

@Getter
@Setter
public class ErrorMessage extends BaseObject {
  private static final long serialVersionUID = 1L;

  private final int code;
  private final String errorMessage;
  private final String referedUrl;

  public ErrorMessage(int code, String errorMessage, String referedUrl) {
    super();
    this.code = code;
    this.errorMessage = errorMessage;
    this.referedUrl = referedUrl;
  }
}

ResponseResult

package com.program.commons.response;

import com.program.commons.exception.AbstractRestapiBaseException;
import com.program.commons.exception.RestapiResultException;
import lombok.Getter;
import com.google.gson.annotations.Expose;
import org.apache.commons.pool2.BaseObject;
import org.springframework.http.HttpStatus;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

@Getter
public class ResponseResult extends BaseObject {
  private static final long serialVersionUID = 1L;
  private static final String DEFAULT_KEY = "result";

  @Expose
  private int code;

  @Expose
  private boolean status;

  @Expose
  private String message;

  @Expose
  private Date timestamp;

  @Expose
  private Map<String, Object> data;

  @Expose
  private ErrorMessage error;

  public ResponseResult() {
    this(HttpStatus.OK);
  }

  public ResponseResult(HttpStatus httpStatus) {
    InitResponseMessage(httpStatus.getReasonPhrase());
  }

  public ResponseResult(Map<String, Object> result) {
    this.data = result;

    InitResponseMessage("");
  }

  public ResponseResult(ResultCode resultCode) {
    InitResponseMessage("");
  }

  public ResponseResult(String key, Object value) {
    this.data = new HashMap<>();
    this.data.put(key, value);

    InitResponseMessage("");
  }

  public ResponseResult(int errorCode, AbstractRestapiBaseException ex, String referedUrl) {
    HttpStatus httpStatus = ex.getHttpStatus();
    this.error = new ErrorMessage(errorCode, ex.getMessage(), referedUrl);
    this.data = null;

    InitResponseErrorMessage(httpStatus);
  }

  public ResponseResult(ResultCode resultCode, AbstractRestapiBaseException ex, String referedUrl) {
    HttpStatus httpStatus = ex.getHttpStatus();
    this.error = new ErrorMessage(resultCode.getCode(), ex.getMessage(), referedUrl);
    this.data = null;

    InitResponseErrorMessage(httpStatus);
  }

  private void InitResponseErrorMessage(HttpStatus httpStatus){
    this.code = httpStatus.value();
    this.status = httpStatus.is2xxSuccessful();
    this.message = httpStatus.getReasonPhrase();
    this.timestamp = new Date();
  }

  private void InitResponseMessage(String message) {
    this.code = ResultCode.Success.getCode(); //code;
    this.status = true;
    this.message = message;
    this.timestamp = new Date();
  }

  public void add(String key, Object value) {
    if(this.data == null) {
      this.data = new HashMap<>();
    }

    this.data.put(key, value);
  }
}

ResultCode

package com.program.commons.response;

public enum ResultCode {
  Base_Error(1),
  IllegalArgument(2),
  Throwable(3),
  Binding(4),
  InvalidRequest(5),

  INTERNAL_SERVER_ERROR(56),
  INTERNAL_SERVER_SQL_CALL_ERROR(60),

  Success(200),

  NOT_FOUND(404),

  private int value;

  ResultCode(int value) {
    this.value = value;
  }
  public int getCode() { return this.value; }
}

AclSecurity

package com.program.commons.security;

import com.program.commons.util.StringUtil;
import com.program.log.Log;
import org.springframework.stereotype.Component;

import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

@Component
public class AclSecurity {
  public static Map<String, IpCount> ipCountMap = new HashMap<>();
  public static Map<String, Object> whiteIpList = new HashMap<>();
  public static Map<String, Object> blackIpList = new HashMap<>();

  String CONFIG_HOME = "/home/user/checklist";
  String BLACKLIST_FILE = CONFIG_HOME + "/blacklist.dat";
  String WHITELIST_FILE = CONFIG_HOME + "/whitelist.dat";
  int MIN_API_GAP = 1000;
  int MAX_API_COUNT_PER_SECOND = 60;

  public AclSecurity() {
    setIpList(BLACKLIST_FILE, blackIpList);
    setIpList(WHITELIST_FILE, whiteIpList);
  }

  private void setIpList(String filePath, Map<String, Object> ipMap) {
    Log.info("AclSecurity Init 시작 Path = {}", filePath);

    try {
      BufferedReader reader = new BufferedReader(new FileReader(filePath));

      String ip;

      while ((ip = reader.readLine()) != null) {
        if(StringUtil.isEmptyString(ip) || !ip.startsWith("#")) {
          continue;
        }

        ipMap.put(ip, new Date());
      }
    } catch (FileNotFoundException ex) {
      Log.info("AclSecurity Init파일이 존재하지 않습니다. Path = {}", filePath);
    }
    catch (Exception ex) {
      Log.error("AclSecurity Init 실패 Path = {}", filePath);
    }

    Log.info("AclSecurity Init 종료 Path = {}", filePath);
  }

  public boolean isBlackListIp(String ip, String url) {
    if(blackIpList.containsKey(ip)) {
      Log.error("Black ip is blocked, ip = {}, url ={}", ip, url);
      return true;
    }

    if(ipCountMap.size() > 1000) {
      Log.error("Black ip is many list, ip = {}, url ={}", ip, url);
      return false;
    }

    Date now = new Date();

    if(!ipCountMap.containsKey(ip)) {
      IpCount ipCount = new IpCount(0, now);
      ipCountMap.put(ip, ipCount);
    } else {
      IpCount ipCount = ipCountMap.get(ip);
      long gapTime = now.getTime() - ipCount.getDate().getTime();

      if(gapTime > MIN_API_GAP) {
        ipCount.setDate(now);
        ipCount.setCount(0);
        ipCountMap.put(ip, ipCount);
        return false;
      }

      ipCount.setCount(ipCount.getCount() + 1);

      if(ipCount.getCount() > MAX_API_COUNT_PER_SECOND) {
        blackIpList.put(ip, now);
        return true;
      }
    }

    return false;
  }

  public boolean isWhiteListIp(String ip) {
    return whiteIpList.containsKey(ip);
  }
}

 

IpCount

package com.program.commons.security;

import lombok.Getter;
import lombok.Setter;

import java.util.Date;

@Getter
@Setter
public class IpCount {
  private Date date;
  private int count;

  public IpCount(int count, Date date) {
    this.count = count;
    this.date = date;
  }
}

SecurityUtil

package com.program.commons.security;

import com.program.log.Log;
import org.apache.commons.codec.binary.Hex;
import org.springframework.stereotype.Component;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Base64;

@Component
public class SecurityUtil {
  //https://www.devglan.com/online-tools/aes-encryption-decryption, CBC, 256, lv, key
  private static String alg = "AES/CBC/PKCS5Padding";

  public static String getSecureSha256(String value) {
    String sha256 = null;

    try {
      MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
      messageDigest.update(value.getBytes(StandardCharsets.UTF_8));
      sha256 = Hex.encodeHexString(messageDigest.digest());
    } catch (Exception ex) {
      Log.error("SecurityUtil Init Failed message = {}", ex.getMessage());
    }

    return sha256;
  }

  public static String getSecureAes256Encrypt(String value, String key) throws Exception {
    String iv = key.substring(0, 16); // 16byte

    Cipher cipher = Cipher.getInstance(alg);
    SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(), "AES");
    IvParameterSpec ivParamSpec = new IvParameterSpec(iv.getBytes());
    cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivParamSpec);

    byte[] encrypted = cipher.doFinal(value.getBytes("UTF-8"));
    return Base64.getEncoder().encodeToString(encrypted);
  }

  public static String getSecureAes256Decrypt(String value, String key) throws Exception {
    String iv = key.substring(0, 16); // 16byte

    Cipher cipher = Cipher.getInstance(alg);
    SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(), "AES");
    IvParameterSpec ivParamSpec = new IvParameterSpec(iv.getBytes());
    cipher.init(Cipher.DECRYPT_MODE, keySpec, ivParamSpec);

    byte[] decodedBytes = Base64.getDecoder().decode(value);
    byte[] decrypted = cipher.doFinal(decodedBytes);
    return new String(decrypted, "UTF-8");
  }
}

JsonConverter

package com.program.commons.util;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonSyntaxException;
import org.springframework.stereotype.Component;

import java.lang.reflect.Type;
import java.util.List;
import java.util.Map;

public class JsonConverter {
  private static final Gson gson = new GsonBuilder()
          .disableHtmlEscaping()
          .setPrettyPrinting()
          .serializeNulls()
          .excludeFieldsWithoutExposeAnnotation()
          .serializeSpecialFloatingPointValues()
          .create();

  public static <T extends Map<?, ?>> String convert(T obj) {
    return gson.toJson(obj);
  }

  public static String convert(Object obj){
    return gson.toJson(obj);
  }

  public static <T extends Class<?>, V> String convert(T cls, V obj) {
    return gson.toJson(obj);
  }

  public static <T, V extends String> List<T> convertToClassList(Class<T> cls, V value) {
    return gson.fromJson(value, com.google.gson.reflect.TypeToken.getParameterized(List.class, cls).getType());
  }

  public static <T, V extends String> T convertToClass(Class<T> cls, V value) throws JsonSyntaxException {
    return gson.fromJson(value, (Type)cls);
  }

  public static String toJson(Object object) {
    if (object == null)
      return null;

    ObjectMapper objectMapper = new ObjectMapper();
    objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
    String json = "";
    try {
      json = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(object);
    } catch (Exception e) {
      e.printStackTrace();
    }

    return json;
  }
}

CommonUtil

package com.program.commons.util;

import java.lang.reflect.Array;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;

public class CommonUtil {
  public static boolean isEmpty(Object value) {
    if(value == null) {
      return true;
    }

    if(value instanceof String) {
      return isEmptyString(String.valueOf(value));
    } else if(value instanceof List) {
      return isEmptyList((List<?>) value);
    } else if(value instanceof Map) {
      return isEmptyMap((Map<?, ?>) value);
    } else if(value instanceof Optional) {
      return isEmptyOptional((Optional<?>) value);
    } else if(value instanceof CharSequence) {
      return isEmptyCharSequence((CharSequence) value);
    } else if(value instanceof Collection) {
      return isEmptyCollection((Collection) value);
    } else if(value instanceof Object[]) {
      return isEmptyArray((Object[]) value);
    } else {
      return false;
    }
  }

  public static boolean isEmptyString(String value) {
    return (value == null || "".equals(value.trim()));
  }

  public static boolean isEmptyList(List<?> value) {
    return (value == null || value.isEmpty());
  }

  public static boolean isEmptyMap(Map<?, ?> value) {
    return (value == null || value.isEmpty());
  }

  public static boolean isEmptyOptional(Optional<?> value) {
    return value.isEmpty();
  }

  public static boolean isEmptyCharSequence(CharSequence value) {
    return (value == null || value.length() == 0);
  }

  public static boolean isEmptyCollection(Collection value) {
    return (value == null || value.isEmpty());
  }

  public static boolean isEmptyArray(Object[] value) {
    return (value == null || Array.getLength(value) == 0);
  }
}

application 프로젝트

BatchAnnotation

package com.program.application.aspect;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface BatchAnnotation {
  String description() default "배치 프로세스";
}

BatchAspect

package com.program.application.aspect;

import com.program.commons.util.StringUtil;
import com.program.log.Log;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

@Component
@Aspect
public class BatchAspect {
  private static String start = "시작";
  private static String end = "종료";
  private static String error = "오류 = {}";

  @Around("@annotation(com.program.application.aspect.BatchAnnotation)")
  public Object doSchedulerLog(ProceedingJoinPoint joinPoint) throws Throwable {
    MethodSignature signature = (MethodSignature) joinPoint.getSignature();
    Method method = signature.getMethod();
    String methodName = joinPoint.getSignature().getName();
    String description = method.getAnnotation(BatchAnnotation.class).description();
    Object obj = null;

    try {
      Log.batchInfo(StringUtil.getConcatString(methodName, description, start));
      obj = joinPoint.proceed();
    } catch (Exception ex) {
      Log.batchErrorInfo(StringUtil.getConcatString(methodName, description, error), ex.getMessage());
    } finally {
      Log.batchInfo(StringUtil.getConcatString(methodName, description, end));
    }

    return obj;
  }
}

DataSourceConfig

package com.program.application.config.database;

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.core.env.Environment;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Objects;

@Configuration
@EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class})
@EnableTransactionManagement
@EnableJpaRepositories(basePackages = { "com.program.application.repository.*" })
public class DataSourceConfig {

  @Autowired Environment env;

  @Bean
  public DataSource masterDataSource() {
    return this.buildDataSource(
            env.getProperty("spring.datasource.driver-class-name"),
            env.getProperty("spring.datasource.url.read-write"),
            env.getProperty("spring.datasource.username"),
            env.getProperty("spring.datasource.password"),
            "master-read-write-pool",
            Integer.parseInt(Objects.requireNonNull(env.getProperty("spring.datasource.hikari.maximum-pool-size"))),
            false,
            "SET @enckey   = '".concat(Objects.requireNonNull(env.getProperty("spring.datasource.encrypt-key"))).concat("'")
    );
  }

  @Bean
  public DataSource slaveDataSource() {
    return this.buildDataSource(
            env.getProperty("spring.datasource.driver-class-name"),
            env.getProperty("spring.datasource.url.read-only"),
            env.getProperty("spring.datasource.username"),
            env.getProperty("spring.datasource.password"),
            "salve-read-only-pool",
            Integer.parseInt(Objects.requireNonNull(env.getProperty("spring.datasource.hikari.maximum-pool-size"))),
            false,
            "SET @enckey   = '".concat(Objects.requireNonNull(env.getProperty("spring.datasource.encrypt-key"))).concat("'")
    );
  }

  @Bean
  public DataSource routingDataSource(@Qualifier("masterDataSource") DataSource masterDataSource,
                                      @Qualifier("slaveDataSource") DataSource slaveDataSource) {
    var routingDataSource = new ReplicationRoutingDataSource();

    var dataSourceMap = new HashMap<>();
    dataSourceMap.put("master", masterDataSource);
    dataSourceMap.put("slave", slaveDataSource);
    routingDataSource.setTargetDataSources(dataSourceMap);
    routingDataSource.setDefaultTargetDataSource(masterDataSource);

    return routingDataSource;
  }

  @Primary
  @Bean
  public DataSource dataSource(@Qualifier("routingDataSource") DataSource routingDataSource) {
    return new LazyConnectionDataSourceProxy(routingDataSource);
  }

  private DataSource buildDataSource(
          String driverClassName,
          String jdbcUrl,
          String username,
          String password,
          String poolName,
          int maximumPoolSize,
          boolean autoCommit,
          String connectionInitSql
  ) {
    HikariConfig config = new HikariConfig();
    config.setDriverClassName(driverClassName);
    config.setJdbcUrl(jdbcUrl);
    config.setUsername(username);
    config.setPassword(password);
    config.setPoolName(poolName);
    config.setMaximumPoolSize(maximumPoolSize);
    config.setAutoCommit(autoCommit);
    config.setConnectionInitSql(connectionInitSql);

    return new HikariDataSource(config);
  }

}

ReplicationRoutingDataSource

package com.program.application.config.database;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.transaction.support.TransactionSynchronizationManager;

public class ReplicationRoutingDataSource extends AbstractRoutingDataSource {
  @Override
  protected Object determineCurrentLookupKey() {
    return TransactionSynchronizationManager.isCurrentTransactionReadOnly() ? RoutingDataSourceLookupKey.SLAVE : RoutingDataSourceLookupKey.MASTER;
  }

  public enum RoutingDataSourceLookupKey{
    MASTER, SLAVE
  }
}

BatchConfig

package com.program.application.config;

import com.program.application.scheduler.DevRedisModeScheduler;
import com.program.application.scheduler.MyScheduler;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;

@EnableScheduling
@Configuration
public class BatchConfig {
  @Bean public MyScheduler myScheduler() { return new MyScheduler(); }

  @Bean
  @ConditionalOnProperty(value = "server.mode", havingValue = "dev")
  public DevRedisModeScheduler devRedisModeScheduler() {
    return new DevRedisModeScheduler();
  }
}

UserController

package com.program.application.controller;

import com.program.application.entity.main.UserEntity;
import com.program.application.service.main.IUserService;
import com.program.commons.response.ResponseResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;

@RestController
@RequestMapping(value = "/user", produces = "application/json; charset=UTF-8")
public class UserController {
  @Autowired
  @Qualifier("IUserMasterServiceImpl")
  private IUserService userMasterService;

  @Autowired
  @Qualifier("IUserSlaveServiceImpl")
  private IUserService userSlaveService;

  @PostMapping(value = "/join")
  public ResponseResult setUserJoin(HttpServletRequest request, @RequestBody(required = true) UserEntity user) {
    return userMasterService.setUserJoin(request, user);
  }
}

UserEntity

package com.program.application.entity.main;

import lombok.Data;

import javax.persistence.*;
import java.util.Date;

@Entity
@Table(name = "t_user")
@Data
/*@Table(
        name="tableName",
        uniqueConstraints={
                @UniqueConstraint(
                        name={"contstraintName"}
                        columnNames={"col1", "col2"}
                )
        }
)*/
public class UserEntity {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private int idx;

  @Column(name ="user_key", unique = true)
  private String userKey;

  private String email;

  private String password;

  @Column(name ="create_time")
  private Date createTime;
}

 

IRepository

package com.program.application.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.repository.NoRepositoryBean;

import javax.transaction.Transactional;
import java.util.List;
import java.util.Optional;

@NoRepositoryBean
@Transactional
public interface IRepository<E, T> extends JpaRepository<E, T> {
  @Override
  List<E> findAll();

  @Override
  Optional<E> findById(final T id);

  @Override
  List<E> findAllById(Iterable<T> idIterable);

  @Override
  void delete(final E entity);

  @Override
  <S extends E> S save(final S entity);

  @Override
  @Transactional
  <S extends E> S saveAndFlush(final S entity);

  @Override
  <S extends E> List<S> saveAll(final Iterable<S> entities);

  @Override
  <S extends E> List<S> saveAllAndFlush(final Iterable<S> entities);
}

IUserRepository

package com.program.application.repository.main;

import com.program.application.entity.main.UserEntity;
import com.program.application.repository.IRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import org.springframework.data.jpa.repository.Modifying;
import javax.transaction.Transactional;

@Repository
public interface IUserRepository extends IRepository<UserEntity, Long> {
  /*@Query(value = "SELECT * FROM address", nativeQuery = true)
  List<AddressEntity> findAddressList();*/

  @Query(value = "SELECT COUNT(*) FROM t_user WHERE email = HEX(AES_ENCRYPT(:email, @enckey))", nativeQuery = true)
  Integer getUserEntityCountByEmail(@Param("email") String userEmail);

  @Transactional
  @Modifying(clearAutomatically = true)
  @Query(value = "INSERT INTO t_user (`user_key`, `email`, `password`)"
          + " VALUES (uuid(), HEX(AES_ENCRYPT(:#{#user.email}, @enckey)), :#{#user.password})", nativeQuery = true)
  /*@Modifying(flushAutomatically = true, clearAutomatically = true)
  @Transactional*/
  Integer insertUserEntity(@Param("user") UserEntity user);
}

 

DevRedisModeScheduler

package com.program.application.scheduler;

import com.program.application.aspect.BatchAnnotation;
import com.program.application.model.DevRedisUser;
import com.program.log.Log;
import org.springframework.scheduling.annotation.Scheduled;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;

public class DevRedisModeScheduler {
  @Scheduled(cron = "*/10 * * * * *")
  @BatchAnnotation(description = "로그인 객체 회수 스케쥴러")
  public void monitor() {
    try {
      Date now = new Date();
      List<String> removeUserList = new ArrayList<>();

      for(String key : DevRedisUser.redisUserMap.keySet()) {
        Date expireTime = DevRedisUser.redisUserMap.get(key);

        if(expireTime.before(now)) {
          removeUserList.add(key);
        }
      }

      Log.batchInfo("회수 로그인 Count = {}", removeUserList.size());

      for (String s : removeUserList) {
        DevRedisUser.redisUserMap.remove(s);
      }
    } catch (Exception e) {
      Log.batchErrorInfo("로그인 객체 회수 스케쥴러 오류. message={}", e.getMessage());
    }
  }
}

IUserService

package com.program.application.service.main;

import com.program.application.entity.main.UserEntity;
import com.program.commons.response.ResponseResult;

import javax.servlet.http.HttpServletRequest;

public interface IUserService {
  ResponseResult getUserInfo();

  ResponseResult setUserJoin(HttpServletRequest request, UserEntity user);
}

IUserMasterServiceImpl

package com.program.application.service.main.master;

import com.program.application.entity.main.UserEntity;
import com.program.application.repository.main.IUserRepository;
import com.program.application.service.main.IUserService;
import com.program.commons.exception.RestapiResultException;
import com.program.commons.exception.RestapiUnknownException;
import com.program.commons.response.ResponseResult;
import com.program.commons.response.ResultCode;
import com.program.commons.security.SecurityUtil;
import com.program.commons.util.StringUtil;
import com.program.log.Log;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.servlet.http.HttpServletRequest;

@Service
@Transactional
public class IUserMasterServiceImpl implements IUserService {
  @Autowired IUserRepository userRepository;

  public ResponseResult getUserInfo() {
    return null;
  }

  @Override
  public ResponseResult setUserJoin(HttpServletRequest request, UserEntity user) {
    if(StringUtil.isEmptyString(user.getEmail()) || StringUtil.isEmptyString(user.getPassword())) {
      throw new RestapiResultException(ResultCode.InvalidParameter.getCode(), "유효하지 않은 정보입니다.");
    }

    int duplicateCount = userRepository.getUserEntityCountByEmail(user.getEmail());

    if(duplicateCount > 0) {
      throw new RestapiResultException(ResultCode.DuplicateUser.getCode(), "이미 가입된 정보가 존재합니다.");
    }

    try {
      String aesKey = 암호키;
      
      String decryptPassword = SecurityUtil.getSecureAes256Decrypt(user.getPassword(), aesKey);
      user.setPassword(decryptPassword);
      userRepository.insertUserEntity(user);
      userRepository.flush();
    } catch (Exception ex) {
      Log.error("setUserJoin message = {}", ex.getMessage());
      throw new RestapiUnknownException(ResultCode.INTERNAL_SERVER_ERROR.getCode(), "서버에 장애가 발생했습니다.");
    }

    return new ResponseResult(ResultCode.Success);
  }
}

IUserSlaveServiceImpl

package com.program.application.service.main.slave;

import com.program.application.aspect.UnSupportSqlServiceAnnotation;
import com.program.application.entity.main.UserEntity;
import com.program.application.service.main.IUserService;
import com.program.commons.exception.RestapiUnSupportSqlException;
import com.program.commons.response.ResponseResult;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.servlet.http.HttpServletRequest;

@Service
@Transactional(readOnly = true)
public class IUserSlaveServiceImpl implements IUserService {
  @Override
  public ResponseResult getUserInfo() {
    return null;
  }

  @Override
  @UnSupportSqlServiceAnnotation
  public ResponseResult setUserJoin(HttpServletRequest request, UserEntity user) {
    throw new RestapiUnSupportSqlException();
  }
}

 

 

 

 

*220811

멀티프로젝트 배포 시 설정을 각 모듈로 나눠진 내용을 최상단으로 변경

 

로컬 테스트 시 설정 변경

스크립트로 실행 확인

 

* 오라클 클라우드에서 해당 방화벽 오픈 후 centos7에서도 오픈해야함.

띄워쓰기 명령어 오류났던건 신경쓰지 말자.
결과

 

'포트폴리오 > Java rest api' 카테고리의 다른 글

Reuters 환율 처리  (1) 2022.11.18
중계 서버  (0) 2022.11.18
rest api filter에서 header, body 변경  (0) 2022.09.07
자바 날씨 API  (0) 2021.06.09
Java 기본 Rest api  (0) 2020.08.11