Initial commit

This commit is contained in:
jooho
2021-10-20 17:12:00 +09:00
parent 0c884beff8
commit 8caa4bbc5a
487 changed files with 44198 additions and 0 deletions

View File

@@ -0,0 +1,15 @@
# openjdk8 base image
FROM openjdk:8-jre-alpine
# config server uri: dockder run --e 로 변경 가능
ENV SPRING_CLOUD_CONFIG_URI https://egov-config.paas-ta.org
# jar 파일이 복사되는 위치
ENV APP_HOME=/usr/app/
# 작업 시작 위치
WORKDIR $APP_HOME
# jar 파일 복사
COPY build/libs/*.jar apigateway.jar
# application port
EXPOSE 8000
# 실행 (application-cf.yml 프로필이 기본값)
CMD ["java", "-Dspring.profiles.active=${profile:cf}", "-jar", "apigateway.jar"]

View File

@@ -0,0 +1,48 @@
plugins {
id 'org.springframework.boot' version '2.4.5'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
}
group = 'org.egovframe.cloud'
version = '0.1'
sourceCompatibility = '1.8'
repositories {
mavenCentral()
}
ext {
set('springCloudVersion', "2020.0.3")
}
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 'net.logstash.logback:logstash-logback-encoder:6.6' // logstash logback
implementation 'io.jsonwebtoken:jjwt:0.9.1'
implementation 'javax.xml.bind:jaxb-api:2.3.1'
implementation group: 'io.springfox', name: 'springfox-swagger2', version: '2.9.2'
implementation group: 'io.springfox', name: 'springfox-swagger-ui', version: '2.9.2'
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()
}

185
backend/apigateway/gradlew vendored Executable file
View File

@@ -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" "$@"

89
backend/apigateway/gradlew.bat vendored Normal file
View File

@@ -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

View File

@@ -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: https://egov-config.paas-ta.org
app_name: egov-apigateway # logstash custom app name
TZ: Asia/Seoul
JAVA_OPTS: -Xss349k

View File

@@ -0,0 +1 @@
rootProject.name = 'apigateway'

View File

@@ -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
* <p>
* 게이트웨이 어플리케이션 클래스
* Eureka Client 로 설정했기 때문에 Eureka Server 가 먼저 기동되어야 한다.
*
* @author 표준프레임워크센터 jaeyeolkim
* @version 1.0
* @since 2021/06/30
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/06/30 jaeyeolkim 최초 생성
* </pre>
*/
@EnableDiscoveryClient
@SpringBootApplication
public class ApigatewayApplication {
public static void main(String[] args) {
SpringApplication.run(ApigatewayApplication.class, args);
}
}

View File

@@ -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.context.i18n.LocaleContextHolder;
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
* <p>
* MessageSource 정상 확인을 위한 컨트롤러
*
* @author 표준프레임워크센터 jaeyeolkim
* @version 1.0
* @since 2021/08/10
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/08/10 jaeyeolkim 최초 생성
* </pre>
*/
@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;
}
}

View File

@@ -0,0 +1,70 @@
package org.egovframe.cloud.apigateway.api;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
import springfox.documentation.swagger.web.*;
import java.util.Optional;
/**
* org.egovframe.cloud.apigateway.api.SwaggerResourcesController
*
* Swagger resource 들을 모으는 controller class
*
* @author 표준프레임워크센터 shinmj
* @version 1.0
* @since 2021/07/07
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/07/07 shinmj 최초 생성
* </pre>
*/
@RestController
@RequestMapping("/swagger-resources")
public class SwaggerResourcesController {
@Autowired(required = false)
private SecurityConfiguration securityConfiguration;
@Autowired(required = false)
private UiConfiguration uiConfiguration;
private final SwaggerResourcesProvider swaggerResources;
@Autowired
public SwaggerResourcesController(SwaggerResourcesProvider swaggerResources) {
this.swaggerResources = swaggerResources;
}
@GetMapping("/configuration/security")
public Mono<ResponseEntity<SecurityConfiguration>> securityConfiguration() {
return Mono.just(new ResponseEntity<>(
Optional.ofNullable(securityConfiguration).orElse(SecurityConfigurationBuilder.builder().build()),
HttpStatus.OK
));
}
@GetMapping("/configuration/ui")
public Mono<ResponseEntity<UiConfiguration>> uiConfiguration() {
return Mono.just(new ResponseEntity<>(
Optional.ofNullable(uiConfiguration).orElse(UiConfigurationBuilder.builder().build()),
HttpStatus.OK
));
}
@GetMapping("")
public Mono<ResponseEntity> swaggerResources() {
return Mono.just(new ResponseEntity(
swaggerResources.get(), HttpStatus.OK
));
}
}

