Initial commit
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
}
|
||||
67
backend/apigateway/src/main/resources/application.yml
Normal file
67
backend/apigateway/src/main/resources/application.yml
Normal 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
|
||||
5
backend/apigateway/src/main/resources/bootstrap.yml
Normal file
5
backend/apigateway/src/main/resources/bootstrap.yml
Normal file
@@ -0,0 +1,5 @@
|
||||
spring:
|
||||
cloud:
|
||||
config:
|
||||
uri: http://localhost:8888
|
||||
name: apigateway
|
||||
34
backend/apigateway/src/main/resources/logback-spring.xml
Normal file
34
backend/apigateway/src/main/resources/logback-spring.xml
Normal 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>
|
||||
@@ -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() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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("로그인");
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user