Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor: add system initialization check and redirect to console if …
…not initialized (#3892) #### What type of PR is this? /kind improvement /area core /milestone 2.5.2 #### What this PR does / why we need it: 添加系统初始化检查,如果未初始化则重定向到控制台。 此检查只针对首页,当用户访问首页时检查到未初始化则跳转到 Console 让用户初始化以优化没有数据时的访问体验。 SetupStateCache 用于缓存系统初始化状态,当数据库状态改变时会更新缓存以优化性能,避免每次访问首页都查询数据。 #### Which issue(s) this PR fixes: A part of #3230 #### Does this PR introduce a user-facing change? ```release-note 添加系统初始化检查,如果未初始化则重定向到控制台 ```
- Loading branch information
Showing
4 changed files
with
267 additions
and
0 deletions.
There are no files selected for viewing
96 changes: 96 additions & 0 deletions
96
application/src/main/java/run/halo/app/infra/SetupStateCache.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
package run.halo.app.infra; | ||
|
||
import io.micrometer.common.util.StringUtils; | ||
import java.util.Optional; | ||
import java.util.concurrent.atomic.AtomicBoolean; | ||
import java.util.function.Supplier; | ||
import lombok.Data; | ||
import org.springframework.lang.NonNull; | ||
import org.springframework.stereotype.Component; | ||
import run.halo.app.extension.ConfigMap; | ||
import run.halo.app.extension.ExtensionClient; | ||
import run.halo.app.extension.controller.Controller; | ||
import run.halo.app.extension.controller.ControllerBuilder; | ||
import run.halo.app.extension.controller.Reconciler; | ||
import run.halo.app.infra.utils.JsonUtils; | ||
|
||
/** | ||
* <p>A cache that caches system setup state.</p> | ||
* when setUp state changed, the cache will be updated. | ||
* | ||
* @author guqing | ||
* @since 2.5.2 | ||
*/ | ||
@Component | ||
public class SetupStateCache implements Reconciler<Reconciler.Request>, Supplier<Boolean> { | ||
public static final String SYSTEM_STATES_CONFIGMAP = "system-states"; | ||
private final ExtensionClient client; | ||
|
||
private final InternalValueCache valueCache = new InternalValueCache(); | ||
|
||
public SetupStateCache(ExtensionClient client) { | ||
this.client = client; | ||
} | ||
|
||
/** | ||
* <p>Gets system setup state.</p> | ||
* Never return null. | ||
* | ||
* @return <code>true</code> if system is initialized, <code>false</code> otherwise. | ||
*/ | ||
@NonNull | ||
@Override | ||
public Boolean get() { | ||
return valueCache.get(); | ||
} | ||
|
||
@Override | ||
public Result reconcile(Request request) { | ||
if (!SYSTEM_STATES_CONFIGMAP.equals(request.name())) { | ||
return Result.doNotRetry(); | ||
} | ||
valueCache.cache(isInitialized()); | ||
return Result.doNotRetry(); | ||
} | ||
|
||
@Override | ||
public Controller setupWith(ControllerBuilder builder) { | ||
return builder | ||
.extension(new ConfigMap()) | ||
.build(); | ||
} | ||
|
||
/** | ||
* Check if system is initialized. | ||
* | ||
* @return <code>true</code> if system is initialized, <code>false</code> otherwise. | ||
*/ | ||
private boolean isInitialized() { | ||
return client.fetch(ConfigMap.class, SYSTEM_STATES_CONFIGMAP) | ||
.filter(configMap -> configMap.getData() != null) | ||
.map(ConfigMap::getData) | ||
.flatMap(map -> Optional.ofNullable(map.get(SystemStates.GROUP)) | ||
.filter(StringUtils::isNotBlank) | ||
.map(value -> JsonUtils.jsonToObject(value, SystemStates.class).getIsSetup()) | ||
) | ||
.orElse(false); | ||
} | ||
|
||
@Data | ||
static class SystemStates { | ||
static final String GROUP = "states"; | ||
Boolean isSetup; | ||
} | ||
|
||
static class InternalValueCache { | ||
private final AtomicBoolean value = new AtomicBoolean(false); | ||
|
||
public boolean cache(boolean newValue) { | ||
return value.getAndSet(newValue); | ||
} | ||
|
||
public boolean get() { | ||
return value.get(); | ||
} | ||
} | ||
} |
57 changes: 57 additions & 0 deletions
57
application/src/main/java/run/halo/app/security/InitializeRedirectionWebFilter.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
package run.halo.app.security; | ||
|
||
import java.net.URI; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.http.HttpMethod; | ||
import org.springframework.lang.NonNull; | ||
import org.springframework.security.web.server.DefaultServerRedirectStrategy; | ||
import org.springframework.security.web.server.ServerRedirectStrategy; | ||
import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher; | ||
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher; | ||
import org.springframework.stereotype.Component; | ||
import org.springframework.util.Assert; | ||
import org.springframework.web.server.ServerWebExchange; | ||
import org.springframework.web.server.WebFilter; | ||
import org.springframework.web.server.WebFilterChain; | ||
import reactor.core.publisher.Mono; | ||
import run.halo.app.infra.SetupStateCache; | ||
|
||
/** | ||
* A web filter that will redirect user to set up page if system is not initialized. | ||
* | ||
* @author guqing | ||
* @since 2.5.2 | ||
*/ | ||
@Component | ||
@RequiredArgsConstructor | ||
public class InitializeRedirectionWebFilter implements WebFilter { | ||
private final URI location = URI.create("/console"); | ||
private final ServerWebExchangeMatcher redirectMatcher = | ||
new PathPatternParserServerWebExchangeMatcher("/", HttpMethod.GET); | ||
|
||
private final SetupStateCache setupStateCache; | ||
|
||
private ServerRedirectStrategy redirectStrategy = new DefaultServerRedirectStrategy(); | ||
|
||
@Override | ||
@NonNull | ||
public Mono<Void> filter(@NonNull ServerWebExchange exchange, @NonNull WebFilterChain chain) { | ||
return redirectMatcher.matches(exchange) | ||
.flatMap(matched -> { | ||
if (!matched.isMatch() || setupStateCache.get()) { | ||
return chain.filter(exchange); | ||
} | ||
// Redirect to set up page if system is not initialized. | ||
return redirectStrategy.sendRedirect(exchange, location); | ||
}); | ||
} | ||
|
||
public ServerRedirectStrategy getRedirectStrategy() { | ||
return redirectStrategy; | ||
} | ||
|
||
public void setRedirectStrategy(ServerRedirectStrategy redirectStrategy) { | ||
Assert.notNull(redirectStrategy, "redirectStrategy cannot be null"); | ||
this.redirectStrategy = redirectStrategy; | ||
} | ||
} |
109 changes: 109 additions & 0 deletions
109
application/src/test/java/run/halo/app/security/InitializeRedirectionWebFilterTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
package run.halo.app.security; | ||
|
||
import static org.mockito.ArgumentMatchers.any; | ||
import static org.mockito.ArgumentMatchers.eq; | ||
import static org.mockito.Mockito.mock; | ||
import static org.mockito.Mockito.never; | ||
import static org.mockito.Mockito.verify; | ||
import static org.mockito.Mockito.when; | ||
|
||
import java.net.URI; | ||
import org.junit.jupiter.api.BeforeEach; | ||
import org.junit.jupiter.api.Test; | ||
import org.junit.jupiter.api.extension.ExtendWith; | ||
import org.mockito.InjectMocks; | ||
import org.mockito.Mock; | ||
import org.mockito.junit.jupiter.MockitoExtension; | ||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest; | ||
import org.springframework.mock.web.server.MockServerWebExchange; | ||
import org.springframework.security.web.server.ServerRedirectStrategy; | ||
import org.springframework.web.server.WebFilterChain; | ||
import reactor.core.publisher.Mono; | ||
import reactor.test.StepVerifier; | ||
import run.halo.app.infra.SetupStateCache; | ||
|
||
/** | ||
* Tests for {@link InitializeRedirectionWebFilter}. | ||
* | ||
* @author guqing | ||
* @since 2.5.2 | ||
*/ | ||
@ExtendWith(MockitoExtension.class) | ||
class InitializeRedirectionWebFilterTest { | ||
|
||
@Mock | ||
private SetupStateCache setupStateCache; | ||
|
||
@Mock | ||
private ServerRedirectStrategy serverRedirectStrategy; | ||
|
||
@InjectMocks | ||
private InitializeRedirectionWebFilter filter; | ||
|
||
@BeforeEach | ||
void setUp() { | ||
filter.setRedirectStrategy(serverRedirectStrategy); | ||
} | ||
|
||
@Test | ||
void shouldRedirectWhenSystemNotInitialized() { | ||
when(setupStateCache.get()).thenReturn(false); | ||
|
||
WebFilterChain chain = mock(WebFilterChain.class); | ||
|
||
MockServerHttpRequest request = MockServerHttpRequest.get("/").build(); | ||
MockServerWebExchange exchange = MockServerWebExchange.from(request); | ||
|
||
when(serverRedirectStrategy.sendRedirect(any(), any())).thenReturn(Mono.empty().then()); | ||
|
||
Mono<Void> result = filter.filter(exchange, chain); | ||
|
||
StepVerifier.create(result) | ||
.expectNextCount(0) | ||
.expectComplete() | ||
.verify(); | ||
|
||
verify(serverRedirectStrategy).sendRedirect(eq(exchange), eq(URI.create("/console"))); | ||
verify(chain, never()).filter(eq(exchange)); | ||
} | ||
|
||
@Test | ||
void shouldNotRedirectWhenSystemInitialized() { | ||
when(setupStateCache.get()).thenReturn(true); | ||
|
||
WebFilterChain chain = mock(WebFilterChain.class); | ||
|
||
MockServerHttpRequest request = MockServerHttpRequest.get("/").build(); | ||
MockServerWebExchange exchange = MockServerWebExchange.from(request); | ||
when(chain.filter(any())).thenReturn(Mono.empty().then()); | ||
Mono<Void> result = filter.filter(exchange, chain); | ||
|
||
StepVerifier.create(result) | ||
.expectNextCount(0) | ||
.expectComplete() | ||
.verify(); | ||
|
||
verify(serverRedirectStrategy, never()).sendRedirect(eq(exchange), | ||
eq(URI.create("/console"))); | ||
verify(chain).filter(eq(exchange)); | ||
} | ||
|
||
@Test | ||
void shouldNotRedirectWhenNotHomePage() { | ||
WebFilterChain chain = mock(WebFilterChain.class); | ||
|
||
MockServerHttpRequest request = MockServerHttpRequest.get("/test").build(); | ||
MockServerWebExchange exchange = MockServerWebExchange.from(request); | ||
when(chain.filter(any())).thenReturn(Mono.empty().then()); | ||
Mono<Void> result = filter.filter(exchange, chain); | ||
|
||
StepVerifier.create(result) | ||
.expectNextCount(0) | ||
.expectComplete() | ||
.verify(); | ||
|
||
verify(serverRedirectStrategy, never()).sendRedirect(eq(exchange), | ||
eq(URI.create("/console"))); | ||
verify(chain).filter(eq(exchange)); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters