지인의 요청으로 간단한 서버 구현.
* 보안 및 민감한 부분은 코드가 없을 수 있습니다. 이 포스팅은 누군가에게 프로그램 구현을 위한 공유가 아닙니다.
* 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 |