有的时候松哥会和大家分享一些 Spring Security 的冷门用法,不是为了显摆,只是希望大家能够从不同的角度加深对 Spring Security 的理解,这些冷门的用法非常有助于大家理解 Spring Security 的内部工作原理。我本来可以纯粹的去讲源码,讲原理,但是那样太枯燥了,所以我会尽量通过一些小的案例来帮助大家理解源码,这些案例的目的只是为了帮助大家理解 Spring Security 源码,仅此而已!所以请大家不要和我抬杠这些用户定义方式没用!
好啦,我今天要给大家表演一个绝活,就是花式定义用户对象。希望大家通过这几个案例,能够更好的理解 ProviderManager 的工作机制。
本文内容和上篇文章【深入理解 AuthenticationManagerBuilder 【源码篇】 】内容强关联,所以强烈建议先学习下上篇文章内容,再来看本文,就会好理解很多。
1.绝活一 先来看如下一段代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 @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 配置都配了啥?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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 的,我们来看下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 @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 一旦传进来就存入了共享对象中,以后需要用的时候再从共享对象中取出来,部分代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 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 配置的:
1 2 3 4 5 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 的定义,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 @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
如果小伙伴们觉得有收获,记得点个在看鼓励下松哥哦~