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

View File

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

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.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());
}

View File

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

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.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());
}

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.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();
}

View File

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

View File

@ -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) {

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;
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<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
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()