001/* 002 * Copyright 2012-2017 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.boot.autoconfigure.security; 018 019import java.util.ArrayList; 020import java.util.Arrays; 021import java.util.List; 022 023import javax.servlet.http.HttpServletRequest; 024 025import org.springframework.beans.factory.ObjectProvider; 026import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 027import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; 028import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 029import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 030import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; 031import org.springframework.boot.autoconfigure.security.SecurityProperties.Headers; 032import org.springframework.boot.autoconfigure.security.SecurityProperties.Headers.ContentSecurityPolicyMode; 033import org.springframework.boot.autoconfigure.web.ErrorController; 034import org.springframework.boot.autoconfigure.web.ServerProperties; 035import org.springframework.boot.context.properties.EnableConfigurationProperties; 036import org.springframework.context.annotation.Bean; 037import org.springframework.context.annotation.Configuration; 038import org.springframework.core.annotation.Order; 039import org.springframework.security.authentication.AuthenticationManager; 040import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 041import org.springframework.security.config.annotation.web.WebSecurityConfigurer; 042import org.springframework.security.config.annotation.web.builders.HttpSecurity; 043import org.springframework.security.config.annotation.web.builders.WebSecurity; 044import org.springframework.security.config.annotation.web.builders.WebSecurity.IgnoredRequestConfigurer; 045import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 046import org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration; 047import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 048import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer; 049import org.springframework.security.web.AuthenticationEntryPoint; 050import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint; 051import org.springframework.security.web.header.writers.HstsHeaderWriter; 052import org.springframework.security.web.util.matcher.AntPathRequestMatcher; 053import org.springframework.security.web.util.matcher.AnyRequestMatcher; 054import org.springframework.security.web.util.matcher.OrRequestMatcher; 055import org.springframework.security.web.util.matcher.RequestMatcher; 056import org.springframework.util.ObjectUtils; 057import org.springframework.util.StringUtils; 058 059/** 060 * Configuration for security of a web application or service. By default everything is 061 * secured with HTTP Basic authentication except the 062 * {@link SecurityProperties#getIgnored() explicitly ignored} paths (defaults to 063 * <code>/css/**, /js/**, /images/**, /**/favicon.ico</code> 064 * ). Many aspects of the behavior can be controller with {@link SecurityProperties} via 065 * externalized application properties (or via an bean definition of that type to set the 066 * defaults). The user details for authentication are just placeholders 067 * {@code (username=user, password=password)} but can easily be customized by providing a 068 * an {@link AuthenticationManager}. Also provides audit logging of authentication events. 069 * <p> 070 * Some common simple customizations: 071 * <ul> 072 * <li>Switch off security completely and permanently: remove Spring Security from the 073 * classpath or {@link EnableAutoConfiguration#exclude() exclude} 074 * {@link SecurityAutoConfiguration}.</li> 075 * <li>Switch off security temporarily (e.g. for a dev environment): set 076 * {@code security.basic.enabled=false}</li> 077 * <li>Customize the user details: autowire an {@link AuthenticationManagerBuilder} into a 078 * method in one of your configuration classes or equivalently add a bean of type 079 * AuthenticationManager</li> 080 * <li>Add form login for user facing resources: add a 081 * {@link WebSecurityConfigurerAdapter} and use {@link HttpSecurity#formLogin()}</li> 082 * </ul> 083 * 084 * @author Dave Syer 085 * @author Andy Wilkinson 086 */ 087@Configuration 088@EnableConfigurationProperties 089@ConditionalOnClass({ EnableWebSecurity.class, AuthenticationEntryPoint.class }) 090@ConditionalOnMissingBean(WebSecurityConfiguration.class) 091@ConditionalOnWebApplication 092@EnableWebSecurity 093public class SpringBootWebSecurityConfiguration { 094 095 private static List<String> DEFAULT_IGNORED = Arrays.asList("/css/**", "/js/**", 096 "/images/**", "/webjars/**", "/**/favicon.ico"); 097 098 @Bean 099 @ConditionalOnMissingBean({ IgnoredPathsWebSecurityConfigurerAdapter.class }) 100 public IgnoredPathsWebSecurityConfigurerAdapter ignoredPathsWebSecurityConfigurerAdapter( 101 List<IgnoredRequestCustomizer> customizers) { 102 return new IgnoredPathsWebSecurityConfigurerAdapter(customizers); 103 } 104 105 @Bean 106 public IgnoredRequestCustomizer defaultIgnoredRequestsCustomizer( 107 ServerProperties server, SecurityProperties security, 108 ObjectProvider<ErrorController> errorController) { 109 return new DefaultIgnoredRequestCustomizer(server, security, 110 errorController.getIfAvailable()); 111 } 112 113 public static void configureHeaders(HeadersConfigurer<?> configurer, 114 SecurityProperties.Headers headers) throws Exception { 115 if (headers.getHsts() != Headers.HSTS.NONE) { 116 boolean includeSubDomains = headers.getHsts() == Headers.HSTS.ALL; 117 HstsHeaderWriter writer = new HstsHeaderWriter(includeSubDomains); 118 writer.setRequestMatcher(AnyRequestMatcher.INSTANCE); 119 configurer.addHeaderWriter(writer); 120 } 121 if (!headers.isContentType()) { 122 configurer.contentTypeOptions().disable(); 123 } 124 if (StringUtils.hasText(headers.getContentSecurityPolicy())) { 125 String policyDirectives = headers.getContentSecurityPolicy(); 126 ContentSecurityPolicyMode mode = headers.getContentSecurityPolicyMode(); 127 if (mode == ContentSecurityPolicyMode.DEFAULT) { 128 configurer.contentSecurityPolicy(policyDirectives); 129 } 130 else { 131 configurer.contentSecurityPolicy(policyDirectives).reportOnly(); 132 } 133 } 134 if (!headers.isXss()) { 135 configurer.xssProtection().disable(); 136 } 137 if (!headers.isCache()) { 138 configurer.cacheControl().disable(); 139 } 140 if (!headers.isFrame()) { 141 configurer.frameOptions().disable(); 142 } 143 } 144 145 // Get the ignored paths in early 146 @Order(SecurityProperties.IGNORED_ORDER) 147 private static class IgnoredPathsWebSecurityConfigurerAdapter 148 implements WebSecurityConfigurer<WebSecurity> { 149 150 private final List<IgnoredRequestCustomizer> customizers; 151 152 IgnoredPathsWebSecurityConfigurerAdapter( 153 List<IgnoredRequestCustomizer> customizers) { 154 this.customizers = customizers; 155 } 156 157 @Override 158 public void configure(WebSecurity builder) throws Exception { 159 } 160 161 @Override 162 public void init(WebSecurity builder) throws Exception { 163 for (IgnoredRequestCustomizer customizer : this.customizers) { 164 customizer.customize(builder.ignoring()); 165 } 166 } 167 168 } 169 170 private class DefaultIgnoredRequestCustomizer implements IgnoredRequestCustomizer { 171 172 private final ServerProperties server; 173 174 private final SecurityProperties security; 175 176 private final ErrorController errorController; 177 178 DefaultIgnoredRequestCustomizer(ServerProperties server, 179 SecurityProperties security, ErrorController errorController) { 180 this.server = server; 181 this.security = security; 182 this.errorController = errorController; 183 } 184 185 @Override 186 public void customize(IgnoredRequestConfigurer configurer) { 187 List<String> ignored = getIgnored(this.security); 188 if (this.errorController != null) { 189 ignored.add(normalizePath(this.errorController.getErrorPath())); 190 } 191 String[] paths = this.server.getPathsArray(ignored); 192 List<RequestMatcher> matchers = new ArrayList<RequestMatcher>(); 193 if (!ObjectUtils.isEmpty(paths)) { 194 for (String pattern : paths) { 195 matchers.add(new AntPathRequestMatcher(pattern, null)); 196 } 197 } 198 if (!matchers.isEmpty()) { 199 configurer.requestMatchers(new OrRequestMatcher(matchers)); 200 } 201 } 202 203 private List<String> getIgnored(SecurityProperties security) { 204 List<String> ignored = new ArrayList<String>(security.getIgnored()); 205 if (ignored.isEmpty()) { 206 ignored.addAll(DEFAULT_IGNORED); 207 } 208 else if (ignored.contains("none")) { 209 ignored.remove("none"); 210 } 211 return ignored; 212 } 213 214 private String normalizePath(String errorPath) { 215 String result = StringUtils.cleanPath(errorPath); 216 if (!result.startsWith("/")) { 217 result = "/" + result; 218 } 219 return result; 220 } 221 222 } 223 224 @Configuration 225 @ConditionalOnProperty(prefix = "security.basic", name = "enabled", havingValue = "false") 226 @Order(SecurityProperties.BASIC_AUTH_ORDER) 227 protected static class ApplicationNoWebSecurityConfigurerAdapter 228 extends WebSecurityConfigurerAdapter { 229 230 @Override 231 protected void configure(HttpSecurity http) throws Exception { 232 http.requestMatcher(new RequestMatcher() { 233 @Override 234 public boolean matches(HttpServletRequest request) { 235 return false; 236 } 237 }); 238 } 239 240 } 241 242 @Configuration 243 @ConditionalOnProperty(prefix = "security.basic", name = "enabled", matchIfMissing = true) 244 @Order(SecurityProperties.BASIC_AUTH_ORDER) 245 protected static class ApplicationWebSecurityConfigurerAdapter 246 extends WebSecurityConfigurerAdapter { 247 248 private SecurityProperties security; 249 250 protected ApplicationWebSecurityConfigurerAdapter(SecurityProperties security) { 251 this.security = security; 252 } 253 254 @Override 255 protected void configure(HttpSecurity http) throws Exception { 256 if (this.security.isRequireSsl()) { 257 http.requiresChannel().anyRequest().requiresSecure(); 258 } 259 if (!this.security.isEnableCsrf()) { 260 http.csrf().disable(); 261 } 262 // No cookies for application endpoints by default 263 http.sessionManagement().sessionCreationPolicy(this.security.getSessions()); 264 SpringBootWebSecurityConfiguration.configureHeaders(http.headers(), 265 this.security.getHeaders()); 266 String[] paths = getSecureApplicationPaths(); 267 if (paths.length > 0) { 268 AuthenticationEntryPoint entryPoint = entryPoint(); 269 http.exceptionHandling().authenticationEntryPoint(entryPoint); 270 http.httpBasic().authenticationEntryPoint(entryPoint); 271 http.requestMatchers().antMatchers(paths); 272 String[] roles = this.security.getUser().getRole().toArray(new String[0]); 273 SecurityAuthorizeMode mode = this.security.getBasic().getAuthorizeMode(); 274 if (mode == null || mode == SecurityAuthorizeMode.ROLE) { 275 http.authorizeRequests().anyRequest().hasAnyRole(roles); 276 } 277 else if (mode == SecurityAuthorizeMode.AUTHENTICATED) { 278 http.authorizeRequests().anyRequest().authenticated(); 279 } 280 } 281 } 282 283 private String[] getSecureApplicationPaths() { 284 List<String> list = new ArrayList<String>(); 285 for (String path : this.security.getBasic().getPath()) { 286 path = (path == null ? "" : path.trim()); 287 if (path.equals("/**")) { return new String[] { path }; 289 } 290 if (!path.equals("")) { 291 list.add(path); 292 } 293 } 294 return list.toArray(new String[list.size()]); 295 } 296 297 private AuthenticationEntryPoint entryPoint() { 298 BasicAuthenticationEntryPoint entryPoint = new BasicAuthenticationEntryPoint(); 299 entryPoint.setRealmName(this.security.getBasic().getRealm()); 300 return entryPoint; 301 } 302 303 } 304 305}