diff --git a/docs/howto.md b/docs/howto.md index c1bd28ef94..31dcd13ae2 100644 --- a/docs/howto.md +++ b/docs/howto.md @@ -476,6 +476,26 @@ default configuration you should find a `BeanNameViewResolver` in your way of doing that. Look at `ErrorMvcAutoConfiguration` for more options. +## Secure an Application + +Web applications will be secure by default (with Basic authentication +on all endpoints) if Spring Security is on the classpath. To add +method-level security to a web application you can simply +`@EnableGlobalMethodSecurity` with your desired settings. + +The default `AuthenticationManager` has a single user (username "user" +and password random, printed at INFO when the application starts +up). You can change the password by providing a +`security.user.password`. This and other useful properties are +externalized via `SecurityProperties`. + +## Change the AuthenticationManager and add User Accounts + +If you provide a `@Bean` of type `AuthenticationManager` the default +one will not be created, so you have the full feature set of Spring +Security available +(e.g. [various authentication options](http://docs.spring.io/spring-security/site/docs/3.2.1.CI-SNAPSHOT/reference/htmlsingle/#jc-authentication)). + ## Use 'Short' Command Line Arguments Some people like to use (for example) `--port=9000` instead of diff --git a/spring-boot-actuator/pom.xml b/spring-boot-actuator/pom.xml index bc302bea7a..7cfb0893ea 100644 --- a/spring-boot-actuator/pom.xml +++ b/spring-boot-actuator/pom.xml @@ -102,6 +102,11 @@ true + + ch.qos.logback + logback-classic + test + org.springframework spring-test diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/ManagementSecurityAutoConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/ManagementSecurityAutoConfiguration.java index c8faa74e3d..58d79722bc 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/ManagementSecurityAutoConfiguration.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/ManagementSecurityAutoConfiguration.java @@ -39,6 +39,7 @@ import org.springframework.boot.autoconfigure.security.AuthenticationManagerConf import org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration; import org.springframework.boot.autoconfigure.security.SecurityPrequisite; import org.springframework.boot.autoconfigure.security.SecurityProperties; +import org.springframework.boot.autoconfigure.security.SpringBootWebSecurityConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -127,7 +128,7 @@ public class ManagementSecurityAutoConfiguration { IgnoredRequestConfigurer ignoring = builder.ignoring(); // The ignores are not cumulative, so to prevent overwriting the defaults we // add them back. - List ignored = SecurityAutoConfiguration.getIgnored(this.security); + List ignored = SpringBootWebSecurityConfiguration.getIgnored(this.security); ignored.addAll(Arrays.asList(getEndpointPaths(this.endpointHandlerMapping, false))); if (!this.management.getSecurity().isEnabled()) { @@ -185,7 +186,7 @@ public class ManagementSecurityAutoConfiguration { http.sessionManagement().sessionCreationPolicy( this.management.getSecurity().getSessions()); - SecurityAutoConfiguration.configureHeaders(http.headers(), + SpringBootWebSecurityConfiguration.configureHeaders(http.headers(), this.security.getHeaders()); } diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/ManagementSecurityAutoConfigurationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/ManagementSecurityAutoConfigurationTests.java index 9666e235db..7ce357e4e4 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/ManagementSecurityAutoConfigurationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/ManagementSecurityAutoConfigurationTests.java @@ -128,11 +128,11 @@ public class ManagementSecurityAutoConfigurationTests { public void testDisableBasicAuthOnApplicationPaths() throws Exception { this.context = new AnnotationConfigWebApplicationContext(); this.context.setServletContext(new MockServletContext()); - this.context.register(SecurityAutoConfiguration.class, - ManagementSecurityAutoConfiguration.class, - HttpMessageConvertersAutoConfiguration.class, + this.context.register(HttpMessageConvertersAutoConfiguration.class, EndpointAutoConfiguration.class, EndpointWebMvcAutoConfiguration.class, ManagementServerPropertiesAutoConfiguration.class, + SecurityAutoConfiguration.class, + ManagementSecurityAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class); EnvironmentTestUtils.addEnvironment(this.context, "security.basic.enabled:false"); this.context.refresh(); diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/SpringApplicationHierarchyTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/SpringApplicationHierarchyTests.java index 43eedfe21d..6f7c8452ce 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/SpringApplicationHierarchyTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/SpringApplicationHierarchyTests.java @@ -19,6 +19,7 @@ package org.springframework.boot.actuate.autoconfigure; import org.junit.After; import org.junit.Test; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration; import org.springframework.boot.autoconfigure.web.ServerPropertiesAutoConfiguration; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.context.ApplicationContext; @@ -61,7 +62,8 @@ public class SpringApplicationHierarchyTests { } @EnableAutoConfiguration(exclude = { ServerPropertiesAutoConfiguration.class, - JolokiaAutoConfiguration.class, EndpointMBeanExportAutoConfiguration.class }) + JolokiaAutoConfiguration.class, EndpointMBeanExportAutoConfiguration.class, + SecurityAutoConfiguration.class }) public static class Parent { } diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/AuthenticationManagerConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/AuthenticationManagerConfiguration.java index e4459388a6..0bdf7d8377 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/AuthenticationManagerConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/AuthenticationManagerConfiguration.java @@ -23,6 +23,8 @@ import java.util.Set; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.security.SecurityProperties.User; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -32,6 +34,8 @@ import org.springframework.security.config.annotation.authentication.builders.Au import org.springframework.security.config.annotation.authentication.configurers.provisioning.InMemoryUserDetailsManagerConfigurer; @Configuration +@ConditionalOnBean(ObjectPostProcessor.class) +@ConditionalOnMissingBean(AuthenticationManager.class) public class AuthenticationManagerConfiguration { private static Log logger = LogFactory diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/SecurityAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/SecurityAutoConfiguration.java index 3d8688f4d6..b5b6bbbc91 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/SecurityAutoConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/SecurityAutoConfiguration.java @@ -16,254 +16,24 @@ package org.springframework.boot.autoconfigure.security; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import javax.servlet.Filter; - -import org.springframework.beans.factory.annotation.Autowired; -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.ConditionalOnExpression; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; -import org.springframework.boot.autoconfigure.security.SecurityProperties.Headers; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.core.Ordered; -import org.springframework.core.annotation.Order; -import org.springframework.security.authentication.AuthenticationEventPublisher; +import org.springframework.context.annotation.Import; import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.authentication.DefaultAuthenticationEventPublisher; -import org.springframework.security.authentication.ProviderManager; -import org.springframework.security.config.annotation.SecurityConfigurer; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.builders.WebSecurity; -import org.springframework.security.config.annotation.web.builders.WebSecurity.IgnoredRequestConfigurer; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; -import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer; -import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity; -import org.springframework.security.web.AuthenticationEntryPoint; -import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint; -import org.springframework.security.web.header.writers.HstsHeaderWriter; -import org.springframework.security.web.util.matcher.AnyRequestMatcher; -import org.springframework.web.servlet.support.RequestDataValueProcessor; -/** - * {@link EnableAutoConfiguration Auto-configuration} for security of a web application or - * service. By default everything is secured with HTTP Basic authentication except the - * {@link SecurityProperties#getIgnored() explicitly ignored} paths (defaults to - * /css/**, /js/**, /images/**, /**/favicon.ico - * ). Many aspects of the behavior can be controller with {@link SecurityProperties} via - * externalized application properties (or via an bean definition of that type to set the - * defaults). The user details for authentication are just placeholders - * (username=user, - * password=password) but can easily be customized by providing a bean definition - * of type {@link AuthenticationManager}. Also provides audit logging of authentication - * events. - * - *

