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>&#47;css&#47;**, &#47;js&#47;**, &#47;images&#47;**, &#47;**&#47;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("/**")) {
288                                        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}