有的时候松哥会和大家分享一些 Spring Security 的冷门用法,不是为了显摆,只是心愿大家可能从不同的角度加深对 Spring Security 的了解,这些冷门的用法十分有助于大家了解 Spring Security 的外部工作原理。我原本能够纯正的去讲源码,讲原理,然而那样太干燥了,所以我会尽量通过一些小的案例来帮忙大家了解源码,这些案例的目标只是为了帮忙大家了解 Spring Security 源码,仅此而已!所以请大家不要和我抬杠这些用户定义形式没用!
好啦,我明天要给大家表演一个绝活,就是花式定义用户对象。心愿大家通过这几个案例,可能更好的了解 ProviderManager 的工作机制。
本文内容和上篇文章【深刻了解 AuthenticationManagerBuilder【源码篇】】内容强关联,所以强烈建议先学习下上篇文章内容,再来看本文,就会好了解很多。
1. 绝活一
先来看如下一段代码:
@Configuration
public class SecurityConfig {
@Bean
UserDetailsService us() {InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("sang").password("{noop}123").roles("admin").build());
return manager;
}
@Configuration
@Order(1)
static class DefaultWebSecurityConfig extends WebSecurityConfigurerAdapter {UserDetailsService us1() {InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("javaboy").password("{noop}123").roles("admin", "aaa", "bbb").build());
return manager;
}
@Override
protected void configure(HttpSecurity http) throws Exception {http.antMatcher("/foo/**")
.authorizeRequests()
.anyRequest().hasRole("admin")
.and()
.formLogin()
.loginProcessingUrl("/foo/login")
.permitAll()
.and()
.userDetailsService(us1())
.csrf().disable();
}
}
@Configuration
@Order(2)
static class DefaultWebSecurityConfig2 extends WebSecurityConfigurerAdapter {UserDetailsService us2() {InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("江南一点雨").password("{noop}123").roles("user", "aaa", "bbb").build());
return manager;
}
@Override
protected void configure(HttpSecurity http) throws Exception {http.antMatcher("/bar/**")
.authorizeRequests()
.anyRequest().hasRole("user")
.and()
.formLogin()
.loginProcessingUrl("/bar/login")
.permitAll()
.and()
.csrf().disable()
.userDetailsService(us2());
}
}
}
看过后面文章(Spring Security 居然能够同时存在多个过滤器链?)的小伙伴应该明确,这里松哥定义了两个过滤器链,这个置信大家都能了解,不了解的话,参考 Spring Security 居然能够同时存在多个过滤器链?一文。
然而大家留神,在每一个过滤器链中,我都提供了一个 UserDetailsService 实例,而后在 configure(HttpSecurity http) 办法中,配置这个 UserDetailsService 实例。除了每一个过滤器链中都配置一个 UserDetailsService 之外,我还提供了一个 UserDetailsService 的 Bean,所以这里前前后后相当于一共有三个用户,那么咱们登录时候,应用哪个用户能够登录胜利呢?
先说论断:
- 如果登录地址是 /foo/login,那么通过 sang 和 javaboy 两个用户能够登录胜利。
- 如果登录地址是 /bar/login,那么通过 sang 和 江南一点雨 两个用户能够登录胜利。
也就是说,那个全局的,公共的 UserDetailsService 总是无效的,而针对不同过滤器链配置的 UserDetailsService 则只针对以后过滤器链失效。
松哥这里为了不便,应用了基于内存的 UserDetailsService,当然你也能够替换为基于数据库的 UserDetailsService。
那么接下来咱们就来剖析一下,为什么是这个样子?
1.1 源码剖析
1.1.1 全局 AuthenticationManager
首先大家留神,尽管我定义了两个过滤器链,然而在两个过滤器链的定义中,我都没有重写 configure(AuthenticationManagerBuilder auth) 办法,联合上篇文章,没有重写这个办法,就意味著 AuthenticationConfiguration 中提供的全局 AuthenticationManager 是无效的,也就是说,零碎默认提供的 AuthenticationManager 将作为其余部分 AuthenticationManager 的 parent。
那么咱们来看下全局的 AuthenticationManager 配置都配了啥?
public AuthenticationManager getAuthenticationManager() throws Exception {if (this.authenticationManagerInitialized) {return this.authenticationManager;}
AuthenticationManagerBuilder authBuilder = this.applicationContext.getBean(AuthenticationManagerBuilder.class);
if (this.buildingAuthenticationManager.getAndSet(true)) {return new AuthenticationManagerDelegator(authBuilder);
}
for (GlobalAuthenticationConfigurerAdapter config : globalAuthConfigurers) {authBuilder.apply(config);
}
authenticationManager = authBuilder.build();
if (authenticationManager == null) {authenticationManager = getAuthenticationManagerBean();
}
this.authenticationManagerInitialized = true;
return authenticationManager;
}
全局的配置中,有一步就是遍历 globalAuthConfigurers,遍历全局的 xxxConfigurer,并进行配置。全局的 xxxConfigurer 一共有三个,别离是:
- EnableGlobalAuthenticationAutowiredConfigurer
- InitializeUserDetailsBeanManagerConfigurer
- InitializeAuthenticationProviderBeanManagerConfigurer
其中 InitializeUserDetailsBeanManagerConfigurer,看名字就是用来配置 UserDetailsService 的,咱们来看下:
@Order(InitializeUserDetailsBeanManagerConfigurer.DEFAULT_ORDER)
class InitializeUserDetailsBeanManagerConfigurer
extends GlobalAuthenticationConfigurerAdapter {
@Override
public void init(AuthenticationManagerBuilder auth) throws Exception {auth.apply(new InitializeUserDetailsManagerConfigurer());
}
class InitializeUserDetailsManagerConfigurer
extends GlobalAuthenticationConfigurerAdapter {
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {if (auth.isConfigured()) {return;}
UserDetailsService userDetailsService = getBeanOrNull(UserDetailsService.class);
if (userDetailsService == null) {return;}
PasswordEncoder passwordEncoder = getBeanOrNull(PasswordEncoder.class);
UserDetailsPasswordService passwordManager = getBeanOrNull(UserDetailsPasswordService.class);
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);
if (passwordEncoder != null) {provider.setPasswordEncoder(passwordEncoder);
}
if (passwordManager != null) {provider.setUserDetailsPasswordService(passwordManager);
}
provider.afterPropertiesSet();
auth.authenticationProvider(provider);
}
}
}
能够看到,InitializeUserDetailsBeanManagerConfigurer 中定义了外部类,在其内部类的 configure 办法中,通过 getBeanOrNull 去从容器中查找 UserDetailsService 实例,查找到之后,创立 DaoAuthenticationProvider,并最终配置给 auth 对象。
这里的 getBeanOrNull 办法从容器中查找到的,实际上就是 Spring 容器中的 Bean,也就是咱们一开始配置了 sang 用户的那个 Bean,这个 Bean 被交给了全局的 AuthenticationManager,也就是所有部分 AuthenticationManager 的 parent。
1.1.2 部分 AuthenticationManager
通过上篇文章的学习,小伙伴们晓得了所有 HttpSecurity 在构建的过程中,都会传递一个部分的 AuthenticationManagerBuilder 进来,这个部分的 AuthenticationManagerBuilder 一旦传进来就存入了共享对象中,当前须要用的时候再从共享对象中取出来,局部代码如下所示:
public HttpSecurity(ObjectPostProcessor<Object> objectPostProcessor,
AuthenticationManagerBuilder authenticationBuilder,
Map<Class<?>, Object> sharedObjects) {super(objectPostProcessor);
Assert.notNull(authenticationBuilder, "authenticationBuilder cannot be null");
setSharedObject(AuthenticationManagerBuilder.class, authenticationBuilder);
// 省略
}
private AuthenticationManagerBuilder getAuthenticationRegistry() {return getSharedObject(AuthenticationManagerBuilder.class);
}
所以,咱们在 HttpSecurity 中配置 UserDetailsService,实际上是给这个 AuthenticationManagerBuilder 配置的:
public HttpSecurity userDetailsService(UserDetailsService userDetailsService)
throws Exception {getAuthenticationRegistry().userDetailsService(userDetailsService);
return this;
}
也就是部分 AuthenticationManager。
至此,整个流程就很清晰了。
松哥再联合上面这张图给大家解释下:
每一个过滤器链都会绑定一个本人的 ProviderManager(即 AuthenticationManager 的实现),而每一个 ProviderManager 中都通过 DaoAuthenticationProvider 持有一个 UserDetailsService 对象,你能够简略了解为一个 ProviderManager 治理了一个 UserDetailsService,当咱们开始认证的时候,首先由过滤器链所持有的部分 ProviderManager 去认证,要是认证失败了,则调用 ProviderManager 的 parent 再去认证,此时就会用到全局 AuthenticationManager 所持有的 UserDetailsService 对象了。
联合一开始的案例,例如你的登录地址是 /foo/login
,如果你的登录用户是 sang/123,那么先去 HttpSecurity 的部分 ProviderManager 中去验证,后果验证失败(部分的 ProviderManager 中对应的用户是 javaboy),此时就会进入部分 ProviderManager 的 parent 中去认证,也就是全局认证,全局的 ProviderManager 中对应的用户就是 sang 了,此时就认证胜利。
可能有点绕,这个过程大家联合上篇文章认真品一品。
2. 绝活二
再次批改 SecurityConfig 的定义,如下:
@Configuration
public class SecurityConfig {
@Bean
UserDetailsService us() {InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("sang").password("{noop}123").roles("admin").build());
return manager;
}
@Configuration
@Order(1)
static class DefaultWebSecurityConfig extends WebSecurityConfigurerAdapter {UserDetailsService us1() {InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("javaboy").password("{noop}123").roles("admin", "aaa", "bbb").build());
return manager;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(us1());
}
@Override
protected void configure(HttpSecurity http) throws Exception {http.antMatcher("/foo/**")
.authorizeRequests()
.anyRequest().hasRole("admin")
.and()
.formLogin()
.loginProcessingUrl("/foo/login")
.permitAll()
.and()
.csrf().disable();
}
}
@Configuration
@Order(2)
static class DefaultWebSecurityConfig2 extends WebSecurityConfigurerAdapter {UserDetailsService us2() {InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("江南一点雨").password("{noop}123").roles("user", "aaa", "bbb").build());
return manager;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(us2());
}
@Override
protected void configure(HttpSecurity http) throws Exception {http.antMatcher("/bar/**")
.authorizeRequests()
.anyRequest().hasRole("user")
.and()
.formLogin()
.loginProcessingUrl("/bar/login")
.permitAll()
.and()
.csrf().disable();
}
}
}
和后面相比,这段代码的外围变动,就是我重写了 configure(AuthenticationManagerBuilder auth)
办法,依据上篇文章的介绍,重写了该办法之后,全局的 AuthenticationMananger 定义就生效了,也就意味着 sang 这个用户定义生效了,换言之,无论是 /foo/login
还是 /bar/login
,应用 sang/123 当初都无奈登录了。
在每一个 HttpSecurity 过滤器链中,我都重写了 configure(AuthenticationManagerBuilder auth)
办法,并且重新配置了 UserDetailsService,这个重写,相当于我在定义 parent 级别的 ProviderManager。而每一个 HttpSecurity 过滤器链则不再蕴含 UserDetailsService。
当用户登录时,先去找到 HttpSecurity 过滤器链中的 ProviderManager 去认证,后果认证失败,而后再找到 ProviderManager 的 parent 去认证,就胜利了。
3. 小结
在理论开发中,这样配置你简直不会见到,然而下面两个案例,能够让你更好的了解 Spring Security 的认证过程,小伙伴们能够认真品一品~
好啦,本文就先说这么多,案例下载地址 https://github.com/lenve/spring-security-samples
如果小伙伴们感觉有播种,记得点个在看激励下松哥哦~