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

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