继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

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

江南一点雨
关注TA
已关注
手记 318
粉丝 1.4万
获赞 3885

有的时候松哥会和大家分享一些 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

如果小伙伴们觉得有收获,记得点个在看鼓励下松哥哦~

打开App,阅读手记
0人推荐
发表评论
随时随地看视频慕课网APP

热门评论

ok

看着不错

查看全部评论