花式玩 Spring Security ,这样的用户定义方式你可能没见过!

有的时候松哥会和你们分享一些 Spring Security 的冷门用法,不是为了显摆,只是但愿你们可以从不一样的角度加深对 Spring Security 的理解,这些冷门的用法很是有助于你们理解 Spring Security 的内部工做原理。我原本能够纯粹的去讲源码,讲原理,可是那样太枯燥了,因此我会尽可能经过一些小的案例来帮助你们理解源码,这些案例的目的只是为了帮助你们理解 Spring Security 源码,仅此而已!因此请你们不要和我抬杠这些用户定义方式没用!java

好啦,我今天要给你们表演一个绝活,就是花式定义用户对象。但愿你们经过这几个案例,可以更好的理解 ProviderManager 的工做机制。git

本文内容和上篇文章【深刻理解 AuthenticationManagerBuilder 【源码篇】】内容强关联,因此强烈建议先学习下上篇文章内容,再来看本文,就会好理解不少。github

1.绝活一

先来看以下一段代码:spring

@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,因此这里前先后后至关于一共有三个用户,那么咱们登陆时候,使用哪一个用户能够登陆成功呢?app

先说结论:ide

  • 若是登陆地址是 /foo/login,那么经过 sang 和 javaboy 两个用户能够登陆成功。
  • 若是登陆地址是 /bar/login,那么经过 sang 和 江南一点雨 两个用户能够登陆成功。

也就是说,那个全局的,公共的 UserDetailsService 老是有效的,而针对不一样过滤器链配置的 UserDetailsService 则只针对当前过滤器链生效。oop

松哥这里为了方便,使用了基于内存的 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

若是小伙伴们以为有收获,记得点个在看鼓励下松哥哦~

相关文章
相关标签/搜索