Bladeren bron

ISSUE-861 Context path fix (#917)

German Osin 3 jaren geleden
bovenliggende
commit
683ab231f4

+ 59 - 0
docker/kafka-ui-auth-context.yaml

@@ -0,0 +1,59 @@
+---
+version: '2'
+services:
+
+  kafka-ui:
+    container_name: kafka-ui
+    image: provectuslabs/kafka-ui:latest
+    ports:
+      - 8080:8080
+    depends_on:
+      - zookeeper0
+      - kafka0
+    environment:
+      KAFKA_CLUSTERS_0_NAME: local
+      KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka0:29092
+      KAFKA_CLUSTERS_0_ZOOKEEPER: zookeeper0:2181
+      KAFKA_CLUSTERS_0_JMXPORT: 9997
+      SERVER_SERVLET_CONTEXT_PATH: /kafkaui
+      AUTH_ENABLED: "true"
+      SPRING_SECURITY_USER_NAME: admin
+      SPRING_SECURITY_USER_PASSWORD: pass
+
+  zookeeper0:
+    image: confluentinc/cp-zookeeper:5.2.4
+    environment:
+      ZOOKEEPER_CLIENT_PORT: 2181
+      ZOOKEEPER_TICK_TIME: 2000
+    ports:
+      - 2181:2181
+
+  kafka0:
+    image: confluentinc/cp-kafka:5.2.4
+    depends_on:
+      - zookeeper0
+    ports:
+      - 9092:9092
+      - 9997:9997
+    environment:
+      KAFKA_BROKER_ID: 1
+      KAFKA_ZOOKEEPER_CONNECT: zookeeper0:2181
+      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka0:29092,PLAINTEXT_HOST://localhost:9092
+      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
+      KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
+      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
+      JMX_PORT: 9997
+      KAFKA_JMX_OPTS: -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=kafka0 -Dcom.sun.management.jmxremote.rmi.port=9997
+
+  kafka-init-topics:
+    image: confluentinc/cp-kafka:5.2.4
+    volumes:
+       - ./message.json:/data/message.json
+    depends_on:
+      - kafka0
+    command: "bash -c 'echo Waiting for Kafka to be ready... && \
+               cub kafka-ready -b kafka0:29092 1 30 && \
+               kafka-topics --create --topic second.users --partitions 3 --replication-factor 1 --if-not-exists --zookeeper zookeeper0:2181 && \
+               kafka-topics --create --topic second.messages --partitions 2 --replication-factor 1 --if-not-exists --zookeeper zookeeper0:2181 && \
+               kafka-topics --create --topic first.messages --partitions 2 --replication-factor 1 --if-not-exists --zookeeper zookeeper0:2181 && \
+               kafka-console-producer --broker-list kafka0:29092 -topic second.users < /data/message.json'"

+ 33 - 0
kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/Config.java

@@ -2,20 +2,53 @@ package com.provectus.kafka.ui.config;
 
 
 import com.provectus.kafka.ui.model.JmxConnectionInfo;
 import com.provectus.kafka.ui.model.JmxConnectionInfo;
 import com.provectus.kafka.ui.util.JmxPoolFactory;
 import com.provectus.kafka.ui.util.JmxPoolFactory;
+import java.util.Collections;
+import java.util.Map;
 import javax.management.remote.JMXConnector;
 import javax.management.remote.JMXConnector;
+import lombok.AllArgsConstructor;
 import org.apache.commons.pool2.KeyedObjectPool;
 import org.apache.commons.pool2.KeyedObjectPool;
 import org.apache.commons.pool2.impl.GenericKeyedObjectPool;
 import org.apache.commons.pool2.impl.GenericKeyedObjectPool;
 import org.apache.commons.pool2.impl.GenericKeyedObjectPoolConfig;
 import org.apache.commons.pool2.impl.GenericKeyedObjectPoolConfig;
+import org.springframework.beans.factory.ObjectProvider;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.web.ServerProperties;
+import org.springframework.boot.autoconfigure.web.reactive.WebFluxProperties;
+import org.springframework.context.ApplicationContext;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.context.annotation.Configuration;
+import org.springframework.http.server.reactive.ContextPathCompositeHandler;
+import org.springframework.http.server.reactive.HttpHandler;
 import org.springframework.jmx.export.MBeanExporter;
 import org.springframework.jmx.export.MBeanExporter;
+import org.springframework.util.StringUtils;
 import org.springframework.util.unit.DataSize;
 import org.springframework.util.unit.DataSize;
 import org.springframework.web.reactive.function.client.WebClient;
 import org.springframework.web.reactive.function.client.WebClient;
+import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
 
 
 @Configuration
 @Configuration
+@AllArgsConstructor
 public class Config {
 public class Config {
 
 
+  private final ApplicationContext applicationContext;
+
+  private final ServerProperties serverProperties;
+
+  @Bean
+  public HttpHandler httpHandler(ObjectProvider<WebFluxProperties> propsProvider) {
+
+    final String basePath = serverProperties.getServlet().getContextPath();
+
+    HttpHandler httpHandler = WebHttpHandlerBuilder
+        .applicationContext(this.applicationContext).build();
+
+    if (StringUtils.hasText(basePath)) {
+      Map<String, HttpHandler> handlersMap =
+          Collections.singletonMap(basePath, httpHandler);
+      return new ContextPathCompositeHandler(handlersMap);
+    }
+    return httpHandler;
+  }
+
+
   @Bean
   @Bean
   public KeyedObjectPool<JmxConnectionInfo, JMXConnector> pool() {
   public KeyedObjectPool<JmxConnectionInfo, JMXConnector> pool() {
     var pool = new GenericKeyedObjectPool<>(new JmxPoolFactory());
     var pool = new GenericKeyedObjectPool<>(new JmxPoolFactory());

+ 19 - 6
kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/CorsGlobalConfiguration.java

@@ -1,45 +1,58 @@
 package com.provectus.kafka.ui.config;
 package com.provectus.kafka.ui.config;
 
 
+import lombok.AllArgsConstructor;
+import org.springframework.boot.autoconfigure.web.ServerProperties;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.context.annotation.Profile;
 import org.springframework.context.annotation.Profile;
 import org.springframework.core.io.ClassPathResource;
 import org.springframework.core.io.ClassPathResource;
+import org.springframework.util.StringUtils;
 import org.springframework.web.reactive.config.CorsRegistry;
 import org.springframework.web.reactive.config.CorsRegistry;
-import org.springframework.web.reactive.config.EnableWebFlux;
 import org.springframework.web.reactive.config.WebFluxConfigurer;
 import org.springframework.web.reactive.config.WebFluxConfigurer;
 import org.springframework.web.reactive.function.server.RouterFunction;
 import org.springframework.web.reactive.function.server.RouterFunction;
 import org.springframework.web.reactive.function.server.RouterFunctions;
 import org.springframework.web.reactive.function.server.RouterFunctions;
 import org.springframework.web.reactive.function.server.ServerResponse;
 import org.springframework.web.reactive.function.server.ServerResponse;
 
 
 @Configuration
 @Configuration
-@EnableWebFlux
 @Profile("local")
 @Profile("local")
+@AllArgsConstructor
 public class CorsGlobalConfiguration implements WebFluxConfigurer {
 public class CorsGlobalConfiguration implements WebFluxConfigurer {
 
 
+  private final ServerProperties serverProperties;
+
   @Override
   @Override
   public void addCorsMappings(CorsRegistry registry) {
   public void addCorsMappings(CorsRegistry registry) {
     registry.addMapping("/**")
     registry.addMapping("/**")
         .allowedOrigins("*")
         .allowedOrigins("*")
         .allowedMethods("*")
         .allowedMethods("*")
         .allowedHeaders("*")
         .allowedHeaders("*")
-        .allowCredentials(true);
+        .allowCredentials(false);
+  }
+
+  private String withContext(String pattern) {
+    final String basePath = serverProperties.getServlet().getContextPath();
+    if (StringUtils.hasText(basePath)) {
+      return basePath + pattern;
+    } else {
+      return pattern;
+    }
   }
   }
 
 
   @Bean
   @Bean
   public RouterFunction<ServerResponse> cssFilesRouter() {
   public RouterFunction<ServerResponse> cssFilesRouter() {
     return RouterFunctions
     return RouterFunctions
-        .resources("/static/css/**", new ClassPathResource("static/static/css/"));
+        .resources(withContext("/static/css/**"), new ClassPathResource("static/static/css/"));
   }
   }
 
 
   @Bean
   @Bean
   public RouterFunction<ServerResponse> jsFilesRouter() {
   public RouterFunction<ServerResponse> jsFilesRouter() {
     return RouterFunctions
     return RouterFunctions
-        .resources("/static/js/**", new ClassPathResource("static/static/js/"));
+        .resources(withContext("/static/js/**"), new ClassPathResource("static/static/js/"));
   }
   }
 
 
   @Bean
   @Bean
   public RouterFunction<ServerResponse> mediaFilesRouter() {
   public RouterFunction<ServerResponse> mediaFilesRouter() {
     return RouterFunctions
     return RouterFunctions
-        .resources("/static/media/**", new ClassPathResource("static/static/media/"));
+        .resources(withContext("/static/media/**"), new ClassPathResource("static/static/media/"));
   }
   }
 }
 }

+ 12 - 20
kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/CustomWebFilter.java

@@ -1,6 +1,5 @@
 package com.provectus.kafka.ui.config;
 package com.provectus.kafka.ui.config;
 
 
-import org.springframework.boot.autoconfigure.web.ServerProperties;
 import org.springframework.stereotype.Component;
 import org.springframework.stereotype.Component;
 import org.springframework.web.server.ServerWebExchange;
 import org.springframework.web.server.ServerWebExchange;
 import org.springframework.web.server.WebFilter;
 import org.springframework.web.server.WebFilter;
@@ -8,32 +7,25 @@ import org.springframework.web.server.WebFilterChain;
 import reactor.core.publisher.Mono;
 import reactor.core.publisher.Mono;
 
 
 @Component
 @Component
-
 public class CustomWebFilter implements WebFilter {
 public class CustomWebFilter implements WebFilter {
 
 
-  private final ServerProperties serverProperties;
-
-  public CustomWebFilter(ServerProperties serverProperties) {
-    this.serverProperties = serverProperties;
-  }
-
   @Override
   @Override
   public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
   public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
-    String contextPath = serverProperties.getServlet().getContextPath() != null 
-        ? serverProperties.getServlet().getContextPath() : "";
 
 
-    final String path = exchange.getRequest().getURI().getPath().replaceAll("/$", "");
-    if (path.equals(contextPath) || path.startsWith(contextPath + "/ui")) {
-      return chain.filter(
-          exchange.mutate().request(exchange.getRequest().mutate().path("/index.html").build())
-              .build()
-      );
-    } else if (path.startsWith(contextPath)) {
+    final String basePath = exchange.getRequest().getPath().contextPath().value();
+
+    final String path = exchange.getRequest().getPath().pathWithinApplication().value();
+
+    if (path.startsWith("/ui") || path.equals("/")) {
       return chain.filter(
       return chain.filter(
-          exchange.mutate().request(exchange.getRequest().mutate().contextPath(contextPath).build())
-              .build()
+          exchange.mutate().request(
+              exchange.getRequest().mutate()
+                  .path(basePath + "/index.html")
+                  .contextPath(basePath)
+                  .build()
+          ).build()
       );
       );
-    }    
+    }
 
 
     return chain.filter(exchange);
     return chain.filter(exchange);
   }
   }

+ 21 - 10
kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/OAuthSecurityConfig.java

@@ -1,6 +1,7 @@
 package com.provectus.kafka.ui.config;
 package com.provectus.kafka.ui.config;
 
 
-import org.springframework.beans.factory.annotation.Autowired;
+import com.provectus.kafka.ui.util.EmptyRedirectStrategy;
+import lombok.AllArgsConstructor;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.context.ApplicationContext;
 import org.springframework.context.ApplicationContext;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Bean;
@@ -8,18 +9,20 @@ import org.springframework.context.annotation.Configuration;
 import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
 import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
 import org.springframework.security.config.web.server.ServerHttpSecurity;
 import org.springframework.security.config.web.server.ServerHttpSecurity;
 import org.springframework.security.web.server.SecurityWebFilterChain;
 import org.springframework.security.web.server.SecurityWebFilterChain;
+import org.springframework.security.web.server.authentication.RedirectServerAuthenticationSuccessHandler;
 import org.springframework.util.ClassUtils;
 import org.springframework.util.ClassUtils;
 
 
 @Configuration
 @Configuration
 @EnableWebFluxSecurity
 @EnableWebFluxSecurity
 @ConditionalOnProperty(value = "auth.enabled", havingValue = "true")
 @ConditionalOnProperty(value = "auth.enabled", havingValue = "true")
+@AllArgsConstructor
 public class OAuthSecurityConfig {
 public class OAuthSecurityConfig {
 
 
   public static final String REACTIVE_CLIENT_REGISTRATION_REPOSITORY_CLASSNAME =
   public static final String REACTIVE_CLIENT_REGISTRATION_REPOSITORY_CLASSNAME =
       "org.springframework.security.oauth2.client.registration."
       "org.springframework.security.oauth2.client.registration."
           + "ReactiveClientRegistrationRepository";
           + "ReactiveClientRegistrationRepository";
 
 
-  private static final boolean isOAuth2Present = ClassUtils.isPresent(
+  private static final boolean IS_OAUTH2_PRESENT = ClassUtils.isPresent(
       REACTIVE_CLIENT_REGISTRATION_REPOSITORY_CLASSNAME,
       REACTIVE_CLIENT_REGISTRATION_REPOSITORY_CLASSNAME,
       OAuthSecurityConfig.class.getClassLoader()
       OAuthSecurityConfig.class.getClassLoader()
   );
   );
@@ -31,37 +34,45 @@ public class OAuthSecurityConfig {
       "/resources/**",
       "/resources/**",
       "/actuator/health",
       "/actuator/health",
       "/actuator/info",
       "/actuator/info",
+      "/auth",
       "/login",
       "/login",
       "/logout",
       "/logout",
       "/oauth2/**"
       "/oauth2/**"
   };
   };
 
 
-  @Autowired
-  ApplicationContext context;
+  private final ApplicationContext context;
 
 
   @Bean
   @Bean
   public SecurityWebFilterChain configure(ServerHttpSecurity http) {
   public SecurityWebFilterChain configure(ServerHttpSecurity http) {
     http.authorizeExchange()
     http.authorizeExchange()
-        .pathMatchers(AUTH_WHITELIST).permitAll()
+        .pathMatchers(
+            AUTH_WHITELIST
+        ).permitAll()
         .anyExchange()
         .anyExchange()
         .authenticated();
         .authenticated();
 
 
-    if (isOAuth2Present && OAuth2ClasspathGuard.shouldConfigure(this.context)) {
+    if (IS_OAUTH2_PRESENT && OAuth2ClasspathGuard.shouldConfigure(this.context)) {
       OAuth2ClasspathGuard.configure(this.context, http);
       OAuth2ClasspathGuard.configure(this.context, http);
     } else {
     } else {
+      final RedirectServerAuthenticationSuccessHandler handler =
+          new RedirectServerAuthenticationSuccessHandler();
+      handler.setRedirectStrategy(new EmptyRedirectStrategy());
+
       http
       http
           .httpBasic().and()
           .httpBasic().and()
-          .formLogin();
+          .formLogin()
+          .loginPage("/auth")
+          .authenticationSuccessHandler(handler);
     }
     }
 
 
-    SecurityWebFilterChain result = http.csrf().disable().build();
-    return result;
+    return http.csrf().disable().build();
   }
   }
 
 
   private static class OAuth2ClasspathGuard {
   private static class OAuth2ClasspathGuard {
     static void configure(ApplicationContext context, ServerHttpSecurity http) {
     static void configure(ApplicationContext context, ServerHttpSecurity http) {
       http
       http
-          .oauth2Login().and()
+          .oauth2Login()
+          .and()
           .oauth2Client();
           .oauth2Client();
     }
     }
 
 

+ 1 - 1
kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/ReadOnlyModeFilter.java

@@ -31,7 +31,7 @@ public class ReadOnlyModeFilter implements WebFilter {
       return chain.filter(exchange);
       return chain.filter(exchange);
     }
     }
 
 
-    var path = exchange.getRequest().getURI().getPath();
+    var path = exchange.getRequest().getPath().pathWithinApplication().value();
     var matcher = CLUSTER_NAME_REGEX.matcher(path);
     var matcher = CLUSTER_NAME_REGEX.matcher(path);
     if (!matcher.find()) {
     if (!matcher.find()) {
       return chain.filter(exchange);
       return chain.filter(exchange);

+ 100 - 0
kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/AuthController.java

@@ -0,0 +1,100 @@
+package com.provectus.kafka.ui.controller;
+
+import java.nio.charset.Charset;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.log4j.Log4j2;
+import org.springframework.security.web.server.csrf.CsrfToken;
+import org.springframework.util.MultiValueMap;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.server.ServerWebExchange;
+import reactor.core.publisher.Mono;
+
+@RestController
+@RequiredArgsConstructor
+@Log4j2
+public class AuthController {
+
+  @GetMapping(value = "/auth", produces = { "text/html" })
+  private Mono<byte[]> getAuth(ServerWebExchange exchange) {
+    Mono<CsrfToken> token = exchange.getAttributeOrDefault(CsrfToken.class.getName(), Mono.empty());
+    return token
+        .map(AuthController::csrfToken)
+        .defaultIfEmpty("")
+        .map(csrfTokenHtmlInput -> createPage(exchange, csrfTokenHtmlInput));
+  }
+
+  private byte[] createPage(ServerWebExchange exchange, String csrfTokenHtmlInput) {
+    MultiValueMap<String, String> queryParams = exchange.getRequest()
+        .getQueryParams();
+    String contextPath = exchange.getRequest().getPath().contextPath().value();
+    String page =
+        "<!DOCTYPE html>\n" + "<html lang=\"en\">\n" + "  <head>\n"
+        + "    <meta charset=\"utf-8\">\n"
+        + "    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, "
+        + "shrink-to-fit=no\">\n"
+        + "    <meta name=\"description\" content=\"\">\n"
+        + "    <meta name=\"author\" content=\"\">\n"
+        + "    <title>Please sign in</title>\n"
+        + "    <link href=\"https://maxcdn.bootstrapcdn.com/bootstrap/"
+        + "4.0.0-beta/css/bootstrap.min.css\" rel=\"stylesheet\" "
+        + "integrity=\"sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M\" "
+        + "crossorigin=\"anonymous\">\n"
+        + "    <link href=\"https://getbootstrap.com/docs/4.0/examples/signin/signin.css\" "
+        + "rel=\"stylesheet\" crossorigin=\"anonymous\"/>\n"
+        + "  </head>\n"
+        + "  <body>\n"
+        + "     <div class=\"container\">\n"
+        + formLogin(queryParams, contextPath, csrfTokenHtmlInput)
+        + "    </div>\n"
+        + "  </body>\n"
+        + "</html>";
+
+    return page.getBytes(Charset.defaultCharset());
+  }
+
+  private String formLogin(
+      MultiValueMap<String, String> queryParams,
+      String contextPath, String csrfTokenHtmlInput) {
+
+    boolean isError = queryParams.containsKey("error");
+    boolean isLogoutSuccess = queryParams.containsKey("logout");
+    return
+        "      <form class=\"form-signin\" method=\"post\" action=\"" + contextPath + "/auth\">\n"
+        + "        <h2 class=\"form-signin-heading\">Please sign in</h2>\n"
+        + createError(isError)
+        + createLogoutSuccess(isLogoutSuccess)
+        + "        <p>\n"
+        + "          <label for=\"username\" class=\"sr-only\">Username</label>\n"
+        + "          <input type=\"text\" id=\"username\" name=\"username\" class=\"form-control\" "
+        + "placeholder=\"Username\" required autofocus>\n"
+        + "        </p>\n" + "        <p>\n"
+        + "          <label for=\"password\" class=\"sr-only\">Password</label>\n"
+        + "          <input type=\"password\" id=\"password\" name=\"password\" "
+        + "class=\"form-control\" placeholder=\"Password\" required>\n"
+        + "        </p>\n" + csrfTokenHtmlInput
+        + "        <button class=\"btn btn-lg btn-primary btn-block\" "
+        + "type=\"submit\">Sign in</button>\n"
+        + "      </form>\n";
+  }
+
+  private static String csrfToken(CsrfToken token) {
+    return "          <input type=\"hidden\" name=\""
+        + token.getParameterName()
+        + "\" value=\""
+        + token.getToken()
+        + "\">\n";
+  }
+
+  private static String createError(boolean isError) {
+    return isError
+        ? "<div class=\"alert alert-danger\" role=\"alert\">Invalid credentials</div>"
+        : "";
+  }
+
+  private static String createLogoutSuccess(boolean isLogoutSuccess) {
+    return isLogoutSuccess
+        ? "<div class=\"alert alert-success\" role=\"alert\">You have been signed out</div>"
+        : "";
+  }
+}

+ 7 - 8
kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/StaticController.java

@@ -1,6 +1,7 @@
 package com.provectus.kafka.ui.controller;
 package com.provectus.kafka.ui.controller;
 
 
 import com.provectus.kafka.ui.util.ResourceUtil;
 import com.provectus.kafka.ui.util.ResourceUtil;
+import java.util.Optional;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.concurrent.atomic.AtomicReference;
 import lombok.RequiredArgsConstructor;
 import lombok.RequiredArgsConstructor;
 import lombok.SneakyThrows;
 import lombok.SneakyThrows;
@@ -11,27 +12,27 @@ import org.springframework.core.io.Resource;
 import org.springframework.http.ResponseEntity;
 import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RestController;
 import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.server.ServerWebExchange;
 import reactor.core.publisher.Mono;
 import reactor.core.publisher.Mono;
 
 
 @RestController
 @RestController
 @RequiredArgsConstructor
 @RequiredArgsConstructor
 @Log4j2
 @Log4j2
 public class StaticController {
 public class StaticController {
-  private final ServerProperties serverProperties;
 
 
   @Value("classpath:static/index.html")
   @Value("classpath:static/index.html")
   private Resource indexFile;
   private Resource indexFile;
   private final AtomicReference<String> renderedIndexFile = new AtomicReference<>();
   private final AtomicReference<String> renderedIndexFile = new AtomicReference<>();
 
 
   @GetMapping(value = "/index.html", produces = { "text/html" })
   @GetMapping(value = "/index.html", produces = { "text/html" })
-  public Mono<ResponseEntity<String>> getIndex() {
-    return Mono.just(ResponseEntity.ok(getRenderedIndexFile()));
+  public Mono<ResponseEntity<String>> getIndex(ServerWebExchange exchange) {
+    return Mono.just(ResponseEntity.ok(getRenderedIndexFile(exchange)));
   }
   }
 
 
-  public String getRenderedIndexFile() {
+  public String getRenderedIndexFile(ServerWebExchange exchange) {
     String rendered = renderedIndexFile.get();
     String rendered = renderedIndexFile.get();
     if (rendered == null) {
     if (rendered == null) {
-      rendered = buildIndexFile();
+      rendered = buildIndexFile(exchange.getRequest().getPath().contextPath().value());
       if (renderedIndexFile.compareAndSet(null, rendered)) {
       if (renderedIndexFile.compareAndSet(null, rendered)) {
         return rendered;
         return rendered;
       } else {
       } else {
@@ -43,9 +44,7 @@ public class StaticController {
   }
   }
 
 
   @SneakyThrows
   @SneakyThrows
-  private String buildIndexFile() {
-    final String contextPath = serverProperties.getServlet().getContextPath() != null
-        ? serverProperties.getServlet().getContextPath() : "";
+  private String buildIndexFile(String contextPath) {
     final String staticPath = contextPath + "/static";
     final String staticPath = contextPath + "/static";
     return ResourceUtil.readAsString(indexFile)
     return ResourceUtil.readAsString(indexFile)
         .replace("href=\"./static", "href=\"" + staticPath)
         .replace("href=\"./static", "href=\"" + staticPath)

+ 50 - 0
kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/EmptyRedirectStrategy.java

@@ -0,0 +1,50 @@
+package com.provectus.kafka.ui.util;
+
+import java.net.URI;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.server.reactive.ServerHttpResponse;
+import org.springframework.security.web.server.ServerRedirectStrategy;
+import org.springframework.util.Assert;
+import org.springframework.web.server.ServerWebExchange;
+import reactor.core.publisher.Mono;
+
+public class EmptyRedirectStrategy implements ServerRedirectStrategy {
+
+  private HttpStatus httpStatus = HttpStatus.FOUND;
+
+  private boolean contextRelative = true;
+
+  public Mono<Void> sendRedirect(ServerWebExchange exchange, URI location) {
+    Assert.notNull(exchange, "exchange cannot be null");
+    Assert.notNull(location, "location cannot be null");
+    return Mono.fromRunnable(() -> {
+      ServerHttpResponse response = exchange.getResponse();
+      response.setStatusCode(this.httpStatus);
+      response.getHeaders().setLocation(createLocation(exchange, location));
+    });
+  }
+
+  private URI createLocation(ServerWebExchange exchange, URI location) {
+    if (!this.contextRelative) {
+      return location;
+    }
+
+    String url = location.getPath().isEmpty() ? "/"
+        : location.toASCIIString();
+
+    if (url.startsWith("/")) {
+      String context = exchange.getRequest().getPath().contextPath().value();
+      return URI.create(context + url);
+    }
+    return location;
+  }
+
+  public void setHttpStatus(HttpStatus httpStatus) {
+    Assert.notNull(httpStatus, "httpStatus cannot be null");
+    this.httpStatus = httpStatus;
+  }
+
+  public void setContextRelative(boolean contextRelative) {
+    this.contextRelative = contextRelative;
+  }
+}