View File

@@ -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
* <p>
* Spring MessageSource 설정
* Message Domain 이 있는 portal-service 에서 messages.properties 를 공유 가능한 외부 위치에 생성한다.
* 각 서비스에서 해당 파일을 통해 다국어를 지원하도록 한다.
* module-common.jar 를 포함하지 않는 서비스에서는 이 configuration을 추가해주어야 한다.
*
* @author 표준프레임워크센터 jaeyeolkim
* @version 1.0
* @since 2021/08/09
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/08/09 jaeyeolkim 최초 생성
* </pre>
*/
@Slf4j
@Configuration
public class MessageSourceConfig {
@Value("${messages.directory}")
private String messagesDirectory;
@Value("${spring.profiles.active:default}")
private String profile;
@Bean
public MessageSource messageSource() {
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
final String MESSAGES = "/messages";
if ("default".equals(profile)) {
Path fileStorageLocation = Paths.get(messagesDirectory).toAbsolutePath().normalize();
String dbMessages = StringUtils.cleanPath("file://" + fileStorageLocation + MESSAGES);
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;
}
}

View File

@@ -0,0 +1,127 @@
package org.egovframe.cloud.apigateway.config;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
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;
/**
* org.egovframe.cloud.apigateway.config.ReactiveAuthorization
* <p>
* Spring Security 에 의해 요청 url에 대한 사용자 인가 서비스를 수행하는 클래스
* 요청에 대한 사용자의 권한여부 체크하여 true/false 리턴한다
*
* @author 표준프레임워크센터 jaeyeolkim
* @version 1.0
* @since 2021/07/19
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/07/19 jaeyeolkim 최초 생성
* </pre>
*/
@Slf4j
@RequiredArgsConstructor
@Component
public class ReactiveAuthorization implements ReactiveAuthorizationManager<AuthorizationContext> {
@Value("${apigateway.host:http://localhost:8000}")
private String APIGATEWAY_HOST;
@Value("${token.secret}")
private String TOKEN_SECRET;
// org.egovframe.cloud.common.config.GlobalConstant 값도 같이 변경해주어야 한다.
public static final String AUTHORIZATION_URI = "/user-service" + "/api/v1/authorizations/check";
public static final String REFRESH_TOKEN_URI = "/user-service" + "/api/v1/users/token/refresh";
/**
* 요청에 대한 사용자의 권한여부 체크하여 true/false 리턴한다
* 헤더에 토큰이 있으면 유효성을 체크한다.
*
* @param authentication
* @param context
* @return
* @see WebFluxSecurityConfig
*/
@Override
public Mono<AuthorizationDecision> check(Mono<Authentication> authentication, AuthorizationContext context) {
ServerHttpRequest request = context.getExchange().getRequest();
RequestPath requestPath = request.getPath();
HttpMethod httpMethod = request.getMethod();
String baseUrl = APIGATEWAY_HOST + AUTHORIZATION_URI + "?httpMethod=" + httpMethod + "&requestPath=" + requestPath;
log.info("baseUrl={}", baseUrl);
String authorizationHeader = "";
if (request.getHeaders().containsKey(HttpHeaders.AUTHORIZATION)
&& StringUtils.hasLength(
request.getHeaders().get(HttpHeaders.AUTHORIZATION).get(0))
&& !"undefined".equals(request.getHeaders().get(HttpHeaders.AUTHORIZATION).get(0))
) {
try {
authorizationHeader = request.getHeaders().get(HttpHeaders.AUTHORIZATION).get(0);
String jwt = authorizationHeader.replace("Bearer", "");
String subject = Jwts.parser().setSigningKey(TOKEN_SECRET)
.parseClaimsJws(jwt)
.getBody()
.getSubject();
// refresh token 요청 시 토큰 검증만 하고 인가 처리 한다.
if (REFRESH_TOKEN_URI.equals(requestPath + "")) {
return Mono.just(new AuthorizationDecision(true));
}
if (subject == null || subject.isEmpty()) {
log.error("토큰 인증 오류");
throw new AuthorizationServiceException("토큰 인증 오류");
}
} catch (IllegalArgumentException e) {
log.error("토큰 헤더 오류 : {}", e.getMessage());
throw new AuthorizationServiceException("토큰 인증 오류");
} catch (ExpiredJwtException e) {
log.error("토큰 유효기간이 만료되었습니다. : {}", e.getMessage());
throw new AuthorizationServiceException("토큰 유효기간 만료");
} catch (Exception e) {
log.error("토큰 인증 오류 Exception : {}", e.getMessage());
throw new AuthorizationServiceException("토큰 인증 오류");
}
}
Boolean granted = false;
try {
String token = authorizationHeader; // Variable used in lambda expression should be final or effectively final
Mono<Boolean> body = WebClient.create(baseUrl)
.get()
.headers(httpHeaders -> {
httpHeaders.add(HttpHeaders.AUTHORIZATION, token);
})
.retrieve().bodyToMono(Boolean.class);
granted = body.block();
log.info("Security AuthorizationDecision granted={}", granted);
} catch (Exception e) {
log.error("인가 서버에 요청 중 오류 : {}", e.getMessage());
throw new AuthorizationServiceException("인가 요청시 오류 발생");
}
return Mono.just(new AuthorizationDecision(granted));
}
}

View File

@@ -0,0 +1,78 @@
package org.egovframe.cloud.apigateway.config;
import lombok.AllArgsConstructor;
import org.springframework.cloud.gateway.config.GatewayProperties;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.support.NameUtils;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;
import springfox.documentation.swagger.web.SwaggerResource;
import springfox.documentation.swagger.web.SwaggerResourcesProvider;
import java.util.ArrayList;
import java.util.List;
/**
* org.egovframe.cloud.apigateway.config.SwaggerProvider
*
* Swagger API Doc aggregator class
* Swagger Resource인 api-docs를 가져오는 provider
*
* @author 표준프레임워크센터 shinmj
* @version 1.0
* @since 2021/07/07
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/07/07 shinmj 최초 생성
* </pre>
*/
@AllArgsConstructor
@Component
@Primary
public class SwaggerProvider implements SwaggerResourcesProvider {
public static final String API_URL = "/v2/api-docs";
public static final String WEBFLUX_API_URL = "/v3/api-docs";
private final RouteLocator routeLocator;
private final GatewayProperties gatewayProperties;
@Override
public List<SwaggerResource> get() {
List<SwaggerResource> resources = new ArrayList<>();
List<String> routes = new ArrayList<>();
routeLocator.getRoutes().subscribe(route -> routes.add(route.getId()));
gatewayProperties.getRoutes().stream()
.filter(routeDefinition -> routes.contains(routeDefinition.getId()))
.forEach(routeDefinition -> routeDefinition.getPredicates().stream()
.filter(predicateDefinition -> ("Path").equalsIgnoreCase(predicateDefinition.getName()))
.forEach(predicateDefinition ->
resources.add(
swaggerResource(routeDefinition.getId(),
predicateDefinition.
getArgs().
get(NameUtils.GENERATED_NAME_PREFIX+"0").
replace("/**", API_URL))))
);
return resources;
}
private SwaggerResource swaggerResource(String name, String location) {
SwaggerResource swaggerResource = new SwaggerResource();
swaggerResource.setName(name);
if (name.contains("reserve")) {
swaggerResource.setLocation(location.replace(API_URL, WEBFLUX_API_URL));
}else {
swaggerResource.setLocation(location);
}
swaggerResource.setSwaggerVersion("2.0");
return swaggerResource;
}
}

View File

@@ -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
* <p>
* Spring Security Config 클래스
* ReactiveAuthorizationManager<AuthorizationContext> 구현체 ReactiveAuthorization 클래스를 통해 인증/인가 처리를 구현한다.
*
* @author 표준프레임워크센터 jaeyeolkim
* @version 1.0
* @since 2021/06/30
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/06/30 jaeyeolkim 최초 생성
* </pre>
*/
@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/?*",
"/?*-service/v2/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<Authentication> authentication, AuthorizationContext context)
* @return
* @throws Exception
*/
@Bean
public SecurityWebFilterChain configure(ServerHttpSecurity http, ReactiveAuthorizationManager<AuthorizationContext> check) throws Exception {
http
.csrf().disable()
.headers().frameOptions().disable()
.and()
.formLogin().disable()
.httpBasic().authenticationEntryPoint(new HttpStatusServerEntryPoint(HttpStatus.UNAUTHORIZED)) // login dialog disabled & 401 HttpStatus return
.and()
.authorizeExchange()
.pathMatchers(PERMITALL_ANTPATTERNS).permitAll()
.pathMatchers(HttpMethod.POST, USER_JOIN_ANTPATTERNS).permitAll()
.anyExchange().access(check);
return http.build();
}
}

