/* * Copyright 2002-2016 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.security.config.annotation.web.configuration; import java.lang.reflect.Field; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.aop.TargetSource; import org.springframework.aop.framework.Advised; import org.springframework.aop.target.LazyInitTargetSource; import org.springframework.beans.FatalBeanException; import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.core.annotation.Order; import org.springframework.core.io.support.SpringFactoriesLoader; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationTrustResolver; import org.springframework.security.authentication.AuthenticationTrustResolverImpl; import org.springframework.security.authentication.DefaultAuthenticationEventPublisher; import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; 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.configurers.AbstractHttpConfigurer; import org.springframework.security.config.annotation.web.configurers.DefaultLoginPageConfigurer; import org.springframework.security.config.annotation.web.configurers.SecurityContextConfigurer; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.web.access.intercept.FilterSecurityInterceptor; import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter; import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; import org.springframework.web.accept.ContentNegotiationStrategy; import org.springframework.web.accept.HeaderContentNegotiationStrategy; /** * Provides a convenient base class for creating a {@link WebSecurityConfigurer} * instance. The implementation allows customization by overriding methods. * * <p> * Will automatically apply the result of looking up * {@link AbstractHttpConfigurer} from {@link SpringFactoriesLoader} to allow * developers to extend the defaults. * To do this, you must create a class that extends AbstractHttpConfigurer and then create a file in the classpath at "META-INF/spring.factories" that looks something like: * </p> * <pre> * org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer = sample.MyClassThatExtendsAbstractHttpConfigurer * </pre> * If you have multiple classes that should be added you can use "," to separate the values. For example: * * <pre> * org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer = sample.MyClassThatExtendsAbstractHttpConfigurer, sample.OtherThatExtendsAbstractHttpConfigurer * </pre> * * @see EnableWebSecurity * * @author Rob Winch */ @Order(100) public abstract class WebSecurityConfigurerAdapter implements WebSecurityConfigurer<WebSecurity> { private final Log logger = LogFactory.getLog(WebSecurityConfigurerAdapter.class); private ApplicationContext context; private ContentNegotiationStrategy contentNegotiationStrategy = new HeaderContentNegotiationStrategy(); private ObjectPostProcessor<Object> objectPostProcessor = new ObjectPostProcessor<Object>() { public <T> T postProcess(T object) { throw new IllegalStateException( ObjectPostProcessor.class.getName() + " is a required bean. Ensure you have used @EnableWebSecurity and @Configuration"); } }; private AuthenticationConfiguration authenticationConfiguration; private AuthenticationManagerBuilder authenticationBuilder; private AuthenticationManagerBuilder localConfigureAuthenticationBldr; private boolean disableLocalConfigureAuthenticationBldr; private boolean authenticationManagerInitialized; private AuthenticationManager authenticationManager; private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl(); private HttpSecurity http; private boolean disableDefaults; /** * Creates an instance with the default configuration enabled. */ protected WebSecurityConfigurerAdapter() { this(false); } /** * Creates an instance which allows specifying if the default configuration should be * enabled. Disabling the default configuration should be considered more advanced * usage as it requires more understanding of how the framework is implemented. * * @param disableDefaults true if the default configuration should be disabled, else * false */ protected WebSecurityConfigurerAdapter(boolean disableDefaults) { this.disableDefaults = disableDefaults; } /** * Used by the default implementation of {@link #authenticationManager()} to attempt * to obtain an {@link AuthenticationManager}. If overridden, the * {@link AuthenticationManagerBuilder} should be used to specify the * {@link AuthenticationManager}. * * <p> * The {@link #authenticationManagerBean()} method can be used to expose the resulting * {@link AuthenticationManager} as a Bean. The {@link #userDetailsServiceBean()} can * be used to expose the last populated {@link UserDetailsService} that is created * with the {@link AuthenticationManagerBuilder} as a Bean. The * {@link UserDetailsService} will also automatically be populated on * {@link HttpSecurity#getSharedObject(Class)} for use with other * {@link SecurityContextConfigurer} (i.e. RememberMeConfigurer ) * </p> * * <p> * For example, the following configuration could be used to register in memory * authentication that exposes an in memory {@link UserDetailsService}: * </p> * * <pre> * @Override * protected void configure(AuthenticationManagerBuilder auth) { * auth * // enable in memory based authentication with a user named * // "user" and "admin" * .inMemoryAuthentication().withUser("user").password("password").roles("USER").and() * .withUser("admin").password("password").roles("USER", "ADMIN"); * } * * // Expose the UserDetailsService as a Bean * @Bean * @Override * public UserDetailsService userDetailsServiceBean() throws Exception { * return super.userDetailsServiceBean(); * } * * </pre> * * @param auth the {@link AuthenticationManagerBuilder} to use * @throws Exception */ protected void configure(AuthenticationManagerBuilder auth) throws Exception { this.disableLocalConfigureAuthenticationBldr = true; } /** * Creates the {@link HttpSecurity} or returns the current instance * * ] * @return the {@link HttpSecurity} * @throws Exception */ @SuppressWarnings({ "rawtypes", "unchecked" }) protected final HttpSecurity getHttp() throws Exception { if (http != null) { return http; } DefaultAuthenticationEventPublisher eventPublisher = objectPostProcessor .postProcess(new DefaultAuthenticationEventPublisher()); localConfigureAuthenticationBldr.authenticationEventPublisher(eventPublisher); AuthenticationManager authenticationManager = authenticationManager(); authenticationBuilder.parentAuthenticationManager(authenticationManager); Map<Class<? extends Object>, Object> sharedObjects = createSharedObjects(); http = new HttpSecurity(objectPostProcessor, authenticationBuilder, sharedObjects); if (!disableDefaults) { // @formatter:off http .csrf().and() .addFilter(new WebAsyncManagerIntegrationFilter()) .exceptionHandling().and() .headers().and() .sessionManagement().and() .securityContext().and() .requestCache().and() .anonymous().and() .servletApi().and() .apply(new DefaultLoginPageConfigurer<HttpSecurity>()).and() .logout(); // @formatter:on ClassLoader classLoader = this.context.getClassLoader(); List<AbstractHttpConfigurer> defaultHttpConfigurers = SpringFactoriesLoader.loadFactories(AbstractHttpConfigurer.class, classLoader); for(AbstractHttpConfigurer configurer : defaultHttpConfigurers) { http.apply(configurer); } } configure(http); return http; } /** * Override this method to expose the {@link AuthenticationManager} from * {@link #configure(AuthenticationManagerBuilder)} to be exposed as a Bean. For * example: * * <pre> * @Bean(name name="myAuthenticationManager") * @Override * public AuthenticationManager authenticationManagerBean() throws Exception { * return super.authenticationManagerBean(); * } * </pre> * * @return the {@link AuthenticationManager} * @throws Exception */ public AuthenticationManager authenticationManagerBean() throws Exception { return new AuthenticationManagerDelegator(authenticationBuilder, context); } /** * Gets the {@link AuthenticationManager} to use. The default strategy is if * {@link #configure(AuthenticationManagerBuilder)} method is overridden to use the * {@link AuthenticationManagerBuilder} that was passed in. Otherwise, autowire the * {@link AuthenticationManager} by type. * * @return * @throws Exception */ protected AuthenticationManager authenticationManager() throws Exception { if (!authenticationManagerInitialized) { configure(localConfigureAuthenticationBldr); if (disableLocalConfigureAuthenticationBldr) { authenticationManager = authenticationConfiguration .getAuthenticationManager(); } else { authenticationManager = localConfigureAuthenticationBldr.build(); } authenticationManagerInitialized = true; } return authenticationManager; } /** * Override this method to expose a {@link UserDetailsService} created from * {@link #configure(AuthenticationManagerBuilder)} as a bean. In general only the * following override should be done of this method: * * <pre> * @Bean(name = "myUserDetailsService") * // any or no name specified is allowed * @Override * public UserDetailsService userDetailsServiceBean() throws Exception { * return super.userDetailsServiceBean(); * } * </pre> * * To change the instance returned, developers should change * {@link #userDetailsService()} instead * @return * @throws Exception * @see #userDetailsService() */ public UserDetailsService userDetailsServiceBean() throws Exception { AuthenticationManagerBuilder globalAuthBuilder = context .getBean(AuthenticationManagerBuilder.class); return new UserDetailsServiceDelegator(Arrays.asList( localConfigureAuthenticationBldr, globalAuthBuilder)); } /** * Allows modifying and accessing the {@link UserDetailsService} from * {@link #userDetailsServiceBean()} without interacting with the * {@link ApplicationContext}. Developers should override this method when changing * the instance of {@link #userDetailsServiceBean()}. * * @return */ protected UserDetailsService userDetailsService() { AuthenticationManagerBuilder globalAuthBuilder = context .getBean(AuthenticationManagerBuilder.class); return new UserDetailsServiceDelegator(Arrays.asList( localConfigureAuthenticationBldr, globalAuthBuilder)); } public void init(final WebSecurity web) throws Exception { final HttpSecurity http = getHttp(); web.addSecurityFilterChainBuilder(http).postBuildAction(new Runnable() { public void run() { FilterSecurityInterceptor securityInterceptor = http .getSharedObject(FilterSecurityInterceptor.class); web.securityInterceptor(securityInterceptor); } }); } /** * Override this method to configure {@link WebSecurity}. For example, if you wish to * ignore certain requests. */ public void configure(WebSecurity web) throws Exception { } /** * Override this method to configure the {@link HttpSecurity}. Typically subclasses * should not invoke this method by calling super as it may override their * configuration. The default configuration is: * * <pre> * http.authorizeRequests().anyRequest().authenticated().and().formLogin().and().httpBasic(); * </pre> * * @param http the {@link HttpSecurity} to modify * @throws Exception if an error occurs */ // @formatter:off protected void configure(HttpSecurity http) throws Exception { logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity)."); http .authorizeRequests() .anyRequest().authenticated() .and() .formLogin().and() .httpBasic(); } // @formatter:on /** * Gets the ApplicationContext * @return the context */ protected final ApplicationContext getApplicationContext() { return this.context; } @Autowired public void setApplicationContext(ApplicationContext context) { this.context = context; } @Autowired(required = false) public void setTrustResolver(AuthenticationTrustResolver trustResolver) { this.trustResolver = trustResolver; } @Autowired(required = false) public void setContentNegotationStrategy( ContentNegotiationStrategy contentNegotiationStrategy) { this.contentNegotiationStrategy = contentNegotiationStrategy; } @Autowired public void setObjectPostProcessor(ObjectPostProcessor<Object> objectPostProcessor) { this.objectPostProcessor = objectPostProcessor; authenticationBuilder = new AuthenticationManagerBuilder(objectPostProcessor); localConfigureAuthenticationBldr = new AuthenticationManagerBuilder( objectPostProcessor) { @Override public AuthenticationManagerBuilder eraseCredentials(boolean eraseCredentials) { authenticationBuilder.eraseCredentials(eraseCredentials); return super.eraseCredentials(eraseCredentials); } }; } @Autowired public void setAuthenticationConfiguration( AuthenticationConfiguration authenticationConfiguration) { this.authenticationConfiguration = authenticationConfiguration; } /** * Creates the shared objects * * @return the shared Objects */ private Map<Class<? extends Object>, Object> createSharedObjects() { Map<Class<? extends Object>, Object> sharedObjects = new HashMap<Class<? extends Object>, Object>(); sharedObjects.putAll(localConfigureAuthenticationBldr.getSharedObjects()); sharedObjects.put(UserDetailsService.class, userDetailsService()); sharedObjects.put(ApplicationContext.class, context); sharedObjects.put(ContentNegotiationStrategy.class, contentNegotiationStrategy); sharedObjects.put(AuthenticationTrustResolver.class, trustResolver); return sharedObjects; } /** * Delays the use of the {@link UserDetailsService} from the * {@link AuthenticationManagerBuilder} to ensure that it has been fully configured. * * @author Rob Winch * @since 3.2 */ static final class UserDetailsServiceDelegator implements UserDetailsService { private List<AuthenticationManagerBuilder> delegateBuilders; private UserDetailsService delegate; private final Object delegateMonitor = new Object(); UserDetailsServiceDelegator(List<AuthenticationManagerBuilder> delegateBuilders) { if (delegateBuilders.contains(null)) { throw new IllegalArgumentException( "delegateBuilders cannot contain null values. Got " + delegateBuilders); } this.delegateBuilders = delegateBuilders; } public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { if (delegate != null) { return delegate.loadUserByUsername(username); } synchronized (delegateMonitor) { if (delegate == null) { for (AuthenticationManagerBuilder delegateBuilder : delegateBuilders) { delegate = delegateBuilder.getDefaultUserDetailsService(); if (delegate != null) { break; } } if (delegate == null) { throw new IllegalStateException("UserDetailsService is required."); } this.delegateBuilders = null; } } return delegate.loadUserByUsername(username); } } /** * Delays the use of the {@link AuthenticationManager} build from the * {@link AuthenticationManagerBuilder} to ensure that it has been fully configured. * * @author Rob Winch * @since 3.2 */ static final class AuthenticationManagerDelegator implements AuthenticationManager { private AuthenticationManagerBuilder delegateBuilder; private AuthenticationManager delegate; private final Object delegateMonitor = new Object(); private Set<String> beanNames; AuthenticationManagerDelegator(AuthenticationManagerBuilder delegateBuilder, ApplicationContext context) { Assert.notNull(delegateBuilder, "delegateBuilder cannot be null"); Field parentAuthMgrField = ReflectionUtils.findField( AuthenticationManagerBuilder.class, "parentAuthenticationManager"); ReflectionUtils.makeAccessible(parentAuthMgrField); beanNames = getAuthenticationManagerBeanNames(context); validateBeanCycle( ReflectionUtils.getField(parentAuthMgrField, delegateBuilder), beanNames); this.delegateBuilder = delegateBuilder; } public Authentication authenticate(Authentication authentication) throws AuthenticationException { if (delegate != null) { return delegate.authenticate(authentication); } synchronized (delegateMonitor) { if (delegate == null) { delegate = this.delegateBuilder.getObject(); this.delegateBuilder = null; } } return delegate.authenticate(authentication); } private static Set<String> getAuthenticationManagerBeanNames( ApplicationContext applicationContext) { String[] beanNamesForType = BeanFactoryUtils .beanNamesForTypeIncludingAncestors(applicationContext, AuthenticationManager.class); return new HashSet<String>(Arrays.asList(beanNamesForType)); } private static void validateBeanCycle(Object auth, Set<String> beanNames) { if (auth != null && !beanNames.isEmpty()) { if (auth instanceof Advised) { Advised advised = (Advised) auth; TargetSource targetSource = advised.getTargetSource(); if (targetSource instanceof LazyInitTargetSource) { LazyInitTargetSource lits = (LazyInitTargetSource) targetSource; if (beanNames.contains(lits.getTargetBeanName())) { throw new FatalBeanException( "A dependency cycle was detected when trying to resolve the AuthenticationManager. Please ensure you have configured authentication."); } } } beanNames = Collections.emptySet(); } } } }