diff --git a/k8s/helm-value.yaml b/k8s/helm-value.yaml index cec13e8..ebba5be 100644 --- a/k8s/helm-value.yaml +++ b/k8s/helm-value.yaml @@ -1,2 +1,15 @@ image: tag: v0.1.1 +env: + MEMBER_URI: "http://backend-member:8080" + SIGHT_URI: "http://backend-sight:8080" + STORY_URI: "http://backend-story:8080" + CORE_URI: "http://backend-core:8080" + ROUTE_URI: "http://backend-route:8080" + MEMBER_API_PATH: "/api/member/**, /api/user/member/**, /api/admin/member/**" + SIGHT_API_PATH: "/api/sight/**, /api/user/sight/**, /api/admin/sight/**" + STORY_API_PATH: "/api/story/**, /api/user/story/**, /api/admin/story/**" + CORE_API_PATH: "/api/core/**, /api/user/core/**, /api/admin/core/**" + ROUTE_API_PATH: "/api/route/**, /api/user/route/**, /api/admin/route/**" + PUBLIC_PATHS: "/.well-known/**,/favicon.ico,/error,/swagger-ui/**,/swagger-ui.html,/api-docs/**,/member/api-docs,/sight/api-docs,/story/api-docs,/core/api-docs,/route/api-docs,/actuator/**,/api/**" + AUTH_PATHS: "/api/user/**,/api/admin/**" \ No newline at end of file diff --git a/src/main/java/com/earseo/gateway/common/config/WebSecurityConfig.java b/src/main/java/com/earseo/gateway/common/config/WebSecurityConfig.java index f2a8639..95a87be 100644 --- a/src/main/java/com/earseo/gateway/common/config/WebSecurityConfig.java +++ b/src/main/java/com/earseo/gateway/common/config/WebSecurityConfig.java @@ -1,9 +1,11 @@ package com.earseo.gateway.common.config; +import com.earseo.gateway.security.SwaggerFilter; import com.earseo.gateway.security.jwt.CustomAccessDeniedHandler; import com.earseo.gateway.security.jwt.CustomAuthenticationEntryPoint; import com.earseo.gateway.security.jwt.JwtAuthenticationFilter; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; @@ -23,9 +25,12 @@ @RequiredArgsConstructor public class WebSecurityConfig { + @Value("${security.path.public}") + private String publicPaths; private final JwtAuthenticationFilter jwtAuthenticationFilter; private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint; private final CustomAccessDeniedHandler customAccessDeniedHandler; + private final SwaggerFilter swaggerFilter; @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { @@ -36,12 +41,12 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti sessionManagementConfigurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .formLogin(AbstractHttpConfigurer::disable) .httpBasic(AbstractHttpConfigurer::disable) + .addFilterBefore(swaggerFilter, UsernamePasswordAuthenticationFilter.class) .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) .authorizeHttpRequests(authorizeRequests -> authorizeRequests .requestMatchers("/api/user/**").hasAnyAuthority("USER", "ADMIN") .requestMatchers("/api/admin/**").hasAuthority("ADMIN") - .requestMatchers("/api/**").permitAll() - .requestMatchers("/actuator/**").permitAll() + .requestMatchers(this.getPublicPaths()).permitAll() .anyRequest().authenticated()) .exceptionHandling(exceptionHandler -> exceptionHandler .authenticationEntryPoint(customAuthenticationEntryPoint) @@ -65,4 +70,8 @@ public CorsConfigurationSource corsConfigurationSource() { return source; } + + private String[] getPublicPaths() { + return publicPaths.split(","); + } } diff --git a/src/main/java/com/earseo/gateway/common/exception/AuthError.java b/src/main/java/com/earseo/gateway/common/exception/AuthError.java index f8d6147..6e6cea3 100644 --- a/src/main/java/com/earseo/gateway/common/exception/AuthError.java +++ b/src/main/java/com/earseo/gateway/common/exception/AuthError.java @@ -10,6 +10,8 @@ public enum AuthError implements ErrorCodeInterface{ EXPIRED_TOKEN("AUTH_001", "토큰이 만료되었습니다.", HttpStatus.BAD_REQUEST), INVALID_TOKEN("AUTH_002", "유효하지 않은 토큰입니다.", HttpStatus.BAD_REQUEST), UNSUPPORTED_JWT("AUTH_003", "지원하지 않는 토큰입니다.", HttpStatus.BAD_REQUEST), + + DISALLOWED_HOST("GATE_001","허가되지 않은 호스트입니다.",HttpStatus.FORBIDDEN), ; private final String status; diff --git a/src/main/java/com/earseo/gateway/security/SwaggerFilter.java b/src/main/java/com/earseo/gateway/security/SwaggerFilter.java new file mode 100644 index 0000000..73f5fba --- /dev/null +++ b/src/main/java/com/earseo/gateway/security/SwaggerFilter.java @@ -0,0 +1,66 @@ +package com.earseo.gateway.security; + +import com.earseo.gateway.common.exception.AuthError; +import com.earseo.gateway.common.exception.BaseException; +import jakarta.servlet.FilterChain; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.util.AntPathMatcher; +import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.servlet.HandlerExceptionResolver; + +import java.util.List; + +@Component +public class SwaggerFilter extends OncePerRequestFilter { + + @Value("${allowed.hosts}") + private List allowedHosts; + private final HandlerExceptionResolver resolver; + + private static final AntPathMatcher pathMatcher = new AntPathMatcher(); + + public SwaggerFilter( @Qualifier("handlerExceptionResolver") HandlerExceptionResolver resolver) { + this.resolver = resolver; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) { + + try { + String host = request.getHeader("Host"); + + if (host == null || !isAllowedHost(host)) { + throw new BaseException(AuthError.DISALLOWED_HOST); + } + + filterChain.doFilter(request, response); + + } catch (Exception e) { + resolver.resolveException(request, response, null, e); + } + } + + @Override + protected boolean shouldNotFilter(HttpServletRequest request) { + String path = request.getRequestURI(); + + return !pathMatcher.match("/swagger-ui/**", path) && + !pathMatcher.match("/swagger-ui.html", path) && + !pathMatcher.match("/**/api-docs", path) && + !pathMatcher.match("/api-docs/**", path); + } + + private boolean isAllowedHost(String host) { + //port 제거 + String hostname = host.split(":")[0]; + + return allowedHosts.stream() + .anyMatch(hostname::equals); + } +} diff --git a/src/main/java/com/earseo/gateway/security/jwt/JwtAuthenticationFilter.java b/src/main/java/com/earseo/gateway/security/jwt/JwtAuthenticationFilter.java index 1c79b0c..d78e0ab 100644 --- a/src/main/java/com/earseo/gateway/security/jwt/JwtAuthenticationFilter.java +++ b/src/main/java/com/earseo/gateway/security/jwt/JwtAuthenticationFilter.java @@ -6,8 +6,8 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequestWrapper; import jakarta.servlet.http.HttpServletResponse; -import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; @@ -20,10 +20,13 @@ import java.io.IOException; import java.util.*; -@Slf4j @Component public class JwtAuthenticationFilter extends OncePerRequestFilter { + @Value("${security.path.public}") + private String publicPath; + @Value("${security.path.auth}") + private String authPath; private final JwtValidator jwtValidator; private final HandlerExceptionResolver resolver; private static final AntPathMatcher pathMatcher = new AntPathMatcher(); @@ -69,12 +72,24 @@ protected void doFilterInternal(HttpServletRequest request, } @Override - protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException { + protected boolean shouldNotFilter(HttpServletRequest request) { String path = request.getRequestURI(); + String[] publicPaths = publicPath.split(","); + String[] authPaths = authPath.split(","); - return !pathMatcher.match("/api/user/**", path) && - !pathMatcher.match("/api/admin/**", path) && - pathMatcher.match("/api/**", path); + for (String pattern : authPaths) { + if (pathMatcher.match(pattern, path)) { + return false; + } + } + + for (String pattern : publicPaths) { + if (pathMatcher.match(pattern, path)) { + return true; + } + } + + return false; } private static class MutableHttpServletRequest extends HttpServletRequestWrapper { diff --git a/src/main/resources/application-dev.yaml b/src/main/resources/application-dev.yaml index 5ece495..4bd0ee8 100644 --- a/src/main/resources/application-dev.yaml +++ b/src/main/resources/application-dev.yaml @@ -2,44 +2,6 @@ spring: config: import: - optional:file:/etc/secret/application-secret.yaml - - application: - name: backend-gateway - - cloud: - gateway: - server: - webmvc: - routes: - - id: backend-member - uri: http://backend-member:8080 - predicates: - - Path=/api/member/**, /api/user/member/**, /api/admin/member/** - - - id: backend-sight - uri: http://backend-sight:8080 - predicates: - - Path=/api/sight/**, /api/user/sight/**, /api/admin/sight/** - - - id: backend-story - uri: http://backend-story:8080 - predicates: - - Path=/api/story/**, /api/user/story/**, /api/admin/story/** - - - id: backend-core - uri: http://backend-core:8080 - predicates: - - Path=/api/core/**, /api/user/core/**, /api/admin/core/** - - - id: backend-route - uri: http://backend-route:8080 - predicates: - - Path=/api/route/**, /api/user/route/**, /api/admin/route/** - - - id: backend-search - uri: http://backend-search:8080 - predicates: - - Path=/api/search/**, /api/user/search/**, /api/admin/search/** otel: propagators: - tracecontext diff --git a/src/main/resources/application-local.yaml b/src/main/resources/application-local.yaml index 503bac4..b73c439 100644 --- a/src/main/resources/application-local.yaml +++ b/src/main/resources/application-local.yaml @@ -1,47 +1,8 @@ server: port: 8080 -spring: - application: - name: backend-gateway - - cloud: - gateway: - server: - webmvc: - routes: - - id: backend-member - uri: http://backend-member:8080 - predicates: - - Path=/api/member/**, /api/user/member/**, /api/admin/member/** - - - id: backend-sight - uri: http://backend-sight:8080 - predicates: - - Path=/api/sight/**, /api/user/sight/**, /api/admin/sight/** - - - id: backend-story - uri: http://backend-story:8080 - predicates: - - Path=/api/story/**, /api/user/story/**, /api/admin/story/** - - - id: backend-core - uri: http://backend-core:8080 - predicates: - - Path=/api/core/**, /api/user/core/**, /api/admin/core/** - - - id: backend-route - uri: http://backend-route:8080 - predicates: - - Path=/api/route/**, /api/user/route/**, /api/admin/route/** - - - id: backend-search - uri: http://backend-search:8080 - predicates: - - Path=/api/search/**, /api/user/search/**, /api/admin/search/** - jwt: - access_secret: AADfaskllew3skllew32dsfasdTG764G2dsfasdTG764GDfaskllew3skllew32dsfasdTG764G2ddskllew32dsfasdTG764G + access_secret: ${JWT_SECRET} management: endpoints: diff --git a/src/main/resources/application-test.yaml b/src/main/resources/application-test.yaml index 156566c..e0071a9 100644 --- a/src/main/resources/application-test.yaml +++ b/src/main/resources/application-test.yaml @@ -1,47 +1,8 @@ server: port: 8080 -spring: - application: - name: backend-gateway - - cloud: - gateway: - server: - webmvc: - routes: - - id: backend-member - uri: http://backend-member:8080 - predicates: - - Path=/api/member/**, /api/user/member/**, /api/admin/member/** - - - id: backend-sight - uri: http://backend-sight:8080 - predicates: - - Path=/api/sight/**, /api/user/sight/**, /api/admin/sight/** - - - id: backend-story - uri: http://backend-story:8080 - predicates: - - Path=/api/story/**, /api/user/story/**, /api/admin/story/** - - - id: backend-core - uri: http://backend-core:8080 - predicates: - - Path=/api/core/**, /api/user/core/**, /api/admin/core/** - - - id: backend-route - uri: http://backend-route:8080 - predicates: - - Path=/api/route/**, /api/user/route/**, /api/admin/route/** - - - id: backend-search - uri: http://backend-search:8080 - predicates: - - Path=/api/search/**, /api/user/search/**, /api/admin/search/** - jwt: - access_secret: AADfaskllew3skllew32dsfasdTG764G2dsfasdTG764GDfaskllew3skllew32dsfasdTG764G2ddskllew32dsfasdTG764G + access_secret: ${JWT_SECRET:jwtsecretkeyjwtsecretkeyjwtsecretkeyjwtsecretkeyjwtsecretkeyjwtsecretkey} management: metrics: diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 66f574e..353b8bd 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -1,3 +1,91 @@ spring: profiles: - default: local \ No newline at end of file + default: local + application: + name: backend-gateway + cloud: + gateway: + server: + webmvc: + routes: + - id: backend-member + uri: ${MEMBER_URI:http://example.com} + predicates: + - Path=${MEMBER_API_PATH:/member} + + - id: backend-sight + uri: ${SIGHT_URI:http://example.com} + predicates: + - Path=${SIGHT_API_PATH:/sight} + + - id: backend-story + uri: ${STORY_URI:http://example.com} + predicates: + - Path=${STORY_API_PATH:/story} + + - id: backend-core + uri: ${CORE_URI:http://example.com} + predicates: + - Path=${CORE_API_PATH:/core} + + - id: backend-route + uri: ${ROUTE_URI:http://example.com} + predicates: + - Path=${ROUTE_API_PATH:/route} + + - id: member-openapi + uri: ${MEMBER_URI:http://example.com} + predicates: + - Path=/member/api-docs + filters: + - RewritePath=/member/api-docs, /api-docs + + - id: sight-openapi + uri: ${SIGHT_URI:http://example.com} + predicates: + - Path=/sight/api-docs + filters: + - RewritePath=/sight/api-docs, /api-docs + + - id: story-openapi + uri: ${STORY_URI:http://example.com} + predicates: + - Path=/story/api-docs + filters: + - RewritePath=/story/api-docs, /api-docs + + - id: core-openapi + uri: ${CORE_URI:http://example.com} + predicates: + - Path=/core/api-docs + filters: + - RewritePath=/core/api-docs, /api-docs +springdoc: + api-docs: + enabled: true + path: /api-docs + swagger-ui: + use-root-path: true + enabled: true + path: /swagger-ui.html + display-request-duration: true + operations-sorter: method + tags-sorter: alpha + urls: + - name: backend-member + url: /member/api-docs + - name: backend-sight + url: /sight/api-docs + - name: backend-story + url: /story/api-docs + - name: backend-core + url: /core/api-docs + - name: backend-route + url: /route/api-docs +security: + path: + public: ${PUBLIC_PATHS:publicpaths} + auth: ${AUTH_PATHS:authpaths} + +allowed: + hosts: ${ALLOWED_HOST:localhost}