View File

@@ -0,0 +1,98 @@
package org.egovframe.cloud.apigateway.exception;
import lombok.extern.slf4j.Slf4j;
import org.egovframe.cloud.apigateway.exception.dto.ErrorCode;
import org.springframework.boot.web.error.ErrorAttributeOptions;
import org.springframework.boot.web.reactive.error.DefaultErrorAttributes;
import org.springframework.boot.web.reactive.error.ErrorAttributes;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.web.reactive.function.server.ServerRequest;
import java.time.LocalDateTime;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* org.egovframe.cloud.apigateway.exception.ReactiveExceptionHandlerConfig
* <p>
* 에러 발생 시 에러 정보 중 필요한 내용만 반환한다
* ErrorCode 에서 status, code, message 세 가지 속성을 의존한다
*
* @author 표준프레임워크센터 jaeyeolkim
* @version 1.0
* @since 2021/07/16
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/07/16 jaeyeolkim 최초 생성
* </pre>
*/
@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<String, Object> getErrorAttributes(ServerRequest request, ErrorAttributeOptions options) {
Map<String, Object> defaultMap = super.getErrorAttributes(request, options);
Map<String, Object> errorAttributes = new LinkedHashMap<>();
int status = (int) defaultMap.get("status");
ErrorCode errorCode = getErrorCode(status);
String message = messageSource.getMessage(errorCode.getMessage(), null, LocaleContextHolder.getLocale());
errorAttributes.put("timestamp", LocalDateTime.now());
errorAttributes.put("message", message);
errorAttributes.put("status", status);
errorAttributes.put("code", errorCode.getCode());
// API Gateway 에서 FieldError는 처리하지 않는다.
log.error("getErrorAttributes()={}", defaultMap);
return errorAttributes;
}
};
}
/**
* 상태코드로부터 ErrorCode 를 매핑하여 리턴한다.
*
* @param status
* @return
*/
private ErrorCode getErrorCode(int status) {
switch (status) {
case 400:
return ErrorCode.ENTITY_NOT_FOUND;
case 401:
return ErrorCode.UNAUTHORIZED;
case 403:
return ErrorCode.ACCESS_DENIED;
case 404:
return ErrorCode.NOT_FOUND;
case 405:
return ErrorCode.METHOD_NOT_ALLOWED;
case 422:
return ErrorCode.UNPROCESSABLE_ENTITY;
default:
return ErrorCode.INTERNAL_SERVER_ERROR;
}
}
}

