From c7e05ddf737bff55edde35e77278c7246c73084b Mon Sep 17 00:00:00 2001 From: Daniel Garnier-Moiroux Date: Tue, 25 Jun 2024 16:13:29 +0200 Subject: [PATCH] Inline CSS for default login and logout page - Remove the dependency on Bootstrap CSS. Results in faster load times, no failures in air-gapped or offline scenarios, and no dependency on an external CDN that may go away some day. --- .../DefaultLoginPageConfigurerTests.java | 241 +++++++++++++----- .../FormLoginBeanDefinitionParserTests.java | 189 +++++++++++--- etc/checkstyle/checkstyle-suppressions.xml | 3 + .../ui/DefaultLoginPageGeneratingFilter.java | 28 +- .../ui/DefaultLogoutPageGeneratingFilter.java | 19 +- .../ui/LoginPageGeneratingWebFilter.java | 27 +- .../ui/LogoutPageGeneratingWebFilter.java | 16 +- .../security/web/util/CssUtils.java | 165 ++++++++++++ ...efaultLogoutPageGeneratingFilterTests.java | 15 +- 9 files changed, 536 insertions(+), 167 deletions(-) create mode 100644 web/src/main/java/org/springframework/security/web/util/CssUtils.java diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurerTests.java index 3b44f86ed83..0f2b64e7241 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -67,6 +67,141 @@ @ExtendWith(SpringTestContextExtension.class) public class DefaultLoginPageConfigurerTests { + //@formatter:off + public static final String EXPECTED_HTML_HEAD = " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " Please sign in\n" + + " \n" + + " \n"; + //@formatter:on + public final SpringTestContext spring = new SpringTestContext(this); @Autowired @@ -89,29 +224,21 @@ public void loginPageThenDefaultLoginPageIsRendered() throws Exception { CsrfToken token = (CsrfToken) result.getRequest().getAttribute(CsrfToken.class.getName()); assertThat(result.getResponse().getContentAsString()).isEqualTo("\n" + "\n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + " Please sign in\n" - + " \n" - + " \n" - + " \n" + + EXPECTED_HTML_HEAD + " \n" - + "
\n" - + "
\n" - + " \n" + + "
\n" + + " \n" + + "

Please sign in

\n" + "

\n" - + " \n" - + " \n" + + " \n" + + " \n" + "

\n" + "

\n" - + " \n" - + " \n" + + " \n" + + " \n" + "

\n" + "\n" - + " \n" + + " \n" + " \n" + "
\n" + ""); @@ -138,28 +265,20 @@ public void loginPageWhenErrorThenDefaultLoginPageWithError() throws Exception { CsrfToken token = (CsrfToken) result.getRequest().getAttribute(CsrfToken.class.getName()); assertThat(result.getResponse().getContentAsString()).isEqualTo("\n" + "\n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + " Please sign in\n" - + " \n" - + " \n" - + " \n" + + EXPECTED_HTML_HEAD + " \n" - + "
\n" - + "
\n" - + " \n" + + "
\n" + + " \n" + + "

Please sign in

\n" + "
Bad credentials

\n" - + " \n" - + " \n" + + " \n" + + " \n" + "

\n" + "

\n" - + " \n" - + " \n" + + " \n" + + " \n" + "

\n" + "\n" - + " \n" + + " \n" + " \n" + "
\n" + ""); @@ -190,29 +309,21 @@ public void loginPageWhenLoggedOutThenDefaultLoginPageWithLogoutMessage() throws CsrfToken token = (CsrfToken) result.getRequest().getAttribute(CsrfToken.class.getName()); assertThat(result.getResponse().getContentAsString()).isEqualTo("\n" + "\n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + " Please sign in\n" - + " \n" - + " \n" - + " \n" + + EXPECTED_HTML_HEAD + " \n" - + "
\n" - + "
\n" - + " \n" + + "
\n" + + " \n" + + "

Please sign in

\n" + "
You have been signed out

\n" - + " \n" - + " \n" + + " \n" + + " \n" + "

\n" + "

\n" - + " \n" - + " \n" + + " \n" + + " \n" + "

\n" + "\n" - + " \n" + + " \n" + " \n" + "
\n" + ""); @@ -243,30 +354,22 @@ public void loginPageWhenRememberConfigureThenDefaultLoginPageWithRememberMeChec CsrfToken token = (CsrfToken) result.getRequest().getAttribute(CsrfToken.class.getName()); assertThat(result.getResponse().getContentAsString()).isEqualTo("\n" + "\n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + " Please sign in\n" - + " \n" - + " \n" - + " \n" + + EXPECTED_HTML_HEAD + " \n" - + "
\n" - + "
\n" - + " \n" + + "
\n" + + " \n" + + "

Please sign in

\n" + "

\n" - + " \n" - + " \n" + + " \n" + + " \n" + "

\n" + "

\n" - + " \n" - + " \n" + + " \n" + + " \n" + "

\n" + "

Remember me on this computer.

\n" + "\n" - + " \n" + + " \n" + " \n" + "
\n" + ""); diff --git a/config/src/test/java/org/springframework/security/config/http/FormLoginBeanDefinitionParserTests.java b/config/src/test/java/org/springframework/security/config/http/FormLoginBeanDefinitionParserTests.java index 86a1c351acb..1d16c26571c 100644 --- a/config/src/test/java/org/springframework/security/config/http/FormLoginBeanDefinitionParserTests.java +++ b/config/src/test/java/org/springframework/security/config/http/FormLoginBeanDefinitionParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -45,6 +45,141 @@ public class FormLoginBeanDefinitionParserTests { private static final String CONFIG_LOCATION_PREFIX = "classpath:org/springframework/security/config/http/FormLoginBeanDefinitionParserTests"; + //@formatter:off + public static final String EXPECTED_HTML_HEAD = " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " Please sign in\n" + + " \n" + + " \n"; + //@formatter:on + public final SpringTestContext spring = new SpringTestContext(this); @Autowired @@ -56,28 +191,20 @@ public void getLoginWhenAutoConfigThenShowsDefaultLoginPage() throws Exception { // @formatter:off String expectedContent = "\n" + "\n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + " Please sign in\n" - + " \n" - + " \n" - + " \n" + + EXPECTED_HTML_HEAD + " \n" - + "
\n" - + "
\n" - + " \n" + + "
\n" + + " \n" + + "

Please sign in

\n" + "

\n" - + " \n" - + " \n" + + " \n" + + " \n" + "

\n" + "

\n" - + " \n" - + " \n" + + " \n" + + " \n" + "

\n" - + " \n" + + " \n" + " \n" + "
\n" + ""; @@ -97,28 +224,20 @@ public void getLoginWhenConfiguredWithCustomAttributesThenLoginPageReflects() th // @formatter:off String expectedContent = "\n" + "\n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + " Please sign in\n" - + " \n" - + " \n" - + " \n" + + EXPECTED_HTML_HEAD + " \n" - + "
\n" - + "
\n" - + " \n" + + "
\n" + + " \n" + + "

Please sign in

\n" + "

\n" - + " \n" - + " \n" + + " \n" + + " \n" + "

\n" + "

\n" - + " \n" - + " \n" + + " \n" + + " \n" + "

\n" - + " \n" + + " \n" + " \n" + "
\n" + ""; diff --git a/etc/checkstyle/checkstyle-suppressions.xml b/etc/checkstyle/checkstyle-suppressions.xml index 74b7de0b19b..7233843cfb5 100644 --- a/etc/checkstyle/checkstyle-suppressions.xml +++ b/etc/checkstyle/checkstyle-suppressions.xml @@ -40,4 +40,7 @@ + + + diff --git a/web/src/main/java/org/springframework/security/web/authentication/ui/DefaultLoginPageGeneratingFilter.java b/web/src/main/java/org/springframework/security/web/authentication/ui/DefaultLoginPageGeneratingFilter.java index c727dbb6238..89b44ecf62c 100644 --- a/web/src/main/java/org/springframework/security/web/authentication/ui/DefaultLoginPageGeneratingFilter.java +++ b/web/src/main/java/org/springframework/security/web/authentication/ui/DefaultLoginPageGeneratingFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,6 +34,7 @@ import org.springframework.security.web.WebAttributes; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices; +import org.springframework.security.web.util.CssUtils; import org.springframework.util.Assert; import org.springframework.util.StringUtils; import org.springframework.web.filter.GenericFilterBean; @@ -201,33 +202,30 @@ private String generateLoginPageHtml(HttpServletRequest request, boolean loginEr sb.append(" \n"); sb.append(" \n"); sb.append(" Please sign in\n"); - sb.append(" \n"); - sb.append(" \n"); + sb.append(CssUtils.getCssStyleBlock().indent(4)); sb.append(" \n"); sb.append(" \n"); - sb.append("
\n"); + sb.append("
\n"); if (this.formLoginEnabled) { - sb.append("
\n"); - sb.append(" \n"); + sb.append("

Please sign in

\n"); sb.append(createError(loginError, errorMsg) + createLogoutSuccess(logoutSuccess) + "

\n"); - sb.append(" \n"); + sb.append(" \n"); sb.append(" \n"); + + "\" placeholder=\"Username\" required autofocus>\n"); sb.append("

\n"); sb.append("

\n"); - sb.append(" \n"); + sb.append(" \n"); sb.append(" \n"); + + "\" placeholder=\"Password\" required>\n"); sb.append("

\n"); sb.append(createRememberMe(this.rememberMeParameter) + renderHiddenInputs(request)); - sb.append(" \n"); + sb.append(" \n"); sb.append("
\n"); } if (this.oauth2LoginEnabled) { - sb.append(""); + sb.append("

Login with OAuth 2.0

"); sb.append(createError(loginError, errorMsg)); sb.append(createLogoutSuccess(logoutSuccess)); sb.append("\n"); @@ -244,7 +242,7 @@ private String generateLoginPageHtml(HttpServletRequest request, boolean loginEr sb.append("
\n"); } if (this.saml2LoginEnabled) { - sb.append(""); + sb.append("

Login with SAML 2.0

"); sb.append(createError(loginError, errorMsg)); sb.append(createLogoutSuccess(logoutSuccess)); sb.append("\n"); diff --git a/web/src/main/java/org/springframework/security/web/authentication/ui/DefaultLogoutPageGeneratingFilter.java b/web/src/main/java/org/springframework/security/web/authentication/ui/DefaultLogoutPageGeneratingFilter.java index 29f4b3d5d5f..9c38b8cb5e5 100644 --- a/web/src/main/java/org/springframework/security/web/authentication/ui/DefaultLogoutPageGeneratingFilter.java +++ b/web/src/main/java/org/springframework/security/web/authentication/ui/DefaultLogoutPageGeneratingFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,6 +27,7 @@ import jakarta.servlet.http.HttpServletResponse; import org.springframework.core.log.LogMessage; +import org.springframework.security.web.util.CssUtils; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.util.Assert; @@ -69,19 +70,15 @@ private void renderLogout(HttpServletRequest request, HttpServletResponse respon sb.append(" \n"); sb.append(" \n"); sb.append(" Confirm Log Out?\n"); - sb.append(" \n"); - sb.append(" \n"); + sb.append(CssUtils.getCssStyleBlock().indent(4)); sb.append(" \n"); sb.append(" \n"); - sb.append("
\n"); - sb.append("
\n"); + sb.append(" \n"); - sb.append(" \n"); - sb.append(renderHiddenInputs(request) - + " \n"); + sb.append("

Are you sure you want to log out?

\n"); + sb.append(renderHiddenInputs(request)); + sb.append(" \n"); sb.append(" \n"); sb.append("
\n"); sb.append(" \n"); diff --git a/web/src/main/java/org/springframework/security/web/server/ui/LoginPageGeneratingWebFilter.java b/web/src/main/java/org/springframework/security/web/server/ui/LoginPageGeneratingWebFilter.java index 2b85e897324..3065796ea4e 100644 --- a/web/src/main/java/org/springframework/security/web/server/ui/LoginPageGeneratingWebFilter.java +++ b/web/src/main/java/org/springframework/security/web/server/ui/LoginPageGeneratingWebFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,6 +31,7 @@ import org.springframework.security.web.server.csrf.CsrfToken; import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher; import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers; +import org.springframework.security.web.util.CssUtils; import org.springframework.util.Assert; import org.springframework.util.MultiValueMap; import org.springframework.web.server.ServerWebExchange; @@ -97,14 +98,10 @@ private byte[] createPage(ServerWebExchange exchange, String csrfTokenHtmlInput) page.append(" \n"); page.append(" \n"); page.append(" Please sign in\n"); - page.append(" \n"); - page.append(" \n"); + page.append(CssUtils.getCssStyleBlock().indent(4)); page.append(" \n"); page.append(" \n"); - page.append("
\n"); + page.append("
\n"); page.append(formLogin(queryParams, contextPath, csrfTokenHtmlInput)); page.append(oauth2LoginLinks(queryParams, contextPath, this.oauth2AuthenticationUrlToClientName)); page.append("
\n"); @@ -120,21 +117,21 @@ private String formLogin(MultiValueMap queryParams, String conte boolean isError = queryParams.containsKey("error"); boolean isLogoutSuccess = queryParams.containsKey("logout"); StringBuilder page = new StringBuilder(); - page.append("
\n"); - page.append(" \n"); + page.append(" \n"); + page.append("

Please sign in

\n"); page.append(createError(isError)); page.append(createLogoutSuccess(isLogoutSuccess)); page.append("

\n"); - page.append(" \n"); + page.append(" \n"); page.append(" \n"); + + "placeholder=\"Username\" required autofocus>\n"); page.append("

\n" + "

\n"); - page.append(" \n"); + page.append(" \n"); page.append(" \n"); + + "placeholder=\"Password\" required>\n"); page.append("

\n"); page.append(csrfTokenHtmlInput); - page.append(" \n"); + page.append(" \n"); page.append(" \n"); return page.toString(); } @@ -146,7 +143,7 @@ private static String oauth2LoginLinks(MultiValueMap queryParams } boolean isError = queryParams.containsKey("error"); StringBuilder sb = new StringBuilder(); - sb.append("
"); + sb.append("

Login with OAuth 2.0

"); sb.append(createError(isError)); sb.append("
\n"); for (Map.Entry clientAuthenticationUrlToClientName : oauth2AuthenticationUrlToClientName diff --git a/web/src/main/java/org/springframework/security/web/server/ui/LogoutPageGeneratingWebFilter.java b/web/src/main/java/org/springframework/security/web/server/ui/LogoutPageGeneratingWebFilter.java index b8ad98cd29d..a691e2fdcbb 100644 --- a/web/src/main/java/org/springframework/security/web/server/ui/LogoutPageGeneratingWebFilter.java +++ b/web/src/main/java/org/springframework/security/web/server/ui/LogoutPageGeneratingWebFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,6 +29,7 @@ import org.springframework.security.web.server.csrf.CsrfToken; import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher; import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers; +import org.springframework.security.web.util.CssUtils; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebFilter; import org.springframework.web.server.WebFilterChain; @@ -78,17 +79,14 @@ private static byte[] createPage(String csrfTokenHtmlInput, String contextPath) page.append(" \n"); page.append(" \n"); page.append(" Confirm Log Out?\n"); - page.append(" \n"); - page.append(" \n"); + page.append(CssUtils.getCssStyleBlock().indent(4)); page.append(" \n"); page.append(" \n"); - page.append("
\n"); - page.append("
\n"); - page.append(" \n"); + page.append("
\n"); + page.append(" \n"); + page.append("

Are you sure you want to log out?

\n"); page.append(csrfTokenHtmlInput); - page.append(" \n"); + page.append(" \n"); page.append(" \n"); page.append("
\n"); page.append(" \n"); diff --git a/web/src/main/java/org/springframework/security/web/util/CssUtils.java b/web/src/main/java/org/springframework/security/web/util/CssUtils.java new file mode 100644 index 00000000000..9503de56944 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/util/CssUtils.java @@ -0,0 +1,165 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.util; + +/** + * Provides common CSS classes and styles, valid across Servlet and Reactive stacks. + * + * @author Daniel Garnier-Moiroux + * @since 6.4 + */ +public final class CssUtils { + + private CssUtils() { + } + + /** + * Generates the CSS used by all Spring Security default pages, such as login or + * logout pages. + * @return the {@code + """; + } + +} diff --git a/web/src/test/java/org/springframework/security/web/authentication/ui/DefaultLogoutPageGeneratingFilterTests.java b/web/src/test/java/org/springframework/security/web/authentication/ui/DefaultLogoutPageGeneratingFilterTests.java index 00885411a2e..796ef2f0730 100644 --- a/web/src/test/java/org/springframework/security/web/authentication/ui/DefaultLogoutPageGeneratingFilterTests.java +++ b/web/src/test/java/org/springframework/security/web/authentication/ui/DefaultLogoutPageGeneratingFilterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,18 +39,7 @@ public class DefaultLogoutPageGeneratingFilterTests { public void doFilterWhenNoHiddenInputsThenPageRendered() throws Exception { MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new Object()).addFilter(this.filter).build(); mockMvc.perform(get("/logout")) - .andExpect(content().string("\n" + "\n" + " \n" - + " \n" - + " \n" - + " \n" + " \n" - + " Confirm Log Out?\n" - + " \n" - + " \n" - + " \n" + " \n" + "
\n" - + "
\n" - + " \n" - + " \n" - + " \n" + "
\n" + " \n" + "")) + .andExpect(content().string(containsString("Are you sure you want to log out?"))) .andExpect(content().contentType("text/html;charset=UTF-8")); }