/*
* Copyright 2012-2017 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.http.HttpServletRequest;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
import org.springframework.boot.autoconfigure.security.SecurityProperties.Headers;
import org.springframework.boot.autoconfigure.security.SecurityProperties.Headers.ContentSecurityPolicyMode;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.autoconfigure.web.servlet.error.ErrorController;
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.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.WebSecurityConfigurer;
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.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.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.AnyRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* 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
* <code>/css/**, /js/**, /images/**, /**/favicon.ico</code>
* ). 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
* {@code (username=user, password=password)} but can easily be customized by providing a
* an {@link AuthenticationManager}. Also provides audit logging of authentication events.
* <p>
* Some common simple customizations:
* <ul>
* <li>Switch off security completely and permanently: remove Spring Security from the
* classpath or {@link EnableAutoConfiguration#exclude() exclude}
* {@link SecurityAutoConfiguration}.</li>
* <li>Switch off security temporarily (e.g. for a dev environment): set
* {@code security.basic.enabled=false}</li>
* <li>Customize the user details: autowire an {@link AuthenticationManagerBuilder} into a
* method in one of your configuration classes or equivalently add a bean of type
* AuthenticationManager</li>
* <li>Add form login for user facing resources: add a
* {@link WebSecurityConfigurerAdapter} and use {@link HttpSecurity#formLogin()}</li>
* </ul>
*
* @author Dave Syer
* @author Andy Wilkinson
*/
@Configuration
@EnableConfigurationProperties(ServerProperties.class)
@ConditionalOnClass({ EnableWebSecurity.class, AuthenticationEntryPoint.class })
@ConditionalOnMissingBean(WebSecurityConfiguration.class)
@ConditionalOnWebApplication(type = Type.SERVLET)
@EnableWebSecurity
public class SpringBootWebSecurityConfiguration {
private static List<String> DEFAULT_IGNORED = Arrays.asList("/css/**", "/js/**",
"/images/**", "/webjars/**", "/**/favicon.ico");
@Bean
@ConditionalOnMissingBean({ IgnoredPathsWebSecurityConfigurerAdapter.class })
public IgnoredPathsWebSecurityConfigurerAdapter ignoredPathsWebSecurityConfigurerAdapter(
List<IgnoredRequestCustomizer> customizers) {
return new IgnoredPathsWebSecurityConfigurerAdapter(customizers);
}
@Bean
public IgnoredRequestCustomizer defaultIgnoredRequestsCustomizer(
ServerProperties server, SecurityProperties security,
ObjectProvider<ErrorController> errorController) {
return new DefaultIgnoredRequestCustomizer(server, security,
errorController.getIfAvailable());
}
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().disable();
}
if (StringUtils.hasText(headers.getContentSecurityPolicy())) {
String policyDirectives = headers.getContentSecurityPolicy();
ContentSecurityPolicyMode mode = headers.getContentSecurityPolicyMode();
if (mode == ContentSecurityPolicyMode.DEFAULT) {
configurer.contentSecurityPolicy(policyDirectives);
}
else {
configurer.contentSecurityPolicy(policyDirectives).reportOnly();
}
}
if (!headers.isXss()) {
configurer.xssProtection().disable();
}
if (!headers.isCache()) {
configurer.cacheControl().disable();
}
if (!headers.isFrame()) {
configurer.frameOptions().disable();
}
}
// Get the ignored paths in early
@Order(SecurityProperties.IGNORED_ORDER)
private static class IgnoredPathsWebSecurityConfigurerAdapter
implements WebSecurityConfigurer<WebSecurity> {
private final List<IgnoredRequestCustomizer> customizers;
IgnoredPathsWebSecurityConfigurerAdapter(
List<IgnoredRequestCustomizer> customizers) {
this.customizers = customizers;
}
@Override
public void configure(WebSecurity builder) throws Exception {
}
@Override
public void init(WebSecurity builder) throws Exception {
for (IgnoredRequestCustomizer customizer : this.customizers) {
customizer.customize(builder.ignoring());
}
}
}
private class DefaultIgnoredRequestCustomizer implements IgnoredRequestCustomizer {
private final ServerProperties server;
private final SecurityProperties security;
private final ErrorController errorController;
DefaultIgnoredRequestCustomizer(ServerProperties server,
SecurityProperties security, ErrorController errorController) {
this.server = server;
this.security = security;
this.errorController = errorController;
}
@Override
public void customize(IgnoredRequestConfigurer configurer) {
List<String> ignored = getIgnored(this.security);
if (this.errorController != null) {
ignored.add(normalizePath(this.errorController.getErrorPath()));
}
String[] paths = this.server.getServlet().getPathsArray(ignored);
List<RequestMatcher> matchers = new ArrayList<>();
if (!ObjectUtils.isEmpty(paths)) {
for (String pattern : paths) {
matchers.add(new AntPathRequestMatcher(pattern, null));
}
}
if (!matchers.isEmpty()) {
configurer.requestMatchers(new OrRequestMatcher(matchers));
}
}
private List<String> getIgnored(SecurityProperties security) {
List<String> ignored = new ArrayList<>(security.getIgnored());
if (ignored.isEmpty()) {
ignored.addAll(DEFAULT_IGNORED);
}
else if (ignored.contains("none")) {
ignored.remove("none");
}
return ignored;
}
private String normalizePath(String errorPath) {
String result = StringUtils.cleanPath(errorPath);
if (!result.startsWith("/")) {
result = "/" + result;
}
return result;
}
}
@Configuration
@ConditionalOnProperty(prefix = "security.basic", name = "enabled", havingValue = "false")
@Order(SecurityProperties.BASIC_AUTH_ORDER)
protected static class ApplicationNoWebSecurityConfigurerAdapter
extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.requestMatcher(new RequestMatcher() {
@Override
public boolean matches(HttpServletRequest request) {
return false;
}
});
}
}
@Configuration
@ConditionalOnProperty(prefix = "security.basic", name = "enabled", matchIfMissing = true)
@Order(SecurityProperties.BASIC_AUTH_ORDER)
protected static class ApplicationWebSecurityConfigurerAdapter
extends WebSecurityConfigurerAdapter {
private SecurityProperties security;
protected ApplicationWebSecurityConfigurerAdapter(SecurityProperties security) {
this.security = security;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
if (this.security.isRequireSsl()) {
http.requiresChannel().anyRequest().requiresSecure();
}
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());
String[] paths = getSecureApplicationPaths();
if (paths.length > 0) {
AuthenticationEntryPoint entryPoint = entryPoint();
http.exceptionHandling().authenticationEntryPoint(entryPoint);
http.httpBasic().authenticationEntryPoint(entryPoint);
http.requestMatchers().antMatchers(paths);
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();
}
}
}
private String[] getSecureApplicationPaths() {
List<String> 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;
}
}
}