View File

@@ -0,0 +1,59 @@
package org.egovframe.cloud.apigateway.exception.dto;
/**
* org.egovframe.cloud.common.exception.dto.ErrorCode
* <p>
* REST API 요청에 대한 오류 반환값을 정의
* ErrorResponse 클래스에서 status, code, message 세 가지 속성을 의존한다
* message 는 MessageSource 의 키 값을 정의하여 다국어 처리를 지원한다
*
* @author 표준프레임워크센터 jaeyeolkim
* @version 1.0
* @since 2021/07/16
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/07/16 jaeyeolkim 최초 생성
* </pre>
*/
public enum ErrorCode {
INVALID_INPUT_VALUE(400, "E001", "err.invalid.input.value"), // Bad Request
INVALID_TYPE_VALUE(400, "E002", "err.invalid.type.value"), // Bad Request
ENTITY_NOT_FOUND(400, "E003", "err.entity.not.found"), // Bad Request
UNAUTHORIZED(401, "E004", "err.unauthorized"), // The request requires an user authentication
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;
}
}

View File

@@ -0,0 +1,49 @@
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<GlobalFilter.Config> {
public GlobalFilter() {
super(Config.class);
}
@Override
public GatewayFilter apply(Config config) {
// Pre filter
return ((exchange, chain) -> {
// Netty 비동기 방식 서버 사용시에는 ServerHttpRequest 를 사용해야 한다.
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
if (config.isPreLogger()) {
log.info("[GlobalFilter Start] request ID: {}, method: {}, path: {}", request.getId(), request.getMethod(), request.getPath());
}
// Post Filter
// 비동기 방식의 단일값 전달시 Mono 사용(Webflux)
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
if (config.isPostLogger()) {
log.info("[GlobalFilter End ] request ID: {}, method: {}, path: {}, statusCode: {}", request.getId(), request.getMethod(), request.getPath(), response.getStatusCode());
}
}));
});
}
@Data
public static class Config {
// put the configure
private String baseMessage;
private boolean preLogger;
private boolean postLogger;
}
}

