diff --git a/apigateway/Dockerfile b/apigateway/Dockerfile new file mode 100644 index 0000000..ce1d5d0 --- /dev/null +++ b/apigateway/Dockerfile @@ -0,0 +1,15 @@ +# openjdk8 base image +FROM openjdk:8-jre-alpine + +# directory 생성 +RUN mkdir -p /usr/app/msa-attach-volume/messages +# jar 파일이 복사되는 위치 +ENV APP_HOME=/usr/app/ +# 작업 시작 위치 +WORKDIR $APP_HOME +# jar 파일 복사 +COPY build/libs/*.jar apigateway.jar +# application port +EXPOSE 8000 +# 실행 +CMD ["java", "-Dspring.profiles.active=${profile:default}", "-jar", "apigateway.jar"] diff --git a/apigateway/DockerfileK8s b/apigateway/DockerfileK8s new file mode 100644 index 0000000..d9b2e0c --- /dev/null +++ b/apigateway/DockerfileK8s @@ -0,0 +1,4 @@ +FROM openjdk:8-jre-alpine +WORKDIR /app/ +COPY build/libs/*.jar app.jar +CMD ["java", "-Dlogstash.url=logstash.default.svc.cluster.local:8088", "-jar", "app.jar"] diff --git a/apigateway/Jenkinsfile b/apigateway/Jenkinsfile new file mode 100644 index 0000000..988557c --- /dev/null +++ b/apigateway/Jenkinsfile @@ -0,0 +1,48 @@ +// https://github.com/jenkinsci/kubernetes-plugin +podTemplate( + name: "jenkins-slave", + serviceAccount: "jenkins", + containers: [ + containerTemplate(name: "docker", image: "docker", ttyEnabled: true, command: 'cat'), + containerTemplate(name: "kubectl", image: "lachlanevenson/k8s-kubectl:v1.17.5", ttyEnabled: true, command: 'cat') + ], + volumes: [ + hostPathVolume(mountPath: "/var/run/docker.sock", hostPath: "/var/run/docker.sock") + ] +) { + node(POD_LABEL) { + def now = java.time.LocalDateTime.now() + def projectId = "apigateway" // 프로젝트 아이디 + + // docker + def dockerhubCredentialsId = "username-password-dockerhub" // 젠킨스 도커허브 자격증명 + def dockerImage = "egovframe/egovframe-msa-edu-backend-${projectId}" // 도커허브 이미지 + def dockerImageTag = "latest" // 도커허브 이미지 태그 + + stage("git") { + checkout scm + } + + stage("gradle") { + sh "./gradlew wrap --gradle-version=7.3 --daemon" + sh "./gradlew bootJar" + } + + stage("docker") { + container("docker") { + withCredentials([usernamePassword(credentialsId: "${dockerhubCredentialsId}", passwordVariable: "password", usernameVariable: "username")]) { + sh "docker login -u $username -p $password" + sh "docker build -f DockerfileK8s -t ${dockerImage}:${dockerImageTag} ." + sh "docker push ${dockerImage}:${dockerImageTag}" + } + } + } + + stage( "kubernetes" ) { + container("kubectl") { + sh "kubectl rollout restart deployment ${projectId}-deployment" + sh "kubectl annotate deployment.apps/${projectId}-deployment kubernetes.io/change-cause='Image updated at ${now} by ${env.BUILD_TAG}.'" + } + } + } +} \ No newline at end of file diff --git a/apigateway/build.gradle b/apigateway/build.gradle new file mode 100644 index 0000000..3120e5e --- /dev/null +++ b/apigateway/build.gradle @@ -0,0 +1,60 @@ +plugins { + id 'org.springframework.boot' version '2.7.12' + id 'org.sonarqube' version '3.5.0.2730' + id 'io.spring.dependency-management' version '1.1.0' + id 'java' +} + +group = 'org.egovframe.cloud' +version = '1.0.0' +sourceCompatibility = '1.8' + +repositories { + mavenCentral() +} + +ext { + set('springCloudVersion', '2021.0.7') + set('log4j2.version', '2.20.0') // log4j 보안 패치 +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-actuator' + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-webflux' + implementation 'org.springframework.cloud:spring-cloud-starter-gateway' + implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client' + implementation 'org.springframework.cloud:spring-cloud-starter-config' // config + implementation 'org.springframework.cloud:spring-cloud-starter-bootstrap' // config + implementation 'org.springframework.cloud:spring-cloud-starter-bus-amqp' // bus + implementation 'org.apache.tomcat.embed:tomcat-embed-el:9.0.73' + implementation 'net.logstash.logback:logstash-logback-encoder:7.4' // logstash logback + implementation 'io.jsonwebtoken:jjwt:0.9.1' + implementation 'javax.xml.bind:jaxb-api:2.3.1' + + // Resilience4j 의존성 추가 + implementation 'io.github.resilience4j:resilience4j-spring-boot2:1.7.0' + implementation 'io.github.resilience4j:resilience4j-reactor:1.7.0' + implementation 'io.micrometer:micrometer-registry-prometheus' + + implementation 'org.springdoc:springdoc-openapi-webflux-ui:1.7.0' + implementation 'com.github.ben-manes.caffeine:caffeine' + + // macOS용 Netty DNS 리졸버 네이티브 라이브러리 추가 + implementation 'io.netty:netty-resolver-dns-native-macos:4.1.79.Final:osx-x86_64' + implementation 'io.netty:netty-resolver-dns-native-macos:4.1.79.Final:osx-aarch_64' + + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.springframework.boot:spring-boot-starter-test' +} + +dependencyManagement { + imports { + mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" + } +} + +test { + useJUnitPlatform() +} diff --git a/apigateway/gradle/wrapper/gradle-wrapper.jar b/apigateway/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..e708b1c Binary files /dev/null and b/apigateway/gradle/wrapper/gradle-wrapper.jar differ diff --git a/apigateway/gradle/wrapper/gradle-wrapper.properties b/apigateway/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..b1159fc --- /dev/null +++ b/apigateway/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/apigateway/gradlew b/apigateway/gradlew new file mode 100644 index 0000000..4f906e0 --- /dev/null +++ b/apigateway/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/apigateway/gradlew.bat b/apigateway/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/apigateway/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/apigateway/manifest.yml b/apigateway/manifest.yml new file mode 100644 index 0000000..bbff6ae --- /dev/null +++ b/apigateway/manifest.yml @@ -0,0 +1,16 @@ +--- +applications: + - name: egov-apigateway # CF push 시 생성되는 이름 +# memory: 512M # 메모리 + instances: 1 # 인스턴스 수 + host: egov-apigateway # host 명으로 유일해야 함 + path: build/libs/apigateway-0.1.jar # build 후 생성된 jar 위치 + buildpack: java_buildpack # cf buildpacks 명령어로 java buildpack 이름 확인 + services: + - egov-discovery-provided-service # discovery service binding + env: + spring_profiles_active: cf + spring_cloud_config_uri: http://localhost:8888 + app_name: egov-apigateway # logstash custom app name + TZ: Asia/Seoul + JAVA_OPTS: -Xss349k diff --git a/apigateway/settings.gradle b/apigateway/settings.gradle new file mode 100644 index 0000000..ed34aee --- /dev/null +++ b/apigateway/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'apigateway' diff --git a/apigateway/src/main/java/org/egovframe/cloud/apigateway/ApigatewayApplication.java b/apigateway/src/main/java/org/egovframe/cloud/apigateway/ApigatewayApplication.java new file mode 100644 index 0000000..d6d315e --- /dev/null +++ b/apigateway/src/main/java/org/egovframe/cloud/apigateway/ApigatewayApplication.java @@ -0,0 +1,33 @@ +package org.egovframe.cloud.apigateway; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; + +/** + * org.egovframe.cloud.apigateway.ApigatewayApplication + *
+ * 게이트웨이 어플리케이션 클래스 + * Eureka Client 로 설정했기 때문에 Eureka Server 가 먼저 기동되어야 한다. + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/06/30 + * + *
+ * << 개정이력(Modification Information) >> + * + * 수정일 수정자 수정내용 + * ---------- -------- --------------------------- + * 2021/06/30 jaeyeolkim 최초 생성 + *+ */ +@EnableDiscoveryClient +@SpringBootApplication +public class ApigatewayApplication { + + public static void main(String[] args) { + SpringApplication.run(ApigatewayApplication.class, args); + } + +} diff --git a/apigateway/src/main/java/org/egovframe/cloud/apigateway/api/MessageSourceApiController.java b/apigateway/src/main/java/org/egovframe/cloud/apigateway/api/MessageSourceApiController.java new file mode 100644 index 0000000..70a925c --- /dev/null +++ b/apigateway/src/main/java/org/egovframe/cloud/apigateway/api/MessageSourceApiController.java @@ -0,0 +1,43 @@ +package org.egovframe.cloud.apigateway.api; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.MessageSource; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Locale; + +/** + * org.egovframe.cloud.apigateway.api.MessageSourceApiController + *
+ * MessageSource 정상 확인을 위한 컨트롤러 + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/08/10 + * + *
+ * << 개정이력(Modification Information) >> + * + * 수정일 수정자 수정내용 + * ---------- -------- --------------------------- + * 2021/08/10 jaeyeolkim 최초 생성 + * 2024/09/24 이백행 컨트리뷰션 이클립스 문제(Problems) 해결 + *+ */ +@Slf4j +@RequiredArgsConstructor +@RestController +public class MessageSourceApiController { + private final MessageSource messageSource; + + @GetMapping("/api/v1/messages/{code}/{lang}") + public String getMessage(@PathVariable String code, @PathVariable String lang) { + Locale locale = "en".equals(lang)? Locale.ENGLISH : Locale.KOREAN; + String message = messageSource.getMessage(code, null, locale); + log.info("code={}, lang={}, message={}", code, lang, message); + return message; + } +} diff --git a/apigateway/src/main/java/org/egovframe/cloud/apigateway/config/MessageSourceConfig.java b/apigateway/src/main/java/org/egovframe/cloud/apigateway/config/MessageSourceConfig.java new file mode 100644 index 0000000..b070d64 --- /dev/null +++ b/apigateway/src/main/java/org/egovframe/cloud/apigateway/config/MessageSourceConfig.java @@ -0,0 +1,64 @@ +package org.egovframe.cloud.apigateway.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.apigateway.config.MessageSourceConfig + *
+ * Spring MessageSource 설정 + * Message Domain 이 있는 portal-service 에서 messages.properties 를 공유 가능한 외부 위치에 생성한다. + * 각 서비스에서 해당 파일을 통해 다국어를 지원하도록 한다. + * module-common.jar 를 포함하지 않는 서비스에서는 이 configuration을 추가해주어야 한다. + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/08/09 + * + *
+ * << 개정이력(Modification Information) >> + * + * 수정일 수정자 수정내용 + * ---------- -------- --------------------------- + * 2021/08/09 jaeyeolkim 최초 생성 + *+ */ +@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); + log.info("DB MessageSource location = {}", dbMessages); + 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; + } +} diff --git a/apigateway/src/main/java/org/egovframe/cloud/apigateway/config/OpenApiDocConfig.java b/apigateway/src/main/java/org/egovframe/cloud/apigateway/config/OpenApiDocConfig.java new file mode 100644 index 0000000..7b674e1 --- /dev/null +++ b/apigateway/src/main/java/org/egovframe/cloud/apigateway/config/OpenApiDocConfig.java @@ -0,0 +1,38 @@ +package org.egovframe.cloud.apigateway.config; + +import org.springdoc.core.GroupedOpenApi; +import org.springdoc.core.SwaggerUiConfigParameters; +import org.springframework.cloud.gateway.route.RouteDefinition; +import org.springframework.cloud.gateway.route.RouteDefinitionLocator; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; + +@Configuration +public class OpenApiDocConfig { + + @Bean + @Lazy(false) + public List
+ * 요청 제한(Rate Limiting) 설정 클래스 + * 동일한 IP 또는 경로에 대한 과도한 요청을 제한하기 위한 설정 + * + * @author changwng + * @version 1.0 + * @since 2025/03/19 + * + *
+ * << 개정이력(Modification Information) >> + * + * 수정일 수정자 수정내용 + * ---------- -------- --------------------------- + * 2025/03/19 changwng 최초 생성 + *+ */ +@Slf4j +@Configuration +public class RateLimiterConfig { + + /** + * IP 주소 기반 요청 제한을 위한 KeyResolver 빈 + * 클라이언트 IP 주소를 키로 사용하여 요청 제한을 적용 + * + * @return KeyResolver IP 주소 기반 키 리졸버 + */ + // @Bean 주석 처리 - 다중 KeyResolver 빈 문제 해결 + public KeyResolver ipKeyResolver() { + return exchange -> { + String ip = exchange.getRequest().getRemoteAddress().getAddress().getHostAddress(); + log.debug("Request IP: {}", ip); + return Mono.just(ip); + }; + } + + /** + * 요청 경로 기반 요청 제한을 위한 KeyResolver 빈 + * 요청 경로를 키로 사용하여 요청 제한을 적용 + * + * @return KeyResolver 요청 경로 기반 키 리졸버 + */ + // @Bean 주석 처리 - 다중 KeyResolver 빈 문제 해결 + public KeyResolver pathKeyResolver() { + return exchange -> { + String path = exchange.getRequest().getPath().value(); + log.debug("Request Path: {}", path); + return Mono.just(path); + }; + } + + /** + * IP와 경로를 조합한 요청 제한을 위한 KeyResolver 빈 + * IP 주소와 요청 경로를 조합하여 키로 사용함으로써 더 세밀한 요청 제한 적용 가능 + * + * @return KeyResolver IP와 경로 조합 기반 키 리졸버 + */ + @Bean + @Primary // 기본 KeyResolver 빈으로 지정 + public KeyResolver ipAndPathKeyResolver() { + return exchange -> { + String ip = exchange.getRequest().getRemoteAddress().getAddress().getHostAddress(); + String path = exchange.getRequest().getPath().value(); + String key = ip + ":" + path; + log.debug("Request Key: {}", key); + return Mono.just(key); + }; + } +} diff --git a/apigateway/src/main/java/org/egovframe/cloud/apigateway/config/ReactiveAuthorization.java b/apigateway/src/main/java/org/egovframe/cloud/apigateway/config/ReactiveAuthorization.java new file mode 100644 index 0000000..bc5a2eb --- /dev/null +++ b/apigateway/src/main/java/org/egovframe/cloud/apigateway/config/ReactiveAuthorization.java @@ -0,0 +1,188 @@ +package org.egovframe.cloud.apigateway.config; + +import io.github.resilience4j.circuitbreaker.CircuitBreaker; +import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig; +import io.github.resilience4j.reactor.circuitbreaker.operator.CircuitBreakerOperator; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.Jwts; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Timer; +import java.time.Duration; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.server.RequestPath; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.security.access.AuthorizationServiceException; +import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.ReactiveAuthorizationManager; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.server.authorization.AuthorizationContext; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; +import reactor.util.retry.Retry; + + +/** + * org.egovframe.cloud.apigateway.config.ReactiveAuthorization + *
+ * Spring Security 에 의해 요청 url에 대한 사용자 인가 서비스를 수행하는 클래스 요청에 대한 사용자의 권한여부 체크하여 true/false 리턴한다 + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/07/19 + * + *
+ * << 개정이력(Modification Information) >> + * + * 수정일 수정자 수정내용 + * ---------- -------- --------------------------- + * 2021/07/19 jaeyeolkim 최초 생성 + *+ */ +@Slf4j +@Component +public class ReactiveAuthorization implements ReactiveAuthorizationManager
+ * WebClient 설정 클래스 + * 타임아웃 및 연결 풀 설정을 통해 안정적인 외부 서비스 호출을 지원 + * + * @author IIPBS 개발팀 + * @version 1.0 + * @since 2025/03/19 + * + *
+ * << 개정이력(Modification Information) >> + * + * 수정일 수정자 수정내용 + * ---------- -------- --------------------------- + * 2025/03/19 IIPBS 최초 생성 + *+ */ +@Configuration +public class WebClientConfig { + + /** + * 커넥션 풀 설정을 통해 효율적인 리소스 관리 + * + * @return ConnectionProvider + */ + @Bean + public ConnectionProvider connectionProvider() { + return ConnectionProvider.builder("http-pool") + .maxConnections(500) + .maxIdleTime(Duration.ofSeconds(60)) + .maxLifeTime(Duration.ofSeconds(300)) + .pendingAcquireTimeout(Duration.ofSeconds(30)) + .evictInBackground(Duration.ofSeconds(30)) + .build(); + } + + /** + * 타임아웃 설정이 적용된 HttpClient 빈 생성 + * + * @param connectionProvider 커넥션 풀 설정 + * @return HttpClient + */ + @Bean + public HttpClient httpClient(ConnectionProvider connectionProvider) { + return HttpClient.create(connectionProvider) + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000) + .responseTimeout(Duration.ofSeconds(5)) + .doOnConnected(conn -> + conn.addHandlerLast(new ReadTimeoutHandler(5, TimeUnit.SECONDS)) + .addHandlerLast(new WriteTimeoutHandler(5, TimeUnit.SECONDS))); + } + + /** + * 타임아웃 및 커넥션 풀 설정이 적용된 WebClient.Builder 빈 생성 + * + * @param httpClient 설정된 HttpClient + * @return WebClient.Builder + */ + @Bean + public WebClient.Builder webClientBuilder(HttpClient httpClient) { + return WebClient.builder() + .clientConnector(new ReactorClientHttpConnector(httpClient)) + .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(2 * 1024 * 1024)); + } +} diff --git a/apigateway/src/main/java/org/egovframe/cloud/apigateway/config/WebFluxSecurityConfig.java b/apigateway/src/main/java/org/egovframe/cloud/apigateway/config/WebFluxSecurityConfig.java new file mode 100644 index 0000000..c0e8d53 --- /dev/null +++ b/apigateway/src/main/java/org/egovframe/cloud/apigateway/config/WebFluxSecurityConfig.java @@ -0,0 +1,67 @@ +package org.egovframe.cloud.apigateway.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.security.authorization.ReactiveAuthorizationManager; +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; +import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.web.server.SecurityWebFilterChain; +import org.springframework.security.web.server.authentication.HttpStatusServerEntryPoint; +import org.springframework.security.web.server.authorization.AuthorizationContext; + +/** + * org.egovframe.cloud.apigateway.config.WebFluxSecurityConfig + *
+ * Spring Security Config 클래스
+ * ReactiveAuthorizationManager
+ * 에러 발생 시 에러 정보 중 필요한 내용만 반환한다
+ * ErrorCode 에서 status, code, message 세 가지 속성을 의존한다
+ *
+ * @author 표준프레임워크센터 jaeyeolkim
+ * @version 1.0
+ * @since 2021/07/16
+ *
+ *
+ * REST API 요청에 대한 오류 반환값을 정의
+ * ErrorResponse 클래스에서 status, code, message 세 가지 속성을 의존한다
+ * message 는 MessageSource 의 키 값을 정의하여 다국어 처리를 지원한다
+ *
+ * @author 표준프레임워크센터 jaeyeolkim
+ * @version 1.0
+ * @since 2021/07/16
+ *
+ *
+ * 요청 제한(Rate Limiting) 필터
+ * 동일한 경로에 대한 과도한 요청을 제한하기 위한 필터
+ *
+ * @author changwng
+ * @version 1.0
+ * @since 2025/03/19
+ *
+ *
+ * << 개정이력(Modification Information) >>
+ *
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * 2021/06/30 jaeyeolkim 최초 생성
+ *
+ */
+@EnableWebFluxSecurity // Spring Security 설정들을 활성화시켜 준다
+public class WebFluxSecurityConfig {
+
+ private final static String[] PERMITALL_ANTPATTERNS = {
+ ReactiveAuthorization.AUTHORIZATION_URI, "/", "/csrf",
+ "/user-service/login", "/?*-service/api/v1/messages/**", "/api/v1/messages/**",
+ "/?*-service/actuator/?*", "/actuator/?*",
+ "/v3/api-docs/**", "/?*-service/v3/api-docs", "**/configuration/*", "/swagger*/**", "/webjars/**"
+ };
+ private final static String USER_JOIN_ANTPATTERNS = "/user-service/api/v1/users";
+
+ /**
+ * WebFlux 스프링 시큐리티 설정
+ *
+ * @see ReactiveAuthorization
+ * @param http
+ * @param check check(Mono
+ * << 개정이력(Modification Information) >>
+ *
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * 2021/07/16 jaeyeolkim 최초 생성
+ * 2024/09/24 이백행 컨트리뷰션 이클립스 문제(Problems) 해결
+ *
+ */
+@Slf4j
+//@Configuration
+public class ReactiveExceptionHandlerConfig {
+
+ private final MessageSource messageSource;
+
+ public ReactiveExceptionHandlerConfig(MessageSource messageSource) {
+ this.messageSource = messageSource;
+ }
+
+ /**
+ * 에러 발생 시 에러 정보 중 필요한 내용만 반환한다
+ *
+ * @return
+ */
+// @Bean
+ public ErrorAttributes errorAttributes() {
+ return new DefaultErrorAttributes() {
+ @Override
+ public Map
+ * << 개정이력(Modification Information) >>
+ *
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * 2021/07/16 jaeyeolkim 최초 생성
+ *
+ */
+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
+ ACCESS_DENIED(403, "E005", "err.access.denied"), // Forbidden, Access is Denied
+ NOT_FOUND(404, "E007", "err.not.found"), // Not found
+ METHOD_NOT_ALLOWED(405, "E008", "err.method.not.allowed"), // 요청 방법이 서버에 의해 알려졌으나, 사용 불가능한 상태
+ UNPROCESSABLE_ENTITY(422, "E009", "err.unprocessable.entity"), // Unprocessable Entity
+ INTERNAL_SERVER_ERROR(500, "E999", "err.internal.server"), // Server Error
+ SERVICE_UNAVAILABLE(503, "E010", "err.service.unavailable") // Service Unavailable
+ ;
+
+
+ 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;
+ }
+
+}
diff --git a/apigateway/src/main/java/org/egovframe/cloud/apigateway/filter/GlobalFilter.java b/apigateway/src/main/java/org/egovframe/cloud/apigateway/filter/GlobalFilter.java
new file mode 100644
index 0000000..7c91e45
--- /dev/null
+++ b/apigateway/src/main/java/org/egovframe/cloud/apigateway/filter/GlobalFilter.java
@@ -0,0 +1,53 @@
+/*
+
+기본필터를 삭제 처리함 (Global Filter)
+package org.egovframe.cloud.apigateway.filter;
+
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.cloud.gateway.filter.GatewayFilter;
+import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
+import org.springframework.http.server.reactive.ServerHttpRequest;
+import org.springframework.http.server.reactive.ServerHttpResponse;
+import org.springframework.stereotype.Component;
+import reactor.core.publisher.Mono;
+
+@Slf4j
+@Component
+public class GlobalFilter extends AbstractGatewayFilterFactory
+ * << 개정이력(Modification Information) >>
+ *
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * 2025/03/19 changwng 최초 생성
+ *
+ */
+@Slf4j
+@Component
+public class RateLimitingFilter extends AbstractGatewayFilterFactory