Add auto-configuration for H2’s web console

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
This commit is contained in:
Andy Wilkinson 2015-08-04 11:33:16 +01:00
parent f95c95a1f1
commit 2a5a32b603
9 changed files with 431 additions and 4 deletions

View File

@ -70,6 +70,11 @@
<artifactId>hazelcast-spring</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.samskivert</groupId>
<artifactId>jmustache</artifactId>
@ -549,8 +554,9 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-jdk14</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
@ -558,9 +564,13 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-jdk14</artifactId>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
</dependency>
</dependencies>
</project>

View File

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

View File

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

View File

@ -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,\

View File

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

View File

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

View File

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

View File

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

View File

@ -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 <<using-spring-boot.adoc#using-boot-devtools,Spring Boot's developer
tools>>
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