SpringSecurity的配置分析
在分析SpringSecurity前,基于多年前使用SpringSecurity和近年来使用Shiro的经验, SpringSecurity这些年在发展和SpringBoot整合之后,也逃不出以下的一些套路:
1. 提供一个AuthenticationManager,用于登录认证
2. 提供一个web的过滤器链,用于保证web请求的安全处理,这个过滤器链中会包括判断用户是否登录,处理用户的认证请求,基于URL的路径检查用户是否能够访问特定的请求等过滤器
3. 提供一个获取用户数据的接口实现
4. 提供一个方法拦截器,用于基于方法的细粒度的授权控制
以上的1、2、3项都是必要的配置,4是增强级配置可以没有
下面我们来看下 SpringSecurity 如何来完成上述1、2、3的配置
一. 上文提到过 org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter 这个类,springboot只要类路径下能找到 WebSecurityConfigurer 接口的实现类后,就不会加载相应的默认配置。 通过重写一些方法来覆盖默认的一些配置
@Configuration
@EnableWebSecurity
public class FormLoginSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/**").hasRole("USER").and().formLogin(); //拦截所有URL的访问,将不具备USER角色的用户将被导航到登录页
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("user").password("password").roles("USER"); //基于内存的用户认证
}
}
从上面这个例子可以看出,我们自己实现的子类能够做这些事:
- 基于url的路径检查哪些当前的访问是不是具备需要的权限(角色),如果不具备相应的角色,则将其导航至表单认证页面
- 基于AuthenticationManagerBuilder 调用其不同方法实现不同的认证机制
这样基本能解决授权和认证,但实际使用时我们不会用例子中的内存数据去完成用户认证,而是通过访问后台的数据库等方式去完成认证,所以还需要配置合适的认证管理器AuthenticationManager和合适的获取用户数据的接口UserDetailsService的实现,下面是AuthenticationManager接口和UserDetailsService的源码:
public interface AuthenticationManager { Authentication authenticate(Authentication authentication) throws AuthenticationException; }
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; // 用于通过用户名获取用户信息
}
二 . 我们发现除了上文提到的SecurityAutoConfiguration自动配置类外,Springboot 还在 spring.factorys 提供UserDetailsServiceAutoConfiguration类,这个配置类中配置了
UserDetailsService接口的bean实现UserDetailsService, 这是一个基于内存用户数据的实现,当然这是一个默认配置,所以我们只要提供了UserDetailsService接口的bean配置,就将覆盖默认的获取用户的方式
@Configuration @ConditionalOnClass(AuthenticationManager.class) @ConditionalOnBean(ObjectPostProcessor.class) @ConditionalOnMissingBean({ AuthenticationManager.class, AuthenticationProvider.class, UserDetailsService.class }) public class UserDetailsServiceAutoConfiguration { private static final String NOOP_PASSWORD_PREFIX = "{noop}"; private static final Pattern PASSWORD_ALGORITHM_PATTERN = Pattern .compile("^\\{.+}.*$"); private static final Log logger = LogFactory .getLog(UserDetailsServiceAutoConfiguration.class); @Bean @ConditionalOnMissingBean(type = "org.springframework.security.oauth2.client.registration.ClientRegistrationRepository") @Lazy public InMemoryUserDetailsManager inMemoryUserDetailsManager( SecurityProperties properties, ObjectProvider<PasswordEncoder> passwordEncoder) { SecurityProperties.User user = properties.getUser(); List<String> roles = user.getRoles(); return new InMemoryUserDetailsManager(User.withUsername(user.getName()) .password(getOrDeducePassword(user, passwordEncoder.getIfAvailable())) .roles(StringUtils.toStringArray(roles)).build()); } private String getOrDeducePassword(SecurityProperties.User user, PasswordEncoder encoder) { String password = user.getPassword(); if (user.isPasswordGenerated()) { logger.info(String.format("%n%nUsing generated security password: %s%n", user.getPassword())); } if (encoder != null || PASSWORD_ALGORITHM_PATTERN.matcher(password).matches()) { return password; } return NOOP_PASSWORD_PREFIX + password; } }
三. 通过上述的分析,认证的用户数据来源和基于对url资源的访问授权检查的配置都有了,就欠缺一个过滤器链了,可以得出这个过滤器链也是有默认配置的,在上文中介绍过
SecurityAutoConfiguration这个配置类会导入三个配置类 SpringBootWebSecurityConfiguration.class , WebSecurityEnablerConfiguration.class, SecurityDataConfiguration. 上文分析了SpringBootWebSecurityConfiguration,现在分析下 WebSecurityEnablerConfiguration ,还是看源码:
@Configuration @ConditionalOnBean(WebSecurityConfigurerAdapter.class) @ConditionalOnMissingBean(name = BeanIds.SPRING_SECURITY_FILTER_CHAIN) @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) @EnableWebSecurity public class WebSecurityEnablerConfiguration { }
通过源码发现引入了 @EnableWebSecurity ,再看EnableWebSecurity源码:
@Retention(value = java.lang.annotation.RetentionPolicy.RUNTIME) @Target(value = { java.lang.annotation.ElementType.TYPE }) @Documented @Import({ WebSecurityConfiguration.class, SpringWebMvcImportSelector.class, OAuth2ImportSelector.class }) @EnableGlobalAuthentication @Configuration public @interface EnableWebSecurity { /** * Controls debugging support for Spring Security. Default is false. * @return if true, enables debug support with Spring Security */ boolean debug() default false; }
又发现导入了 WebSecurityConfiguration.class ,在这个类中有如下配置:
/** * Creates the Spring Security Filter Chain * @return the {@link Filter} that represents the security filter chain * @throws Exception */ @Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME) public Filter springSecurityFilterChain() throws Exception { boolean hasConfigurers = webSecurityConfigurers != null && !webSecurityConfigurers.isEmpty(); if (!hasConfigurers) { WebSecurityConfigurerAdapter adapter = objectObjectPostProcessor .postProcess(new WebSecurityConfigurerAdapter() { }); webSecurity.apply(adapter); } return webSecurity.build(); }
这也就是springSecurity 默认帮我们配置的过滤器链的bean ,先通过写一个单元测试,看看这个默认帮我们配置的过滤器链的bean 里面都包含了哪些过滤器:
@RunWith(SpringRunner.class) @SpringBootTest(classes=App.class) public class FilterChainProxyTest { @Autowired FilterChainProxy chain ; @Test public void test() { SecurityFilterChain filterChain = chain.getFilterChains().get(0) ; List<Filter> filters = filterChain.getFilters() ; for(Filter filter:filters) { System.out.println( filter.getClass().getSimpleName() ); } } }
执行后输出:
WebAsyncManagerIntegrationFilter
SecurityContextPersistenceFilter
HeaderWriterFilter
CsrfFilter
LogoutFilter
UsernamePasswordAuthenticationFilter
DefaultLoginPageGeneratingFilter
DefaultLogoutPageGeneratingFilter
BasicAuthenticationFilter
RequestCacheAwareFilter
SecurityContextHolderAwareRequestFilter
AnonymousAuthenticationFilter
SessionManagementFilter
ExceptionTranslationFilter
FilterSecurityInterceptor
默认竟然在链中配置了15个过滤器,已经考虑得很周全了, 所以这个bean一般情况下我们不需要去自定义配置,我们可以通过对WebSecurityConfigurerAdapter 中一些方法的重写来改变这个过滤器链中的内容,从而达到定制的目的就可以了。