ISSUE-861 Context path fix (#917)
This commit is contained in:
parent
f3419fbb85
commit
683ab231f4
9 changed files with 302 additions and 45 deletions
59
docker/kafka-ui-auth-context.yaml
Normal file
59
docker/kafka-ui-auth-context.yaml
Normal file
|
@ -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'"
|
|
@ -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());
|
||||||
|
|
|
@ -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/"));
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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,30 +7,23 @@ 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("/$", "");
|
final String basePath = exchange.getRequest().getPath().contextPath().value();
|
||||||
if (path.equals(contextPath) || path.startsWith(contextPath + "/ui")) {
|
|
||||||
|
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().path("/index.html").build())
|
exchange.mutate().request(
|
||||||
.build()
|
exchange.getRequest().mutate()
|
||||||
);
|
.path(basePath + "/index.html")
|
||||||
} else if (path.startsWith(contextPath)) {
|
.contextPath(basePath)
|
||||||
return chain.filter(
|
|
||||||
exchange.mutate().request(exchange.getRequest().mutate().contextPath(contextPath).build())
|
|
||||||
.build()
|
.build()
|
||||||
|
).build()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
private final ApplicationContext context;
|
||||||
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 http.csrf().disable().build();
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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>"
|
||||||
|
: "";
|
||||||
|
}
|
||||||
|
}
|
|
@ -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() {
|
public Mono<ResponseEntity<String>> getIndex(ServerWebExchange exchange) {
|
||||||
return Mono.just(ResponseEntity.ok(getRenderedIndexFile()));
|
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() {
|
private String buildIndexFile(String contextPath) {
|
||||||
final String contextPath = serverProperties.getServlet().getContextPath() != null
|
|
||||||
? serverProperties.getServlet().getContextPath() : "";
|
|
||||||
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)
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue