This commit is contained in:
@@ -3,9 +3,11 @@ package com.kaidi.finance.setup;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
|
||||
@Configuration
|
||||
@ConditionalOnProperty(name = "finance.setup.enabled", havingValue = "true")
|
||||
@@ -19,8 +21,20 @@ public class SetupSecurityConfig {
|
||||
.formLogin(AbstractHttpConfigurer::disable)
|
||||
.logout(AbstractHttpConfigurer::disable)
|
||||
.authorizeHttpRequests(authorize -> authorize
|
||||
.requestMatchers("/api/v1/setup/**", "/actuator/health/**", "/error").permitAll()
|
||||
.requestMatchers("/api/v1/setup/**", "/actuator/health/**", "/error",
|
||||
"/", "/index.html", "/favicon.ico", "/assets/**").permitAll()
|
||||
.requestMatchers((RequestMatcher) request -> isSpaRoute(request)).permitAll()
|
||||
.anyRequest().denyAll());
|
||||
return http.build();
|
||||
}
|
||||
|
||||
private boolean isSpaRoute(jakarta.servlet.http.HttpServletRequest request) {
|
||||
if (!HttpMethod.GET.matches(request.getMethod()) && !HttpMethod.HEAD.matches(request.getMethod())) {
|
||||
return false;
|
||||
}
|
||||
String path = request.getRequestURI();
|
||||
return !path.contains(".") && !path.equals("/api") && !path.startsWith("/api/")
|
||||
&& !path.equals("/actuator") && !path.startsWith("/actuator/")
|
||||
&& !path.equals("/error");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
|
||||
import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
|
||||
@Configuration
|
||||
@EnableMethodSecurity
|
||||
@@ -37,7 +38,9 @@ public class SecurityConfig {
|
||||
.authorizeHttpRequests(authorize -> authorize
|
||||
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
|
||||
.requestMatchers("/api/v1/auth/csrf", "/api/v1/auth/login", "/actuator/health/**",
|
||||
"/api-docs/**", "/swagger-ui.html", "/swagger-ui/**", "/error").permitAll()
|
||||
"/api-docs/**", "/swagger-ui.html", "/swagger-ui/**", "/error",
|
||||
"/", "/index.html", "/favicon.ico", "/assets/**").permitAll()
|
||||
.requestMatchers((RequestMatcher) request -> isSpaRoute(request)).permitAll()
|
||||
.requestMatchers("/api/v1/**").permitAll()
|
||||
.anyRequest().authenticated())
|
||||
.exceptionHandling(exceptions -> exceptions
|
||||
@@ -48,6 +51,18 @@ public class SecurityConfig {
|
||||
return http.build();
|
||||
}
|
||||
|
||||
private boolean isSpaRoute(jakarta.servlet.http.HttpServletRequest request) {
|
||||
if (!HttpMethod.GET.matches(request.getMethod()) && !HttpMethod.HEAD.matches(request.getMethod())) {
|
||||
return false;
|
||||
}
|
||||
String path = request.getRequestURI();
|
||||
return !path.contains(".") && !path.equals("/api") && !path.startsWith("/api/")
|
||||
&& !path.equals("/actuator") && !path.startsWith("/actuator/")
|
||||
&& !path.equals("/api-docs") && !path.startsWith("/api-docs/")
|
||||
&& !path.equals("/swagger-ui.html") && !path.startsWith("/swagger-ui/")
|
||||
&& !path.equals("/error");
|
||||
}
|
||||
|
||||
@Bean
|
||||
PasswordEncoder passwordEncoder() {
|
||||
return Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8();
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.kaidi.finance.shared.web;
|
||||
|
||||
import jakarta.servlet.DispatcherType;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
/** Forwards extensionless browser routes to the bundled Vue entry point. */
|
||||
@Component
|
||||
public class SpaForwardFilter extends OncePerRequestFilter {
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
|
||||
FilterChain filterChain) throws ServletException, IOException {
|
||||
if (isBrowserRoute(request)) {
|
||||
request.getRequestDispatcher("/index.html").forward(request, response);
|
||||
return;
|
||||
}
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
private boolean isBrowserRoute(HttpServletRequest request) {
|
||||
if (!"GET".equalsIgnoreCase(request.getMethod()) && !"HEAD".equalsIgnoreCase(request.getMethod())) {
|
||||
return false;
|
||||
}
|
||||
String path = request.getRequestURI();
|
||||
String contextPath = request.getContextPath();
|
||||
if (contextPath != null && !contextPath.isEmpty() && path.startsWith(contextPath)) {
|
||||
path = path.substring(contextPath.length());
|
||||
}
|
||||
if (path.isEmpty()) {
|
||||
path = "/";
|
||||
}
|
||||
if (isReserved(path) || path.contains(".")) {
|
||||
return false;
|
||||
}
|
||||
return DispatcherType.REQUEST.equals(request.getDispatcherType());
|
||||
}
|
||||
|
||||
private boolean isReserved(String path) {
|
||||
return path.equals("/api") || path.startsWith("/api/")
|
||||
|| path.equals("/actuator") || path.startsWith("/actuator/")
|
||||
|| path.equals("/api-docs") || path.startsWith("/api-docs/")
|
||||
|| path.equals("/swagger-ui.html") || path.startsWith("/swagger-ui/")
|
||||
|| path.equals("/error");
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.kaidi.setup;
|
||||
|
||||
import com.kaidi.finance.setup.SetupProperties;
|
||||
import com.kaidi.finance.shared.web.SpaForwardFilter;
|
||||
import org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
@@ -9,7 +11,7 @@ import org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration
|
||||
import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import com.kaidi.finance.setup.SetupProperties;
|
||||
import org.springframework.context.annotation.Import;
|
||||
|
||||
/**
|
||||
* Minimal first-run context. It deliberately does not create a business DataSource or run
|
||||
@@ -28,6 +30,7 @@ import com.kaidi.finance.setup.SetupProperties;
|
||||
}
|
||||
)
|
||||
@EnableConfigurationProperties(SetupProperties.class)
|
||||
@Import(SpaForwardFilter.class)
|
||||
public class SetupApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
server:
|
||||
port: ${SERVER_PORT:18080}
|
||||
address: ${SERVER_ADDRESS:127.0.0.1}
|
||||
shutdown: graceful
|
||||
servlet:
|
||||
session:
|
||||
@@ -12,6 +13,9 @@ server:
|
||||
spring:
|
||||
application:
|
||||
name: kaidi-finance
|
||||
web:
|
||||
resources:
|
||||
static-locations: ${FINANCE_STATIC_LOCATIONS:file:./public/}
|
||||
profiles:
|
||||
default: local
|
||||
datasource:
|
||||
|
||||
@@ -31,6 +31,7 @@ class SetupContextSmokeTest {
|
||||
registry.add("finance.setup.env-file", () -> STATE_ROOT.resolve("application.env").toString());
|
||||
registry.add("finance.setup.marker-file", () -> STATE_ROOT.resolve("locked").toString());
|
||||
registry.add("finance.setup.restart-after-complete", () -> false);
|
||||
registry.add("spring.web.resources.static-locations", () -> "classpath:/spa-fixture/");
|
||||
}
|
||||
|
||||
@Autowired
|
||||
@@ -49,6 +50,14 @@ class SetupContextSmokeTest {
|
||||
assertThat(status.getBody().path("data").path("supportedDatabaseTypes").toString())
|
||||
.isEqualTo("[\"MYSQL\"]");
|
||||
|
||||
ResponseEntity<String> setupPage = rest.getForEntity(url("/setup"), String.class);
|
||||
assertThat(setupPage.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
assertThat(setupPage.getBody()).contains("kaidi-spa-fixture");
|
||||
|
||||
ResponseEntity<String> asset = rest.getForEntity(url("/assets/app.js"), String.class);
|
||||
assertThat(asset.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
assertThat(asset.getBody()).contains("kaidi-spa-fixture");
|
||||
|
||||
ResponseEntity<JsonNode> business = rest.getForEntity(url("/api/v1/auth/session"), JsonNode.class);
|
||||
assertThat(business.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.kaidi.finance.shared.web;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
|
||||
class SpaForwardFilterTest {
|
||||
|
||||
private final SpaForwardFilter filter = new SpaForwardFilter();
|
||||
|
||||
@Test
|
||||
void forwardsExtensionlessBrowserRouteToVueEntryPoint() throws Exception {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/finance/workbench");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
AtomicBoolean continued = new AtomicBoolean();
|
||||
|
||||
filter.doFilter(request, response, (ignoredRequest, ignoredResponse) -> continued.set(true));
|
||||
|
||||
assertThat(response.getForwardedUrl()).isEqualTo("/index.html");
|
||||
assertThat(continued).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void leavesApiAndStaticAssetRequestsToSpringMvc() throws Exception {
|
||||
assertContinues("/api/v1/unknown-resource");
|
||||
assertContinues("/actuator/health");
|
||||
assertContinues("/assets/index-a1b2c3.js");
|
||||
}
|
||||
|
||||
private void assertContinues(String uri) throws Exception {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", uri);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
AtomicBoolean continued = new AtomicBoolean();
|
||||
|
||||
filter.doFilter(request, response, (ignoredRequest, ignoredResponse) -> continued.set(true));
|
||||
|
||||
assertThat(continued).isTrue();
|
||||
assertThat(response.getForwardedUrl()).isNull();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
window.__KAIDI_SPA_FIXTURE__ = 'kaidi-spa-fixture';
|
||||
@@ -0,0 +1,5 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head><meta charset="UTF-8"><title>kaidi-spa-fixture</title></head>
|
||||
<body><div id="app">kaidi-spa-fixture</div></body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user