From 0ad565f35c7caf9ca18e712b1bf612a6959e203e Mon Sep 17 00:00:00 2001 From: John Niang Date: Sat, 12 Oct 2024 12:11:09 +0800 Subject: [PATCH] Do not cache template result for pre-auth pages (#6829) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #### What type of PR is this? /kind bug /area core /milestone 2.20.x #### What this PR does / why we need it: This PR prevents caching from cache plugin for pre-auth pages and logout page. #### Which issue(s) this PR fixes: Fixes #6826 #### Special notes for your reviewer: 1. Install `Page Cache Plugin` from . 2. Open a private browser window 3. Access login page twice 4. Try to login 5. See the result #### Does this PR introduce a user-facing change? ```release-note 解决因缓存插件缓存登录页面导致无法登录的问题 ``` --- .../run/halo/app/theme/router/ModelConst.java | 6 +++ .../run/halo/app/infra/utils/HaloUtils.java | 14 +++++ .../security/LogoutSecurityConfigurer.java | 5 ++ .../PreAuthEmailPasswordResetEndpoint.java | 5 +- .../preauth/PreAuthLoginEndpoint.java | 3 +- .../preauth/PreAuthSignUpEndpoint.java | 5 +- .../preauth/PreAuthTwoFactorEndpoint.java | 2 + .../security/preauth/SystemSetupEndpoint.java | 2 + .../run/halo/app/theme/HaloViewResolver.java | 5 +- .../halo/app/infra/utils/HaloUtilsTest.java | 25 +++++++++ .../halo/app/theme/ThemeIntegrationTest.java | 53 ++++++++++++++++++- 11 files changed, 120 insertions(+), 5 deletions(-) create mode 100644 application/src/test/java/run/halo/app/infra/utils/HaloUtilsTest.java diff --git a/api/src/main/java/run/halo/app/theme/router/ModelConst.java b/api/src/main/java/run/halo/app/theme/router/ModelConst.java index 86af7e07e..e6ac84bba 100644 --- a/api/src/main/java/run/halo/app/theme/router/ModelConst.java +++ b/api/src/main/java/run/halo/app/theme/router/ModelConst.java @@ -10,5 +10,11 @@ public enum ModelConst { ; public static final String TEMPLATE_ID = "_templateId"; public static final String POWERED_BY_HALO_TEMPLATE_ENGINE = "poweredByHaloTemplateEngine"; + + /** + * This key is used to prevent caching from cache plugins. + */ + public static final String NO_CACHE = "HALO_TEMPLATE_ENGINE.NO_CACHE"; + public static final Integer DEFAULT_PAGE_SIZE = 10; } diff --git a/application/src/main/java/run/halo/app/infra/utils/HaloUtils.java b/application/src/main/java/run/halo/app/infra/utils/HaloUtils.java index 190536beb..170b1cdbe 100644 --- a/application/src/main/java/run/halo/app/infra/utils/HaloUtils.java +++ b/application/src/main/java/run/halo/app/infra/utils/HaloUtils.java @@ -5,6 +5,7 @@ import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.time.Instant; import java.time.ZoneId; +import java.util.function.UnaryOperator; import lombok.experimental.UtilityClass; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; @@ -13,6 +14,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.util.Assert; import org.springframework.util.StreamUtils; import org.springframework.web.reactive.function.server.ServerRequest; +import run.halo.app.theme.router.ModelConst; /** * Halo utilities. @@ -81,4 +83,16 @@ public class HaloUtils { Assert.notNull(instant, "Instant must not be null"); return String.valueOf(instant.atZone(ZoneId.systemDefault()).getYear()); } + + /** + * Mark the response as no cache. + * + * @return the server request operator + */ + public static UnaryOperator noCache() { + return request -> { + request.exchange().getAttributes().put(ModelConst.NO_CACHE, true); + return request; + }; + } } diff --git a/application/src/main/java/run/halo/app/security/LogoutSecurityConfigurer.java b/application/src/main/java/run/halo/app/security/LogoutSecurityConfigurer.java index b6bf4ec12..7406322b3 100644 --- a/application/src/main/java/run/halo/app/security/LogoutSecurityConfigurer.java +++ b/application/src/main/java/run/halo/app/security/LogoutSecurityConfigurer.java @@ -26,6 +26,7 @@ import reactor.core.publisher.Mono; import run.halo.app.core.user.service.UserService; import run.halo.app.security.authentication.SecurityConfigurer; import run.halo.app.security.authentication.rememberme.RememberMeServices; +import run.halo.app.theme.router.ModelConst; @Component @RequiredArgsConstructor @@ -72,6 +73,10 @@ public class LogoutSecurityConfigurer implements SecurityConfigurer { "user", user )); }) + .before(request -> { + request.exchange().getAttributes().put(ModelConst.NO_CACHE, true); + return request; + }) .build(); } diff --git a/application/src/main/java/run/halo/app/security/preauth/PreAuthEmailPasswordResetEndpoint.java b/application/src/main/java/run/halo/app/security/preauth/PreAuthEmailPasswordResetEndpoint.java index 1054fc236..55c40d5d1 100644 --- a/application/src/main/java/run/halo/app/security/preauth/PreAuthEmailPasswordResetEndpoint.java +++ b/application/src/main/java/run/halo/app/security/preauth/PreAuthEmailPasswordResetEndpoint.java @@ -31,6 +31,7 @@ import run.halo.app.core.user.service.EmailPasswordRecoveryService; import run.halo.app.core.user.service.InvalidResetTokenException; import run.halo.app.infra.ValidationUtils; import run.halo.app.infra.actuator.GlobalInfoService; +import run.halo.app.infra.utils.HaloUtils; import run.halo.app.infra.utils.IpAddressUtils; /** @@ -168,7 +169,9 @@ class PreAuthEmailPasswordResetEndpoint { return ServerResponse.status(HttpStatus.TOO_MANY_REQUESTS) .render(SEND_TEMPLATE, model); }); - })) + }) + ) + .before(HaloUtils.noCache()) .build()); } diff --git a/application/src/main/java/run/halo/app/security/preauth/PreAuthLoginEndpoint.java b/application/src/main/java/run/halo/app/security/preauth/PreAuthLoginEndpoint.java index b6c18b81a..f506ca0d8 100644 --- a/application/src/main/java/run/halo/app/security/preauth/PreAuthLoginEndpoint.java +++ b/application/src/main/java/run/halo/app/security/preauth/PreAuthLoginEndpoint.java @@ -17,6 +17,7 @@ import org.springframework.web.server.ServerWebInputException; import reactor.core.publisher.Mono; import run.halo.app.core.extension.AuthProvider; import run.halo.app.infra.actuator.GlobalInfoService; +import run.halo.app.infra.utils.HaloUtils; import run.halo.app.plugin.PluginConst; import run.halo.app.security.AuthProviderService; import run.halo.app.security.HaloServerRequestCache; @@ -50,7 +51,6 @@ class PreAuthLoginEndpoint { RouterFunction preAuthLoginEndpoints() { return RouterFunctions.nest(path("/login"), RouterFunctions.route() .GET("", request -> { - // TODO get redirect URI and cache it var exchange = request.exchange(); var contextPath = exchange.getRequest().getPath().contextPath().value(); var publicKey = cryptoService.readPublicKey() @@ -96,6 +96,7 @@ class PreAuthLoginEndpoint { )) )); }) + .before(HaloUtils.noCache()) .build()); } } diff --git a/application/src/main/java/run/halo/app/security/preauth/PreAuthSignUpEndpoint.java b/application/src/main/java/run/halo/app/security/preauth/PreAuthSignUpEndpoint.java index a43072397..b653250b2 100644 --- a/application/src/main/java/run/halo/app/security/preauth/PreAuthSignUpEndpoint.java +++ b/application/src/main/java/run/halo/app/security/preauth/PreAuthSignUpEndpoint.java @@ -32,6 +32,7 @@ import run.halo.app.infra.exception.DuplicateNameException; import run.halo.app.infra.exception.EmailVerificationFailed; import run.halo.app.infra.exception.RateLimitExceededException; import run.halo.app.infra.exception.RequestBodyValidationException; +import run.halo.app.infra.utils.HaloUtils; import run.halo.app.infra.utils.IpAddressUtils; /** @@ -131,7 +132,9 @@ class PreAuthSignUpEndpoint { ) .onErrorMap(RequestNotPermitted.class, RateLimitExceededException::new); }) - .then(ServerResponse.accepted().build())) + .then(ServerResponse.accepted().build()) + ) + .before(HaloUtils.noCache()) .build()); } diff --git a/application/src/main/java/run/halo/app/security/preauth/PreAuthTwoFactorEndpoint.java b/application/src/main/java/run/halo/app/security/preauth/PreAuthTwoFactorEndpoint.java index 08ac8052b..6f5c75e04 100644 --- a/application/src/main/java/run/halo/app/security/preauth/PreAuthTwoFactorEndpoint.java +++ b/application/src/main/java/run/halo/app/security/preauth/PreAuthTwoFactorEndpoint.java @@ -7,6 +7,7 @@ import org.springframework.web.reactive.function.server.RouterFunction; import org.springframework.web.reactive.function.server.RouterFunctions; import org.springframework.web.reactive.function.server.ServerResponse; import run.halo.app.infra.actuator.GlobalInfoService; +import run.halo.app.infra.utils.HaloUtils; /** * Pre-auth two-factor endpoints. @@ -25,6 +26,7 @@ class PreAuthTwoFactorEndpoint { "globalInfo", globalInfoService.getGlobalInfo() )) ) + .before(HaloUtils.noCache()) .build(); } diff --git a/application/src/main/java/run/halo/app/security/preauth/SystemSetupEndpoint.java b/application/src/main/java/run/halo/app/security/preauth/SystemSetupEndpoint.java index a3bd6572b..5fa81998e 100644 --- a/application/src/main/java/run/halo/app/security/preauth/SystemSetupEndpoint.java +++ b/application/src/main/java/run/halo/app/security/preauth/SystemSetupEndpoint.java @@ -108,6 +108,8 @@ public class SystemSetupEndpoint { .implementation(Void.class) ) ) + .before(HaloUtils.noCache(), builder -> { + }) .build(); } diff --git a/application/src/main/java/run/halo/app/theme/HaloViewResolver.java b/application/src/main/java/run/halo/app/theme/HaloViewResolver.java index 3446240d0..1719cc920 100644 --- a/application/src/main/java/run/halo/app/theme/HaloViewResolver.java +++ b/application/src/main/java/run/halo/app/theme/HaloViewResolver.java @@ -97,7 +97,10 @@ public class HaloViewResolver extends ThymeleafReactiveViewResolver implements I return themeResolver.getTheme(exchange).flatMap(theme -> { // calculate the engine before rendering setTemplateEngine(engineManager.getTemplateEngine(theme)); - exchange.getAttributes().put(ModelConst.POWERED_BY_HALO_TEMPLATE_ENGINE, true); + var noCache = (Boolean) exchange.getAttributes() + .getOrDefault(ModelConst.NO_CACHE, false); + exchange.getAttributes() + .put(ModelConst.POWERED_BY_HALO_TEMPLATE_ENGINE, !noCache); return super.render(model, contentType, exchange) .onErrorMap(TemplateProcessingException.class::isInstance, tee -> { if (tee instanceof TemplateInputException) { diff --git a/application/src/test/java/run/halo/app/infra/utils/HaloUtilsTest.java b/application/src/test/java/run/halo/app/infra/utils/HaloUtilsTest.java new file mode 100644 index 000000000..59a13c37a --- /dev/null +++ b/application/src/test/java/run/halo/app/infra/utils/HaloUtilsTest.java @@ -0,0 +1,25 @@ +package run.halo.app.infra.utils; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.mock.web.reactive.function.server.MockServerRequest; +import org.springframework.mock.web.server.MockServerWebExchange; +import run.halo.app.theme.router.ModelConst; + +class HaloUtilsTest { + + @Test + void checkNoCache() { + var exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/").build()); + var request = MockServerRequest.builder() + .exchange(exchange) + .build(); + var applied = HaloUtils.noCache().apply(request); + assertEquals(applied, request); + assertTrue(() -> exchange.getRequiredAttribute(ModelConst.NO_CACHE)); + } + +} \ No newline at end of file diff --git a/application/src/test/java/run/halo/app/theme/ThemeIntegrationTest.java b/application/src/test/java/run/halo/app/theme/ThemeIntegrationTest.java index 32f90d23c..95a246fcb 100644 --- a/application/src/test/java/run/halo/app/theme/ThemeIntegrationTest.java +++ b/application/src/test/java/run/halo/app/theme/ThemeIntegrationTest.java @@ -1,6 +1,10 @@ package run.halo.app.theme; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.when; +import static org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers.pathMatchers; import java.util.LinkedHashSet; import java.util.List; @@ -13,7 +17,9 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; +import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher.MatchResult; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.reactive.server.WebTestClient; @@ -26,6 +32,9 @@ import run.halo.app.core.extension.MenuItem; import run.halo.app.extension.ExtensionClient; import run.halo.app.extension.Metadata; import run.halo.app.infra.InitializationStateGetter; +import run.halo.app.infra.utils.HaloUtils; +import run.halo.app.security.AfterSecurityWebFilter; +import run.halo.app.theme.router.ModelConst; @SpringBootTest @Import(ThemeIntegrationTest.TestConfig.class) @@ -78,11 +87,53 @@ public class ThemeIntegrationTest { .build(); } + @Bean + RouterFunction noCacheRoute() { + return RouterFunctions.route() + .GET( + "/should-not-cache", + request -> ServerResponse.ok().render("no-template-exists") + ) + .before(HaloUtils.noCache()) + .build(); + } + + @Bean + AfterSecurityWebFilter poweredByHaloTemplateEngineCheckFilter() { + var matcher = pathMatchers(HttpMethod.GET, "/should-not-cache"); + return (exchange, chain) -> chain.filter(exchange) + .flatMap(v -> matcher.matches(exchange) + .filter(MatchResult::isMatch) + .switchIfEmpty(Mono.fromRunnable(() -> { + assertNull(exchange.getAttribute(ModelConst.NO_CACHE)); + assertTrue(exchange.getRequiredAttribute( + ModelConst.POWERED_BY_HALO_TEMPLATE_ENGINE) + ); + }).then(Mono.empty())) + .doOnNext(m -> { + assertTrue(exchange.getRequiredAttribute(ModelConst.NO_CACHE)); + assertFalse(exchange.getRequiredAttribute( + ModelConst.POWERED_BY_HALO_TEMPLATE_ENGINE) + ); + }) + ) + .then(); + } + } @Test void shouldRespondNotFoundIfNoTemplateFound() { - webClient.get().uri("/no-template-exists") + webClient.get() + .uri("/no-template-exists") + .accept(MediaType.TEXT_HTML) + .exchange() + .expectStatus().isNotFound() + .expectBody(String.class) + .value(Matchers.containsString("Template no-template-exists was not found")); + + webClient.get() + .uri("/should-not-cache") .accept(MediaType.TEXT_HTML) .exchange() .expectStatus().isNotFound()