Do not cache template result for pre-auth pages (#6829)

#### 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 <https://www.halo.run/store/apps/app-BaamQ>.
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
解决因缓存插件缓存登录页面导致无法登录的问题
```
pull/6836/head
John Niang 2024-10-12 12:11:09 +08:00 committed by GitHub
parent 98a131309c
commit 0ad565f35c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 120 additions and 5 deletions

View File

@ -10,5 +10,11 @@ public enum ModelConst {
; ;
public static final String TEMPLATE_ID = "_templateId"; public static final String TEMPLATE_ID = "_templateId";
public static final String POWERED_BY_HALO_TEMPLATE_ENGINE = "poweredByHaloTemplateEngine"; 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; public static final Integer DEFAULT_PAGE_SIZE = 10;
} }

View File

@ -5,6 +5,7 @@ import java.io.InputStream;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.time.Instant; import java.time.Instant;
import java.time.ZoneId; import java.time.ZoneId;
import java.util.function.UnaryOperator;
import lombok.experimental.UtilityClass; import lombok.experimental.UtilityClass;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
@ -13,6 +14,7 @@ import org.springframework.http.HttpHeaders;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.StreamUtils; import org.springframework.util.StreamUtils;
import org.springframework.web.reactive.function.server.ServerRequest; import org.springframework.web.reactive.function.server.ServerRequest;
import run.halo.app.theme.router.ModelConst;
/** /**
* Halo utilities. * Halo utilities.
@ -81,4 +83,16 @@ public class HaloUtils {
Assert.notNull(instant, "Instant must not be null"); Assert.notNull(instant, "Instant must not be null");
return String.valueOf(instant.atZone(ZoneId.systemDefault()).getYear()); return String.valueOf(instant.atZone(ZoneId.systemDefault()).getYear());
} }
/**
* Mark the response as no cache.
*
* @return the server request operator
*/
public static UnaryOperator<ServerRequest> noCache() {
return request -> {
request.exchange().getAttributes().put(ModelConst.NO_CACHE, true);
return request;
};
}
} }

View File

@ -26,6 +26,7 @@ import reactor.core.publisher.Mono;
import run.halo.app.core.user.service.UserService; import run.halo.app.core.user.service.UserService;
import run.halo.app.security.authentication.SecurityConfigurer; import run.halo.app.security.authentication.SecurityConfigurer;
import run.halo.app.security.authentication.rememberme.RememberMeServices; import run.halo.app.security.authentication.rememberme.RememberMeServices;
import run.halo.app.theme.router.ModelConst;
@Component @Component
@RequiredArgsConstructor @RequiredArgsConstructor
@ -72,6 +73,10 @@ public class LogoutSecurityConfigurer implements SecurityConfigurer {
"user", user "user", user
)); ));
}) })
.before(request -> {
request.exchange().getAttributes().put(ModelConst.NO_CACHE, true);
return request;
})
.build(); .build();
} }

View File

@ -31,6 +31,7 @@ import run.halo.app.core.user.service.EmailPasswordRecoveryService;
import run.halo.app.core.user.service.InvalidResetTokenException; import run.halo.app.core.user.service.InvalidResetTokenException;
import run.halo.app.infra.ValidationUtils; import run.halo.app.infra.ValidationUtils;
import run.halo.app.infra.actuator.GlobalInfoService; import run.halo.app.infra.actuator.GlobalInfoService;
import run.halo.app.infra.utils.HaloUtils;
import run.halo.app.infra.utils.IpAddressUtils; import run.halo.app.infra.utils.IpAddressUtils;
/** /**
@ -168,7 +169,9 @@ class PreAuthEmailPasswordResetEndpoint {
return ServerResponse.status(HttpStatus.TOO_MANY_REQUESTS) return ServerResponse.status(HttpStatus.TOO_MANY_REQUESTS)
.render(SEND_TEMPLATE, model); .render(SEND_TEMPLATE, model);
}); });
})) })
)
.before(HaloUtils.noCache())
.build()); .build());
} }

View File

@ -17,6 +17,7 @@ import org.springframework.web.server.ServerWebInputException;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import run.halo.app.core.extension.AuthProvider; import run.halo.app.core.extension.AuthProvider;
import run.halo.app.infra.actuator.GlobalInfoService; import run.halo.app.infra.actuator.GlobalInfoService;
import run.halo.app.infra.utils.HaloUtils;
import run.halo.app.plugin.PluginConst; import run.halo.app.plugin.PluginConst;
import run.halo.app.security.AuthProviderService; import run.halo.app.security.AuthProviderService;
import run.halo.app.security.HaloServerRequestCache; import run.halo.app.security.HaloServerRequestCache;
@ -50,7 +51,6 @@ class PreAuthLoginEndpoint {
RouterFunction<ServerResponse> preAuthLoginEndpoints() { RouterFunction<ServerResponse> preAuthLoginEndpoints() {
return RouterFunctions.nest(path("/login"), RouterFunctions.route() return RouterFunctions.nest(path("/login"), RouterFunctions.route()
.GET("", request -> { .GET("", request -> {
// TODO get redirect URI and cache it
var exchange = request.exchange(); var exchange = request.exchange();
var contextPath = exchange.getRequest().getPath().contextPath().value(); var contextPath = exchange.getRequest().getPath().contextPath().value();
var publicKey = cryptoService.readPublicKey() var publicKey = cryptoService.readPublicKey()
@ -96,6 +96,7 @@ class PreAuthLoginEndpoint {
)) ))
)); ));
}) })
.before(HaloUtils.noCache())
.build()); .build());
} }
} }

View File

@ -32,6 +32,7 @@ import run.halo.app.infra.exception.DuplicateNameException;
import run.halo.app.infra.exception.EmailVerificationFailed; import run.halo.app.infra.exception.EmailVerificationFailed;
import run.halo.app.infra.exception.RateLimitExceededException; import run.halo.app.infra.exception.RateLimitExceededException;
import run.halo.app.infra.exception.RequestBodyValidationException; import run.halo.app.infra.exception.RequestBodyValidationException;
import run.halo.app.infra.utils.HaloUtils;
import run.halo.app.infra.utils.IpAddressUtils; import run.halo.app.infra.utils.IpAddressUtils;
/** /**
@ -131,7 +132,9 @@ class PreAuthSignUpEndpoint {
) )
.onErrorMap(RequestNotPermitted.class, RateLimitExceededException::new); .onErrorMap(RequestNotPermitted.class, RateLimitExceededException::new);
}) })
.then(ServerResponse.accepted().build())) .then(ServerResponse.accepted().build())
)
.before(HaloUtils.noCache())
.build()); .build());
} }

View File

@ -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.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerResponse; import org.springframework.web.reactive.function.server.ServerResponse;
import run.halo.app.infra.actuator.GlobalInfoService; import run.halo.app.infra.actuator.GlobalInfoService;
import run.halo.app.infra.utils.HaloUtils;
/** /**
* Pre-auth two-factor endpoints. * Pre-auth two-factor endpoints.
@ -25,6 +26,7 @@ class PreAuthTwoFactorEndpoint {
"globalInfo", globalInfoService.getGlobalInfo() "globalInfo", globalInfoService.getGlobalInfo()
)) ))
) )
.before(HaloUtils.noCache())
.build(); .build();
} }

View File

@ -108,6 +108,8 @@ public class SystemSetupEndpoint {
.implementation(Void.class) .implementation(Void.class)
) )
) )
.before(HaloUtils.noCache(), builder -> {
})
.build(); .build();
} }

View File

@ -97,7 +97,10 @@ public class HaloViewResolver extends ThymeleafReactiveViewResolver implements I
return themeResolver.getTheme(exchange).flatMap(theme -> { return themeResolver.getTheme(exchange).flatMap(theme -> {
// calculate the engine before rendering // calculate the engine before rendering
setTemplateEngine(engineManager.getTemplateEngine(theme)); 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) return super.render(model, contentType, exchange)
.onErrorMap(TemplateProcessingException.class::isInstance, tee -> { .onErrorMap(TemplateProcessingException.class::isInstance, tee -> {
if (tee instanceof TemplateInputException) { if (tee instanceof TemplateInputException) {

View File

@ -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));
}
}

View File

@ -1,6 +1,10 @@
package run.halo.app.theme; 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.mockito.Mockito.when;
import static org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers.pathMatchers;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.List; 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.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Import;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType; 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.annotation.DirtiesContext;
import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.reactive.server.WebTestClient; 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.ExtensionClient;
import run.halo.app.extension.Metadata; import run.halo.app.extension.Metadata;
import run.halo.app.infra.InitializationStateGetter; 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 @SpringBootTest
@Import(ThemeIntegrationTest.TestConfig.class) @Import(ThemeIntegrationTest.TestConfig.class)
@ -78,11 +87,53 @@ public class ThemeIntegrationTest {
.build(); .build();
} }
@Bean
RouterFunction<ServerResponse> 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 @Test
void shouldRespondNotFoundIfNoTemplateFound() { 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) .accept(MediaType.TEXT_HTML)
.exchange() .exchange()
.expectStatus().isNotFound() .expectStatus().isNotFound()