- * Some common simple customizations: - *

    - *
  • Switch off security completely and permanently: remove Spring Security from the - * classpath or {@link EnableAutoConfiguration#exclude() exclude} this configuration.
  • - *
  • Switch off security temporarily (e.g. for a dev environment): set - * security.basic.enabled: false
  • - *
  • Customize the user details: add an AuthenticationManager bean
  • - *
  • Add form login for user facing resources: add a - * {@link WebSecurityConfigurerAdapter} and use {@link HttpSecurity#formLogin()}
  • - *
- * - * @author Dave Syer - */ @Configuration +@ConditionalOnClass(AuthenticationManager.class) @EnableConfigurationProperties -@ConditionalOnClass({ EnableWebSecurity.class }) -@ConditionalOnMissingBean(WebSecurityConfiguration.class) -// @ConditionalOnMissingBean(annotation = EnableWebSecurity.class) +@Import({ SpringBootWebSecurityConfiguration.class, AuthenticationManagerConfiguration.class }) public class SecurityAutoConfiguration { - private static List DEFAULT_IGNORED = Arrays.asList("/css/**", "/js/**", - "/images/**", "/**/favicon.ico"); - @Bean(name = "org.springframework.autoconfigure.security.SecurityProperties") @ConditionalOnMissingBean public SecurityProperties securityProperties() { return new SecurityProperties(); } - @Bean - @ConditionalOnMissingBean - public AuthenticationEventPublisher authenticationEventPublisher() { - return new DefaultAuthenticationEventPublisher(); - } - - @Bean - @ConditionalOnMissingBean({ IgnoredPathsWebSecurityConfigurerAdapter.class }) - // @ConditionalOnBean(annotation = EnableWebSecurity.class) - @ConditionalOnBean(WebSecurityConfiguration.class) - public SecurityConfigurer ignoredPathsWebSecurityConfigurerAdapter() { - return new IgnoredPathsWebSecurityConfigurerAdapter(); - } - - // Get the ignored paths in early - @Order(Ordered.HIGHEST_PRECEDENCE) - private static class IgnoredPathsWebSecurityConfigurerAdapter implements - SecurityConfigurer { - - @Autowired - private SecurityProperties security; - - @Override - public void configure(WebSecurity builder) throws Exception { - } - - @Override - public void init(WebSecurity builder) throws Exception { - IgnoredRequestConfigurer ignoring = builder.ignoring(); - List ignored = getIgnored(this.security); - ignoring.antMatchers(ignored.toArray(new String[0])); - } - - } - - // Pull in @EnableWebMvcSecurity if Spring MVC is available and no-one defined a - // RequestDataValueProcessor - @ConditionalOnClass(RequestDataValueProcessor.class) - @ConditionalOnMissingBean(RequestDataValueProcessor.class) - @ConditionalOnExpression("${security.basic.enabled:true}") - @Configuration - protected static class WebMvcSecurityConfigurationConditions { - @Configuration - @EnableWebMvcSecurity - protected static class DefaultWebMvcSecurityConfiguration { - } - } - - // Pull in a plain @EnableWebSecurity if Spring MVC is not available - @ConditionalOnMissingBean(WebMvcSecurityConfigurationConditions.class) - @ConditionalOnMissingClass(name = "org.springframework.web.servlet.support.RequestDataValueProcessor") - @ConditionalOnExpression("${security.basic.enabled:true}") - @Configuration - @EnableWebSecurity - protected static class DefaultWebSecurityConfiguration { - } - - @ConditionalOnExpression("${security.basic.enabled:true}") - @Configuration - @Order(Ordered.LOWEST_PRECEDENCE - 5) - protected static class ApplicationWebSecurityConfigurerAdapter extends - WebSecurityConfigurerAdapter { - - @Autowired - private SecurityProperties security; - - @Autowired - private AuthenticationEventPublisher authenticationEventPublisher; - - @Override - protected void configure(HttpSecurity http) throws Exception { - - if (this.security.isRequireSsl()) { - http.requiresChannel().anyRequest().requiresSecure(); - } - - String[] paths = getSecureApplicationPaths(); - if (this.security.getBasic().isEnabled() && paths.length > 0) { - http.exceptionHandling().authenticationEntryPoint(entryPoint()); - http.requestMatchers().antMatchers(paths); - http.authorizeRequests() - .anyRequest() - .hasAnyRole( - this.security.getUser().getRole().toArray(new String[0])) // - .and().httpBasic() // - .and().anonymous().disable(); - } - if (!this.security.isEnableCsrf()) { - http.csrf().disable(); - } - // No cookies for application endpoints by default - http.sessionManagement().sessionCreationPolicy(this.security.getSessions()); - - SecurityAutoConfiguration.configureHeaders(http.headers(), - this.security.getHeaders()); - - } - - private String[] getSecureApplicationPaths() { - List list = new ArrayList(); - for (String path : this.security.getBasic().getPath()) { - path = (path == null ? "" : path.trim()); - if (path.equals("/**")) { - return new String[] { path }; - } - if (!path.equals("")) { - list.add(path); - } - } - return list.toArray(new String[list.size()]); - } - - private AuthenticationEntryPoint entryPoint() { - BasicAuthenticationEntryPoint entryPoint = new BasicAuthenticationEntryPoint(); - entryPoint.setRealmName(this.security.getBasic().getRealm()); - return entryPoint; - } - - @Override - protected AuthenticationManager authenticationManager() throws Exception { - AuthenticationManager manager = super.authenticationManager(); - if (manager instanceof ProviderManager) { - ((ProviderManager) manager) - .setAuthenticationEventPublisher(this.authenticationEventPublisher); - } - return manager; - } - - @Configuration - @ConditionalOnMissingBean(AuthenticationManager.class) - protected static class ApplicationAuthenticationManagerConfiguration extends - AuthenticationManagerConfiguration { - } - - } - - public static void configureHeaders(HeadersConfigurer configurer, - SecurityProperties.Headers headers) throws Exception { - if (headers.getHsts() != Headers.HSTS.none) { - boolean includeSubdomains = headers.getHsts() == Headers.HSTS.all; - HstsHeaderWriter writer = new HstsHeaderWriter(includeSubdomains); - writer.setRequestMatcher(AnyRequestMatcher.INSTANCE); - configurer.addHeaderWriter(writer); - } - if (headers.isContentType()) { - configurer.contentTypeOptions(); - } - if (headers.isXss()) { - configurer.xssProtection(); - } - if (headers.isCache()) { - configurer.cacheControl(); - } - if (headers.isFrame()) { - configurer.frameOptions(); - } - } - - public static List getIgnored(SecurityProperties security) { - List ignored = new ArrayList(security.getIgnored()); - if (ignored.isEmpty()) { - ignored.addAll(DEFAULT_IGNORED); - } - else if (ignored.contains("none")) { - ignored.remove("none"); - } - return ignored; - } - -} +} \ No newline at end of file diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/SpringBootWebSecurityConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/SpringBootWebSecurityConfiguration.java new file mode 100644 index 0000000000..349af7dd27 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/SpringBootWebSecurityConfiguration.java @@ -0,0 +1,259 @@ +/* + * Copyright 2012-2013 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.security; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import javax.servlet.Filter; + +import org.springframework.beans.factory.annotation.Autowired; +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.ConditionalOnExpression; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.autoconfigure.security.SecurityProperties.Headers; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.security.authentication.AuthenticationEventPublisher; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.DefaultAuthenticationEventPublisher; +import org.springframework.security.authentication.ProviderManager; +import org.springframework.security.config.annotation.SecurityConfigurer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.builders.WebSecurity; +import org.springframework.security.config.annotation.web.builders.WebSecurity.IgnoredRequestConfigurer; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer; +import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint; +import org.springframework.security.web.header.writers.HstsHeaderWriter; +import org.springframework.security.web.util.matcher.AnyRequestMatcher; +import org.springframework.web.servlet.support.RequestDataValueProcessor; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for security of a web application or + * service. By default everything is secured with HTTP Basic authentication except the + * {@link SecurityProperties#getIgnored() explicitly ignored} paths (defaults to + * /css/**, /js/**, /images/**, /**/favicon.ico + * ). Many aspects of the behavior can be controller with {@link SecurityProperties} via + * externalized application properties (or via an bean definition of that type to set the + * defaults). The user details for authentication are just placeholders + * (username=user, + * password=password) but can easily be customized by providing a bean definition + * of type {@link AuthenticationManager}. Also provides audit logging of authentication + * events. + * + *

+ * Some common simple customizations: + *

    + *
  • Switch off security completely and permanently: remove Spring Security from the + * classpath or {@link EnableAutoConfiguration#exclude() exclude} this configuration.
  • + *
  • Switch off security temporarily (e.g. for a dev environment): set + * security.basic.enabled: false
  • + *
  • Customize the user details: add an AuthenticationManager bean
  • + *
  • Add form login for user facing resources: add a + * {@link WebSecurityConfigurerAdapter} and use {@link HttpSecurity#formLogin()}
  • + *
+ * + * @author Dave Syer + */ +@Configuration +@EnableConfigurationProperties +@ConditionalOnClass({ EnableWebSecurity.class }) +@ConditionalOnMissingBean(WebSecurityConfiguration.class) +@ConditionalOnWebApplication +// @ConditionalOnMissingBean(annotation = EnableWebSecurity.class) +public class SpringBootWebSecurityConfiguration { + + private static List DEFAULT_IGNORED = Arrays.asList("/css/**", "/js/**", + "/images/**", "/**/favicon.ico"); + + @Bean + @ConditionalOnMissingBean + public AuthenticationEventPublisher authenticationEventPublisher() { + return new DefaultAuthenticationEventPublisher(); + } + + @Bean + @ConditionalOnMissingBean({ IgnoredPathsWebSecurityConfigurerAdapter.class }) + // @ConditionalOnBean(annotation = EnableWebSecurity.class) + @ConditionalOnBean(SpringBootWebSecurityConfiguration.class) + public SecurityConfigurer ignoredPathsWebSecurityConfigurerAdapter() { + return new IgnoredPathsWebSecurityConfigurerAdapter(); + } + + // Get the ignored paths in early + @Order(Ordered.HIGHEST_PRECEDENCE) + private static class IgnoredPathsWebSecurityConfigurerAdapter implements + SecurityConfigurer { + + @Autowired + private SecurityProperties security; + + @Override + public void configure(WebSecurity builder) throws Exception { + } + + @Override + public void init(WebSecurity builder) throws Exception { + IgnoredRequestConfigurer ignoring = builder.ignoring(); + List ignored = getIgnored(this.security); + ignoring.antMatchers(ignored.toArray(new String[0])); + } + + } + + // Pull in @EnableWebMvcSecurity if Spring MVC is available and no-one defined a + // RequestDataValueProcessor + @ConditionalOnClass(RequestDataValueProcessor.class) + @ConditionalOnMissingBean(RequestDataValueProcessor.class) + @ConditionalOnExpression("${security.basic.enabled:true}") + @Configuration + protected static class WebMvcSecurityConfigurationConditions { + @Configuration + @EnableWebMvcSecurity + protected static class DefaultWebMvcSecurityConfiguration { + } + } + + // Pull in a plain @EnableWebSecurity if Spring MVC is not available + @ConditionalOnMissingBean(WebMvcSecurityConfigurationConditions.class) + @ConditionalOnMissingClass(name = "org.springframework.web.servlet.support.RequestDataValueProcessor") + @ConditionalOnExpression("${security.basic.enabled:true}") + @Configuration + @EnableWebSecurity + protected static class DefaultWebSecurityConfiguration { + } + + @ConditionalOnExpression("${security.basic.enabled:true}") + @Configuration + @Order(Ordered.LOWEST_PRECEDENCE - 5) + protected static class ApplicationWebSecurityConfigurerAdapter extends + WebSecurityConfigurerAdapter { + + @Autowired + private SecurityProperties security; + + @Autowired + private AuthenticationEventPublisher authenticationEventPublisher; + + @Override + protected void configure(HttpSecurity http) throws Exception { + + if (this.security.isRequireSsl()) { + http.requiresChannel().anyRequest().requiresSecure(); + } + + String[] paths = getSecureApplicationPaths(); + if (this.security.getBasic().isEnabled() && paths.length > 0) { + http.exceptionHandling().authenticationEntryPoint(entryPoint()); + http.requestMatchers().antMatchers(paths); + http.authorizeRequests() + .anyRequest() + .hasAnyRole( + this.security.getUser().getRole().toArray(new String[0])) // + .and().httpBasic() // + .and().anonymous().disable(); + } + if (!this.security.isEnableCsrf()) { + http.csrf().disable(); + } + // No cookies for application endpoints by default + http.sessionManagement().sessionCreationPolicy(this.security.getSessions()); + + SpringBootWebSecurityConfiguration.configureHeaders(http.headers(), + this.security.getHeaders()); + + } + + private String[] getSecureApplicationPaths() { + List list = new ArrayList(); + for (String path : this.security.getBasic().getPath()) { + path = (path == null ? "" : path.trim()); + if (path.equals("/**")) { + return new String[] { path }; + } + if (!path.equals("")) { + list.add(path); + } + } + return list.toArray(new String[list.size()]); + } + + private AuthenticationEntryPoint entryPoint() { + BasicAuthenticationEntryPoint entryPoint = new BasicAuthenticationEntryPoint(); + entryPoint.setRealmName(this.security.getBasic().getRealm()); + return entryPoint; + } + + @Override + protected AuthenticationManager authenticationManager() throws Exception { + AuthenticationManager manager = super.authenticationManager(); + if (manager instanceof ProviderManager) { + ((ProviderManager) manager) + .setAuthenticationEventPublisher(this.authenticationEventPublisher); + } + return manager; + } + + } + + public static void configureHeaders(HeadersConfigurer configurer, + SecurityProperties.Headers headers) throws Exception { + if (headers.getHsts() != Headers.HSTS.none) { + boolean includeSubdomains = headers.getHsts() == Headers.HSTS.all; + HstsHeaderWriter writer = new HstsHeaderWriter(includeSubdomains); + writer.setRequestMatcher(AnyRequestMatcher.INSTANCE); + configurer.addHeaderWriter(writer); + } + if (headers.isContentType()) { + configurer.contentTypeOptions(); + } + if (headers.isXss()) { + configurer.xssProtection(); + } + if (headers.isCache()) { + configurer.cacheControl(); + } + if (headers.isFrame()) { + configurer.frameOptions(); + } + } + + public static List getIgnored(SecurityProperties security) { + List ignored = new ArrayList(security.getIgnored()); + if (ignored.isEmpty()) { + ignored.addAll(DEFAULT_IGNORED); + } + else if (ignored.contains("none")) { + ignored.remove("none"); + } + return ignored; + } + +} diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/SecurityAutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/SecurityAutoConfigurationTests.java index be8ede25e8..9fa40f7efd 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/SecurityAutoConfigurationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/SecurityAutoConfigurationTests.java @@ -106,12 +106,12 @@ public class SecurityAutoConfigurationTests { public void testJpaCoexistsHappily() throws Exception { this.context = new AnnotationConfigWebApplicationContext(); this.context.setServletContext(new MockServletContext()); - this.context.register(EntityConfiguration.class, TestConfiguration.class, + this.context.register(EntityConfiguration.class, PropertyPlaceholderAutoConfiguration.class, DataSourceAutoConfiguration.class, HibernateJpaAutoConfiguration.class, SecurityAutoConfiguration.class); // This can fail if security @Conditionals force early instantiation of the - // HibernateJpaAutoConfiguration + // HibernateJpaAutoConfiguration (e.g. the EntityManagerFactory is not found) this.context.refresh(); assertNotNull(this.context.getBean(JpaTransactionManager.class)); } diff --git a/spring-boot-samples/pom.xml b/spring-boot-samples/pom.xml index c16cec7ccb..a10fd482fe 100644 --- a/spring-boot-samples/pom.xml +++ b/spring-boot-samples/pom.xml @@ -32,6 +32,7 @@ spring-boot-sample-simple spring-boot-sample-tomcat spring-boot-sample-traditional + spring-boot-sample-web-secure spring-boot-sample-web-static spring-boot-sample-web-jsp spring-boot-sample-web-ui diff --git a/spring-boot-samples/spring-boot-sample-secure/pom.xml b/spring-boot-samples/spring-boot-sample-secure/pom.xml index 9b0ba1bfad..6780b3a896 100644 --- a/spring-boot-samples/spring-boot-sample-secure/pom.xml +++ b/spring-boot-samples/spring-boot-sample-secure/pom.xml @@ -19,15 +19,6 @@ spring-boot-starter-security ${project.version}
- - ${project.groupId} - spring-boot-starter-web - ${project.version} - - - org.thymeleaf - thymeleaf-spring4 - diff --git a/spring-boot-samples/spring-boot-sample-secure/resources/test.properties b/spring-boot-samples/spring-boot-sample-secure/resources/test.properties new file mode 100644 index 0000000000..2eef63c566 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-secure/resources/test.properties @@ -0,0 +1 @@ +security.user.password=password \ No newline at end of file diff --git a/spring-boot-samples/spring-boot-sample-secure/src/main/java/sample/secure/SampleSecureApplication.java b/spring-boot-samples/spring-boot-sample-secure/src/main/java/sample/secure/SampleSecureApplication.java new file mode 100644 index 0000000000..9d72d44af1 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-secure/src/main/java/sample/secure/SampleSecureApplication.java @@ -0,0 +1,53 @@ +/* + * Copyright 2012-2013 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 sample.secure; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.context.SecurityContextHolder; + +@EnableAutoConfiguration +@ComponentScan +@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true) +public class SampleSecureApplication implements CommandLineRunner { + + @Autowired + private SampleService service; + + @Override + public void run(String... args) throws Exception { + SecurityContextHolder.getContext().setAuthentication( + new UsernamePasswordAuthenticationToken("user", "N/A", AuthorityUtils + .commaSeparatedStringToAuthorityList("ROLE_USER"))); + try { + System.out.println(service.secure()); + } finally { + SecurityContextHolder.clearContext(); + } + } + + public static void main(String[] args) throws Exception { + SpringApplication.run(SampleSecureApplication.class, args); + } + +} diff --git a/spring-boot-samples/spring-boot-sample-secure/src/main/java/sample/secure/SampleService.java b/spring-boot-samples/spring-boot-sample-secure/src/main/java/sample/secure/SampleService.java new file mode 100644 index 0000000000..a5b02eb676 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-secure/src/main/java/sample/secure/SampleService.java @@ -0,0 +1,45 @@ +/* + * Copyright 2012-2013 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 sample.secure; + +import org.springframework.security.access.annotation.Secured; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Service; + +/** + * @author Dave Syer + * + */ +@Service +public class SampleService { + + @Secured("ROLE_USER") + public String secure() { + return "Hello Security"; + } + + @PreAuthorize("true") + public String authorized() { + return "Hello World"; + } + + @PreAuthorize("false") + public String denied() { + return "Goodbye World"; + } + +} diff --git a/spring-boot-samples/spring-boot-sample-secure/src/test/java/sample/secure/SampleSecureApplicationTests.java b/spring-boot-samples/spring-boot-sample-secure/src/test/java/sample/secure/SampleSecureApplicationTests.java new file mode 100644 index 0000000000..cb458ae69e --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-secure/src/test/java/sample/secure/SampleSecureApplicationTests.java @@ -0,0 +1,99 @@ +/* + * Copyright 2012-2013 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 sample.secure; + +import static org.junit.Assert.assertEquals; + +import org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import sample.secure.SampleSecureApplicationTests.TestConfiguration; + +/** + * Basic integration tests for demo application. + * + * @author Dave Syer + */ +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = { SampleSecureApplication.class, + TestConfiguration.class }) +public class SampleSecureApplicationTests { + + @Autowired + private SampleService service; + + @Autowired + private Authentication authentication; + + @After + public void close() { + SecurityContextHolder.clearContext(); + } + + @Test(expected = AuthenticationException.class) + public void secure() throws Exception { + assertEquals(service.secure(), "Hello Security"); + } + + @Test + public void authenticated() throws Exception { + SecurityContextHolder.getContext().setAuthentication(authentication); + assertEquals(service.secure(), "Hello Security"); + } + + @Test + public void preauth() throws Exception { + SecurityContextHolder.getContext().setAuthentication(authentication); + assertEquals(service.authorized(), "Hello World"); + } + + @Test(expected = AccessDeniedException.class) + public void denied() throws Exception { + SecurityContextHolder.getContext().setAuthentication(authentication); + assertEquals(service.denied(), "Goodbye World"); + } + + @PropertySource("classpath:test.properties") + @Configuration + protected static class TestConfiguration { + + @Autowired + private AuthenticationManager authenticationManager; + + @Bean + public Authentication user() { + return authenticationManager + .authenticate(new UsernamePasswordAuthenticationToken("user", + "password")); + } + + } + +} diff --git a/spring-boot-samples/spring-boot-sample-web-secure/pom.xml b/spring-boot-samples/spring-boot-sample-web-secure/pom.xml new file mode 100644 index 0000000000..534cd7f3bb --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-web-secure/pom.xml @@ -0,0 +1,40 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-samples + 0.5.0.BUILD-SNAPSHOT + + spring-boot-sample-web-secure + jar + + ${basedir}/../.. + + + + ${project.groupId} + spring-boot-starter-security + ${project.version} + + + ${project.groupId} + spring-boot-starter-web + ${project.version} + + + org.thymeleaf + thymeleaf-spring4 + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/spring-boot-samples/spring-boot-sample-secure/src/main/java/sample/ops/ui/SampleSecureApplication.java b/spring-boot-samples/spring-boot-sample-web-secure/src/main/java/sample/ops/ui/SampleSecureApplication.java similarity index 100% rename from spring-boot-samples/spring-boot-sample-secure/src/main/java/sample/ops/ui/SampleSecureApplication.java rename to spring-boot-samples/spring-boot-sample-web-secure/src/main/java/sample/ops/ui/SampleSecureApplication.java diff --git a/spring-boot-samples/spring-boot-sample-web-secure/src/main/resources/application.properties b/spring-boot-samples/spring-boot-sample-web-secure/src/main/resources/application.properties new file mode 100644 index 0000000000..03e8cdc841 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-web-secure/src/main/resources/application.properties @@ -0,0 +1,2 @@ +spring.thymeleaf.cache: false +debug: true \ No newline at end of file diff --git a/spring-boot-samples/spring-boot-sample-secure/src/main/resources/logback.xml b/spring-boot-samples/spring-boot-sample-web-secure/src/main/resources/logback.xml similarity index 100% rename from spring-boot-samples/spring-boot-sample-secure/src/main/resources/logback.xml rename to spring-boot-samples/spring-boot-sample-web-secure/src/main/resources/logback.xml diff --git a/spring-boot-samples/spring-boot-sample-secure/src/main/resources/static/css/bootstrap.min.css b/spring-boot-samples/spring-boot-sample-web-secure/src/main/resources/static/css/bootstrap.min.css similarity index 100% rename from spring-boot-samples/spring-boot-sample-secure/src/main/resources/static/css/bootstrap.min.css rename to spring-boot-samples/spring-boot-sample-web-secure/src/main/resources/static/css/bootstrap.min.css diff --git a/spring-boot-samples/spring-boot-sample-secure/src/main/resources/templates/error.html b/spring-boot-samples/spring-boot-sample-web-secure/src/main/resources/templates/error.html similarity index 100% rename from spring-boot-samples/spring-boot-sample-secure/src/main/resources/templates/error.html rename to spring-boot-samples/spring-boot-sample-web-secure/src/main/resources/templates/error.html diff --git a/spring-boot-samples/spring-boot-sample-secure/src/main/resources/templates/home.html b/spring-boot-samples/spring-boot-sample-web-secure/src/main/resources/templates/home.html similarity index 100% rename from spring-boot-samples/spring-boot-sample-secure/src/main/resources/templates/home.html rename to spring-boot-samples/spring-boot-sample-web-secure/src/main/resources/templates/home.html diff --git a/spring-boot-samples/spring-boot-sample-secure/src/main/resources/templates/login.html b/spring-boot-samples/spring-boot-sample-web-secure/src/main/resources/templates/login.html similarity index 100% rename from spring-boot-samples/spring-boot-sample-secure/src/main/resources/templates/login.html rename to spring-boot-samples/spring-boot-sample-web-secure/src/main/resources/templates/login.html diff --git a/spring-boot-samples/spring-boot-sample-secure/src/test/java/sample/ops/ui/SampleSecureApplicationTests.java b/spring-boot-samples/spring-boot-sample-web-secure/src/test/java/sample/ops/ui/SampleSecureApplicationTests.java similarity index 100% rename from spring-boot-samples/spring-boot-sample-secure/src/test/java/sample/ops/ui/SampleSecureApplicationTests.java rename to spring-boot-samples/spring-boot-sample-web-secure/src/test/java/sample/ops/ui/SampleSecureApplicationTests.java