From 2a5a32b6038355ac2669f8a73c9b3413176a17f9 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 4 Aug 2015 11:33:16 +0100 Subject: [PATCH] =?UTF-8?q?Add=20auto-configuration=20for=20H2=E2=80=99s?= =?UTF-8?q?=20web=20console?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three conditions must be met for the console to be enabled: - H2 is on the classpath - The application is a web application - spring.h2.console.enabled is set to true If spring-boot-devtools is on the classpath, spring.h2.console.enabled will be set to true automatically. Without the dev tools, the enabled property will have to be set to true in application.properties. By default, the console is available at /h2-console. This can be configured via the spring.h2.console.path property. The value of this property must begin with a '/'. When Spring Security is on the classpath the console will be secured based on the user's security.* configuration. When the console is secured, CSRF protection is disabled and frame options is set to SAMEORIGIN for its path. Both settings are required in order for the console to function. Closes gh-766 --- spring-boot-autoconfigure/pom.xml | 18 ++- .../h2/H2ConsoleAutoConfiguration.java | 106 +++++++++++++++++ .../autoconfigure/h2/H2ConsoleProperties.java | 61 ++++++++++ .../main/resources/META-INF/spring.factories | 1 + ...soleAutoConfigurationIntegrationTests.java | 93 +++++++++++++++ .../h2/H2ConsoleAutoConfigurationTests.java | 112 ++++++++++++++++++ ...DevToolsPropertyDefaultsPostProcessor.java | 1 + .../appendix-application-properties.adoc | 4 + .../main/asciidoc/spring-boot-features.adoc | 39 ++++++ 9 files changed, 431 insertions(+), 4 deletions(-) create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/h2/H2ConsoleAutoConfiguration.java create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/h2/H2ConsoleProperties.java create mode 100644 spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/h2/H2ConsoleAutoConfigurationIntegrationTests.java create mode 100644 spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/h2/H2ConsoleAutoConfigurationTests.java diff --git a/spring-boot-autoconfigure/pom.xml b/spring-boot-autoconfigure/pom.xml index bedbc8262dd..a68effcaf09 100644 --- a/spring-boot-autoconfigure/pom.xml +++ b/spring-boot-autoconfigure/pom.xml @@ -70,6 +70,11 @@ hazelcast-spring true + + com.h2database + h2 + true + com.samskivert jmustache @@ -549,8 +554,9 @@ test - org.yaml - snakeyaml + org.slf4j + slf4j-jdk14 + test org.springframework @@ -558,9 +564,13 @@ test - org.slf4j - slf4j-jdk14 + org.springframework.security + spring-security-test test + + org.yaml + snakeyaml + diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/h2/H2ConsoleAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/h2/H2ConsoleAutoConfiguration.java new file mode 100644 index 00000000000..944c9d2e652 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/h2/H2ConsoleAutoConfiguration.java @@ -0,0 +1,106 @@ +/* + * Copyright 2012-2015 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 + * + * http://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.boot.autoconfigure.h2; + +import org.h2.server.web.WebServlet; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.autoconfigure.security.SecurityAuthorizeMode; +import org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration; +import org.springframework.boot.autoconfigure.security.SecurityProperties; +import org.springframework.boot.context.embedded.ServletRegistrationBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for H2's web console + * + * @author Andy Wilkinson + * @since 1.3.0 + */ +@Configuration +@ConditionalOnWebApplication +@ConditionalOnClass(WebServlet.class) +@ConditionalOnProperty(prefix = "spring.h2.console", name = "enabled", havingValue = "true", matchIfMissing = false) +@EnableConfigurationProperties(H2ConsoleProperties.class) +@AutoConfigureAfter(SecurityAutoConfiguration.class) +public class H2ConsoleAutoConfiguration { + + @Autowired + private H2ConsoleProperties properties; + + @Bean + public ServletRegistrationBean h2Console() { + return new ServletRegistrationBean(new WebServlet(), this.properties.getPath() + .endsWith("/") ? this.properties.getPath() + "*" + : this.properties.getPath() + "/*"); + } + + @Configuration + @ConditionalOnClass(WebSecurityConfigurerAdapter.class) + @ConditionalOnBean(ObjectPostProcessor.class) + @ConditionalOnProperty(prefix = "security.basic", name = "enabled", matchIfMissing = true) + static class H2ConsoleSecurityConfiguration { + + @Bean + public WebSecurityConfigurerAdapter h2ConsoleSecurityConfigurer() { + return new H2ConsoleSecurityConfigurer(); + } + + @Order(SecurityProperties.BASIC_AUTH_ORDER - 10) + private static class H2ConsoleSecurityConfigurer extends + WebSecurityConfigurerAdapter { + + @Autowired + private H2ConsoleProperties console; + + @Autowired + private SecurityProperties security; + + @Override + public void configure(HttpSecurity http) throws Exception { + HttpSecurity h2Console = http.antMatcher(this.console.getPath().endsWith( + "/") ? this.console.getPath() + "**" : this.console.getPath() + + "/**"); + h2Console.csrf().disable(); + h2Console.httpBasic(); + h2Console.headers().frameOptions().sameOrigin(); + String[] roles = this.security.getUser().getRole().toArray(new String[0]); + SecurityAuthorizeMode mode = this.security.getBasic().getAuthorizeMode(); + if (mode == null || mode == SecurityAuthorizeMode.ROLE) { + http.authorizeRequests().anyRequest().hasAnyRole(roles); + } + else if (mode == SecurityAuthorizeMode.AUTHENTICATED) { + http.authorizeRequests().anyRequest().authenticated(); + } + } + + } + + } + +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/h2/H2ConsoleProperties.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/h2/H2ConsoleProperties.java new file mode 100644 index 00000000000..498706ad23f --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/h2/H2ConsoleProperties.java @@ -0,0 +1,61 @@ +/* + * Copyright 2012-2015 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 + * + * http://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.boot.autoconfigure.h2; + +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Pattern; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Configuration properties for H2's console + * + * @author Andy Wilkinson + * @since 1.3.0 + */ +@ConfigurationProperties(prefix = "spring.h2.console") +public class H2ConsoleProperties { + + /** + * Path at which the console will be available. + */ + @NotNull + @Pattern(regexp = "/[^?#]*", message = "Path must start with /") + private String path = "/h2-console"; + + /** + * Enable the console. + */ + private boolean enabled = false; + + public String getPath() { + return this.path; + } + + public void setPath(String path) { + this.path = path; + } + + public boolean getEnabled() { + return this.enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + +} diff --git a/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories index efe6b7a887f..6fd13ee1033 100644 --- a/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories @@ -22,6 +22,7 @@ org.springframework.boot.autoconfigure.data.rest.RepositoryRestMvcAutoConfigurat org.springframework.boot.autoconfigure.data.web.SpringDataWebAutoConfiguration,\ org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfiguration,\ org.springframework.boot.autoconfigure.gson.GsonAutoConfiguration,\ +org.springframework.boot.autoconfigure.h2.H2ConsoleAutoConfiguration,\ org.springframework.boot.autoconfigure.hateoas.HypermediaAutoConfiguration,\ org.springframework.boot.autoconfigure.integration.IntegrationAutoConfiguration,\ org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration,\ diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/h2/H2ConsoleAutoConfigurationIntegrationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/h2/H2ConsoleAutoConfigurationIntegrationTests.java new file mode 100644 index 00000000000..a7299b2d4b6 --- /dev/null +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/h2/H2ConsoleAutoConfigurationIntegrationTests.java @@ -0,0 +1,93 @@ +/* + * Copyright 2012-2015 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 + * + * http://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.boot.autoconfigure.h2; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.h2.H2ConsoleAutoConfigurationIntegrationTests.TestConfiguration; +import org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration; +import org.springframework.boot.autoconfigure.web.ServerPropertiesAutoConfiguration; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.stereotype.Controller; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.context.WebApplicationContext; + +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Integration tests for {@link H2ConsoleAutoConfiguration} + * + * @author Andy Wilkinson + */ +@RunWith(SpringJUnit4ClassRunner.class) +@WebAppConfiguration +@ContextConfiguration(classes = TestConfiguration.class) +@TestPropertySource(properties = "spring.h2.console.enabled:true") +public class H2ConsoleAutoConfigurationIntegrationTests { + + @Autowired + private WebApplicationContext context; + + @Test + public void noPrincipal() throws Exception { + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(springSecurity()).build(); + mockMvc.perform(get("/h2-console/")).andExpect(status().isUnauthorized()); + } + + @Test + public void userPrincipal() throws Exception { + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(springSecurity()).build(); + mockMvc.perform(get("/h2-console/").with(user("test").roles("USER"))) + .andExpect(status().isOk()) + .andExpect(header().string("X-Frame-Options", "SAMEORIGIN")); + } + + @Test + public void someOtherPrincipal() throws Exception { + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(springSecurity()).build(); + mockMvc.perform(get("/h2-console/").with(user("test").roles("FOO"))).andExpect( + status().isForbidden()); + } + + @Configuration + @Import({ SecurityAutoConfiguration.class, ServerPropertiesAutoConfiguration.class, + H2ConsoleAutoConfiguration.class }) + @Controller + static class TestConfiguration { + + @RequestMapping("/h2-console/**") + public void mockConsole() { + + } + } + +} diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/h2/H2ConsoleAutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/h2/H2ConsoleAutoConfigurationTests.java new file mode 100644 index 00000000000..04ad734e90f --- /dev/null +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/h2/H2ConsoleAutoConfigurationTests.java @@ -0,0 +1,112 @@ +/* + * Copyright 2012-2015 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 + * + * http://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.boot.autoconfigure.h2; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.boot.context.embedded.ServletRegistrationBean; +import org.springframework.boot.test.EnvironmentTestUtils; +import org.springframework.mock.web.MockServletContext; +import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasItems; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThat; + +/** + * Tests for {@link H2ConsoleAutoConfiguration} + * + * @author Andy Wilkinson + */ +public class H2ConsoleAutoConfigurationTests { + + private AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Before + public void setupContext() { + this.context.setServletContext(new MockServletContext()); + } + + @After + public void close() { + if (this.context != null) { + this.context.close(); + } + } + + @Test + public void consoleIsDisabledByDefault() { + this.context.register(H2ConsoleAutoConfiguration.class); + this.context.refresh(); + assertThat(this.context.getBeansOfType(ServletRegistrationBean.class).size(), + is(equalTo(0))); + } + + @Test + public void propertyCanEnableConsole() { + this.context.register(H2ConsoleAutoConfiguration.class); + EnvironmentTestUtils.addEnvironment(this.context, + "spring.h2.console.enabled:true"); + this.context.refresh(); + assertThat(this.context.getBeansOfType(ServletRegistrationBean.class).size(), + is(equalTo(1))); + assertThat(this.context.getBean(ServletRegistrationBean.class).getUrlMappings(), + hasItems("/h2-console/*")); + } + + @Test + public void customPathMustBeginWithASlash() { + this.thrown.expect(BeanCreationException.class); + this.thrown.expectMessage("Path must start with /"); + this.context.register(H2ConsoleAutoConfiguration.class); + EnvironmentTestUtils.addEnvironment(this.context, + "spring.h2.console.enabled:true", "spring.h2.console.path:custom"); + this.context.refresh(); + } + + @Test + public void customPathWithTrailingSlash() { + this.context.register(H2ConsoleAutoConfiguration.class); + EnvironmentTestUtils.addEnvironment(this.context, + "spring.h2.console.enabled:true", "spring.h2.console.path:/custom/"); + this.context.refresh(); + assertThat(this.context.getBeansOfType(ServletRegistrationBean.class).size(), + is(equalTo(1))); + assertThat(this.context.getBean(ServletRegistrationBean.class).getUrlMappings(), + hasItems("/custom/*")); + } + + @Test + public void customPath() { + this.context.register(H2ConsoleAutoConfiguration.class); + EnvironmentTestUtils.addEnvironment(this.context, + "spring.h2.console.enabled:true", "spring.h2.console.path:/custom"); + this.context.refresh(); + assertThat(this.context.getBeansOfType(ServletRegistrationBean.class).size(), + is(equalTo(1))); + assertThat(this.context.getBean(ServletRegistrationBean.class).getUrlMappings(), + hasItems("/custom/*")); + } +} diff --git a/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/autoconfigure/DevToolsPropertyDefaultsPostProcessor.java b/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/autoconfigure/DevToolsPropertyDefaultsPostProcessor.java index 45815b8221b..785b65a12cf 100644 --- a/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/autoconfigure/DevToolsPropertyDefaultsPostProcessor.java +++ b/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/autoconfigure/DevToolsPropertyDefaultsPostProcessor.java @@ -47,6 +47,7 @@ class DevToolsPropertyDefaultsPostProcessor implements BeanFactoryPostProcessor, properties.put("spring.velocity.cache", "false"); properties.put("spring.mustache.cache", "false"); properties.put("server.session.persistent", "true"); + properties.put("spring.h2.console.enabled", "true"); PROPERTIES = Collections.unmodifiableMap(properties); } diff --git a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc index 64d40d16479..4d28ce6125a 100644 --- a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc +++ b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc @@ -162,6 +162,10 @@ content into your application; rather pick only the properties that you need. multipart.max-file-size=1Mb # Max file size. multipart.max-request-size=10Mb # Max request size. + # H2 Web Console ({sc-spring-boot-autoconfigure}/h2/H2ConsoleProperties.{sc-ext}[H2ConsoleProperties]) + spring.h2.console.enable=false # Enable the console + spring.h2.console.path=/h2-console # Path at which the console can be accessed + # SPRING HATEOAS ({sc-spring-boot-autoconfigure}/hateoas/HateoasProperties.{sc-ext}[HateoasProperties]) spring.hateoas.apply-to-primary-object-mapper=true # if the primary mapper should also be configured diff --git a/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc b/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc index 130f20cacc5..6bfb832a9b0 100644 --- a/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc +++ b/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc @@ -2258,6 +2258,45 @@ Hibernate autoconfig is active because the `ddl-auto` settings are more fine-gra +[[boot-features-sql-h2-console]] +=== Using H2's web console +The http://www.h2database.com[H2 database] provides a +http://www.h2database.com/html/quickstart.html#h2_console[browser-based console] that +Spring Boot can auto-configure for you. The console will be auto-configured when the +following conditions are met: + +* You are developing a web application +* `com.h2database:h2` is on the classpath +* You are using <> + +TIP: If you are not using Spring Boot's developer tools, but would still like to make use +of H2's console, then you can do so by configuring the `spring.h2.console.enabled` +property with a value of `true`. The H2 console is only intended for use during +development so care should be taken to ensure that `spring.h2.console.enabled` is not set +to `true` in production. + + + +[[boot-features-sql-h2-console-custom-path]] +==== Changing the H2 console's path +By default the console will be available at `/h2-console`. You can customize the console's +path using the `spring.h2.console.path` property. + + + +[[boot-features-sql-h2-console-securing]] +==== Securing the H2 console +When Spring Security is one the and basic auth is enabled, the H2 console will be +automatically secured using basic auth. The following properties can be used to customize +the security configuration: + +* `security.user.role` +* `security.basic.authorize-mode` +* `security.basic.enabled` + + + [[boot-features-jooq]] == Using jOOQ Java Object Oriented Querying (http://www.jooq.org/[jOOQ]) is a popular product from