View File

@@ -0,0 +1,49 @@
package org.egovframe.cloud.apigateway.filter;
import org.egovframe.cloud.apigateway.config.SwaggerProvider;
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.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
/**
* org.egovframe.cloud.apigateway.filter.SwaggerHeaderFilter
*
* Swagger header filter class
* 각 서비스 명을 붙여서 호출 할 수 있도록 filter를 추가 한다.
*
* @author 표준프레임워크센터 shinmj
* @version 1.0
* @since 2021/07/07
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/07/07 shinmj 최초 생성
* </pre>
*/
@Component
public class SwaggerHeaderFilter extends AbstractGatewayFilterFactory {
private static final String HEADER_NAME = "X-Forwarded-Prefix";
@Override
public GatewayFilter apply(Object config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
String path = request.getURI().getPath();
if (!StringUtils.endsWithIgnoreCase(path, SwaggerProvider.API_URL)) {
return chain.filter(exchange);
}
String basePath = path.substring(0, path.lastIndexOf(SwaggerProvider.API_URL));
ServerHttpRequest newRequest = request.mutate().header(HEADER_NAME, basePath).build();
ServerWebExchange newExchange = exchange.mutate().request(newRequest).build();
return chain.filter(newExchange);
};
}
}

View File

@@ -0,0 +1,67 @@
server:
port: 8000
spring:
application:
name: apigateway
cloud:
gateway:
routes:
- id: user-service
uri: lb://USER-SERVICE
predicates:
- Path=/user-service/**
filters:
- RemoveRequestHeader=Cookie
- RewritePath=/user-service/(?<segment>.*), /$\{segment}
- SwaggerHeaderFilter
- id: portal-service
uri: lb://PORTAL-SERVICE
predicates:
- Path=/portal-service/**
filters:
- RewritePath=/portal-service/(?<segment>.*), /$\{segment}
- SwaggerHeaderFilter
- id: board-service
uri: lb://BOARD-SERVICE
predicates:
- Path=/board-service/**
filters:
- RewritePath=/board-service/(?<segment>.*), /$\{segment}
- SwaggerHeaderFilter
- id: reserve-item-service
uri: lb://RESERVE-ITEM-SERVICE
predicates:
- Path=/reserve-item-service/**
filters:
- RewritePath=/reserve-item-service/(?<segment>.*), /$\{segment}
- SwaggerHeaderFilter
- id: reserve-check-service
uri: lb://RESERVE-CHECK-SERVICE
predicates:
- Path=/reserve-check-service/**
filters:
- RewritePath=/reserve-check-service/(?<segment>.*), /$\{segment}
- SwaggerHeaderFilter
- id: reserve-request-service
uri: lb://RESERVE-REQUEST-SERVICE
predicates:
- Path=/reserve-request-service/**
filters:
- RewritePath=/reserve-request-service/(?<segment>.*), /$\{segment}
- SwaggerHeaderFilter
default-filters:
- name: GlobalFilter
args:
preLogger: true
postLogger: true
discovery:
locator:
enabled: true
# config server actuator
management:
endpoints:
web:
exposure:
include: refresh, health, beans

View File

@@ -0,0 +1,5 @@
spring:
cloud:
config:
uri: http://localhost:8888
name: apigateway

View File

@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %thread %-5level %logger - %m%n</pattern>
</encoder>
</appender>
<!-- 로컬에서는 로그를 전송하지 않도록 설정 -->
<springProfile name="default">
<root level="INFO">
<appender-ref ref="STDOUT" />
</root>
</springProfile>
<springProfile name="!default">
<!-- java -Ddestination="localhost:8088" 와 같이 변경할 수 있다. cf 환경에서는 manifest.yml 파일에 환경변수로 추가 -->
<property name="destination" value="localhost:8088" />
<property name="app_name" value="${app_name}" />
<!-- ELK - Logstash 로 로그를 전송하기 위한 appender -->
<appender name="LOGSTASH" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
<destination>${destination}</destination><!-- native profile => localhost:8088 -->
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<customFields>{"app.name":"${app_name}"}</customFields>
</encoder>
</appender>
<root level="WARN">
<appender-ref ref="LOGSTASH" />
<appender-ref ref="STDOUT" />
</root>
</springProfile>
</configuration>

View File

@@ -0,0 +1,13 @@
package org.egovframe.cloud.apigateway;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class ApigatewayApplicationTests {
@Test
void contextLoads() {
}
}

View File

@@ -0,0 +1,25 @@
package org.egovframe.cloud.apigateway.config;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
@SpringBootTest(webEnvironment = RANDOM_PORT)
class MessageSourceConfigTest {
@Autowired
TestRestTemplate restTemplate;
@Test
public void 메세지를_외부위치에서_읽어온다() throws Exception {
// when
String message = restTemplate.getForObject("http://localhost:8000/api/v1/messages/common.login/ko", String.class);
// then
assertThat(message).isEqualTo("로그인");
}
}

View File

@@ -0,0 +1,28 @@
package org.egovframe.cloud.apigateway.config;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.HttpHeaders;
import org.springframework.test.web.reactive.server.WebTestClient;
@SpringBootTest
class ReactiveAuthorizationTest {
@Value("${server.port}")
private String PORT;
@Test
public void API요청시_토큰인증_만료된다() throws Exception {
// given
String baseUrl = "http://localhost:" + PORT;
String notValidToken = "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiI2NWEwMGY2NS04NDYwLTQ5YWYtOThlYy0wNDI5NzdlNTZmNGIiLCJhdXRob3JpdGllcyI6IlJPTEVfVVNFUiIsImV4cCI6MTYyNjc4MjQ0N30.qiScvtr1m88SHPLpHqcJiklXFyIQ7WBJdiFcdcb2B8YSWC59QcdRRgMtXDGSZnjBgF194W-GRBpHUta6VCkrfQ";
// when, then
WebTestClient.bindToServer().baseUrl(baseUrl).defaultHeader(HttpHeaders.AUTHORIZATION, notValidToken).build()
.get()
.uri("/user-service/api/v1/users")
.exchange().expectStatus().isUnauthorized()
;
}
}