Spring Security学习(七)——父子AuthenticationManager(ProviderManager)

本文分析了SpringSecurity中父子AuthenticationManager的工作原理,展示了如何在不添加DaoAuthenticationProvider的情况下实现认证,以及如何自定义父ProviderManager,结合Redis进行用户验证。重点在于理解其内部机制和应用场景。
摘要由CSDN通过智能技术生成

前言

Spring Security学习(六)——配置多个Provider》有个很奇怪的现象,如果我们不添加DaoAuthenticationProvider到HttpSecurity中,似乎也能够达到类似的效果。那我们为什么要多此一举呢?从文章的效果来看确实是多此一举,但其实这里面暗藏玄机。也引出了本文的父子AuthenticationManager(ProviderManager)的话题。

探究父子AuthenticationManager(ProviderManager)体系

我们在Spring Security源码的ProviderManager(在org.springframework.security.authentication包中)中,在authenticate方法的以下位置断点调试:

 然后查看断点的变量:

目前程序准备处理MyProvider的authenticate方法,目前providers列表中有三个provider,分别是我们添加的MyProvider和DaoAuthenticationProvider,还有一个AnonymousAuthenticationProvider是Spring Security在HttpSecurity初始化时加进去的(参考HttpSecurityConfiguration的httpSecurity方法)。

重点来了,我们还看到parent,里面有个DaoAuthenticationProvider。parent的ProviderManager和里面的DaoAuthenticationProvider又是什么时候加进去的?这个源码上有点复杂,我也不准备大段大段的粘出来(大概率读者一下子很难看懂),提示一下读者,在HttpSecurityConfiguration的httpSecurity方法的这一行:

在蓝色标出的位置设置的父ProviderManager。这个父ProviderManager是AuthenticationConfiguration配置中创建InitializeUserDetailsBeanManagerConfigurer到Spring容器中,然后在Spring Security初始化时调用InitializeUserDetailsBeanManagerConfigurer的configue方法获取并设置ProviderManager,在父ProviderManager中设置DaoAuthenticationProvider也是类似的原理。

读者可以尝试删掉《Spring Securi习(六)——配置多个Provider》WebSecurityConfig中添加DaoAuthenticationProvider到HttpSecurity的代码,再断点调试一下。

父AuthenticationManager的作用

父子AuthenticationManager的机制是这样的:先对子AuthenticationManager中的AuthenticationProvider列表进行逐个匹配,若都无法匹配,则会对父AuthenticationManager中的AuthenticationProvider列表进行逐个匹配。

下面两张图是来自Spring Security官网文档的:

通过第二张图的多个子ProviderManager,我认为父AuthenticationManager的作用就是给多个子ProviderManager一个公共的匹配方式。

多个ProviderManager又是用在什么场景呢?根据Spring Security官网描述,是存在多个SecurityFilterChain,并且存在不同的登陆认证机制时使用。

自定义父ProviderManager

Spring Security学习(六)——配置多个Provider》中直接调用http.authenticationProvider是把Provider加入子ProviderManager中。如果想加入到父ProviderManager中要怎么做呢?

查看过源码后,往父ProviderManager加Provider是比较复杂(当然也不是做不到),所以本次我们的目标是自定义父ProviderManager,加入需要的Provider,然后覆盖原父ProviderManager。

自定义一个新的Provider,从redis中取出账号密码进行比对,新建MyRedisProvider:

@Data
public class MyRedisProvider extends AbstractUserDetailsAuthenticationProvider{

	private UserDetailsServiceImpl userDetailsServiceImpl;
	
	private PasswordEncoder passwordEncoder;
	
	private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword";
	
	private volatile String userNotFoundEncodedPassword;

	@Override
	protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
			throws AuthenticationException {
		prepareTimingAttackProtection();
		try {
			UserDetails loadedUser = userDetailsServiceImpl.loadUserByRedis(username);
			if (loadedUser == null) {
				throw new InternalAuthenticationServiceException(
						"UserDetailsService returned null, which is an interface contract violation");
			}
			return loadedUser;
		}
		catch (UsernameNotFoundException ex) {
			mitigateAgainstTimingAttack(authentication);
			throw ex;
		}
		catch (InternalAuthenticationServiceException ex) {
			throw ex;
		}
		catch (Exception ex) {
			throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
		}
	}

	@Override
	protected void additionalAuthenticationChecks(UserDetails userDetails,
			UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
		if (authentication.getCredentials() == null) {
			this.logger.debug("Failed to authenticate since no credentials provided");
			throw new BadCredentialsException(this.messages
					.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
		}
		String presentedPassword = authentication.getCredentials().toString();
		if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
			this.logger.debug("Failed to authenticate since password does not match stored value");
			throw new BadCredentialsException(this.messages
					.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
		}
	}
	
	private void prepareTimingAttackProtection() {
		if (this.userNotFoundEncodedPassword == null) {
			this.userNotFoundEncodedPassword = this.passwordEncoder.encode(USER_NOT_FOUND_PASSWORD);
		}
	}

	private void mitigateAgainstTimingAttack(UsernamePasswordAuthenticationToken authentication) {
		if (authentication.getCredentials() != null) {
			String presentedPassword = authentication.getCredentials().toString();
			this.passwordEncoder.matches(presentedPassword, this.userNotFoundEncodedPassword);
		}
	}
}

和之前的MyProvider相比,仅仅是更改了第17行userDetailsServiceImpl.loadUserByRedis,从redis读取用户信息。当然在UserDetailsServiceImpl中我们也要加上loadUserByRedis方法:

@Component
public class UserDetailsServiceImpl implements UserDetailsService, InitializingBean{

	@Autowired
	private SysUserService userService;
	
	@Autowired
	private RedisTemplate redisTemplate;
	
	private static Map<String, SysUserEntity> userMap = new HashMap<String, SysUserEntity>();
	
	@Override
	public void afterPropertiesSet() throws Exception {
		SysUserEntity memorySysUser = new SysUserEntity();
		memorySysUser.setUsername("test");
		memorySysUser.setPassword("test###");
		userMap.put("test", memorySysUser);
		
		SysUserEntity redisSysUser = new SysUserEntity();
		redisSysUser.setUsername("admin");
		redisSysUser.setPassword("admin123###");
		redisTemplate.opsForValue().set("admin", redisSysUser);
	}
	
	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		QueryWrapper<SysUserEntity> queryWrapper = new QueryWrapper<SysUserEntity>();
		queryWrapper.eq("username", username);
		queryWrapper.last("limit 1");
		SysUserEntity user = userService.getOne(queryWrapper);
		if(user == null) {
			throw new UsernameNotFoundException("username not found");
		}
		return (new LoginUser(user));
	}
	
	public UserDetails loadUserByMemory(String username) throws UsernameNotFoundException {
		if(StrUtil.isNotEmpty(username)) {
			SysUserEntity user = userMap.get(username);
			if(user == null) {
				throw new UsernameNotFoundException("username not found");
			}
			return (new LoginUser(user));
		}
		return null;
	}
	
	public UserDetails loadUserByRedis(String username) throws UsernameNotFoundException {
		if(StrUtil.isNotEmpty(username)) {
			SysUserEntity user = (SysUserEntity)redisTemplate.opsForValue().get(username);
			if(user == null) {
				throw new UsernameNotFoundException("username not found");
			}
			return (new LoginUser(user));
		}
		return null;
	}
}

上述代码除了增加loadUserByRedis方法,为了在初始化时设置数据,还实现了InitializingBean接口的afterPropertiesSet方法,当然这只是测试使用。

为了使用redis,我们在pom.xml中引入相关依赖:

		<dependency>
		    <groupId>org.springframework.boot</groupId>
		    <artifactId>spring-boot-starter-data-redis</artifactId>
		</dependency>
		<dependency>
			<groupId>org.apache.commons</groupId>
			<artifactId>commons-pool2</artifactId>
		</dependency>

另外要在application.yml文件增加redis配置:

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driverClassName: com.mysql.cj.jdbc.Driver
    druid:
        url: jdbc:mysql://127.0.0.1:3306/security_test?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false&serverTimezone=Asia/Shanghai
        username: root
        password: 
  thymeleaf:
    prefix: classpath:/templates/
  redis:
    host: 127.0.0.1
    port: 6379
    password:
    lettuce:
      pool:
        max-active: 500
        max-idle: 300
        max-wait: 1000
        min-idle: 0
    database: 8

增加一个配置类RedisConfig:

@Configuration
public class RedisConfig {

	@Bean
	public RedisTemplate<String, Serializable> redisTemplate(LettuceConnectionFactory connectionFactory){
		RedisTemplate<String, Serializable> redisTemplate = new RedisTemplate<String, Serializable>();
		redisTemplate.setKeySerializer(new StringRedisSerializer());
		redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
		redisTemplate.setConnectionFactory(connectionFactory);
		return redisTemplate;
	}
}

这样就可以使用RedisTemplate处理序列化的内容。

最后修改一下WebSecurityConfig:

@EnableWebSecurity
public class WebSecurityConfig{
	
	@Bean
	public MyPasswordEncoder PasswordEncoder() {
		return new MyPasswordEncoder();
	}
	
	@Bean
	public AuthenticationManager createParentAuthenticationManager() {
		List<AuthenticationProvider> providerList = new ArrayList<AuthenticationProvider>();
		MyRedisProvider myRedisProvider = new MyRedisProvider();
		myRedisProvider.setPasswordEncoder(PasswordEncoder());
		UserDetailsServiceImpl UserDetailsServiceImpl = SpringUtils.getBean(UserDetailsServiceImpl.class);
		myRedisProvider.setUserDetailsServiceImpl(UserDetailsServiceImpl);
		providerList.add(myRedisProvider);
		AuthenticationManager parentAuthenticationManager = new ProviderManager(providerList);
		return parentAuthenticationManager;
	}

	@Bean
	public SecurityFilterChain formLoginFilterChain(HttpSecurity http) throws Exception {
		http
			.authorizeHttpRequests(authorize -> authorize
				.anyRequest().authenticated()
			)
			.formLogin(Customizer.withDefaults());
		MyProvider myProvider = new MyProvider();
		myProvider.setPasswordEncoder(PasswordEncoder());
		UserDetailsServiceImpl UserDetailsServiceImpl = SpringUtils.getBean(UserDetailsServiceImpl.class);
		myProvider.setUserDetailsServiceImpl(UserDetailsServiceImpl);
		http.authenticationProvider(myProvider);
		
		DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
		daoAuthenticationProvider.setPasswordEncoder(PasswordEncoder());
		daoAuthenticationProvider.setUserDetailsService(UserDetailsServiceImpl);
		http.authenticationProvider(daoAuthenticationProvider);
		
		AuthenticationManagerBuilder authenticationManagerBuilder = http.getSharedObject(AuthenticationManagerBuilder.class);
		ApplicationContext applicationContext = http.getSharedObject(ApplicationContext.class);
		AuthenticationManager parentAuthenticationManager = applicationContext.getBean(AuthenticationManager.class);
		authenticationManagerBuilder.parentAuthenticationManager(parentAuthenticationManager);
		return http.build();
	}
}

9-19行创建自定义的myRedisProvider,配置好加密器、userDetailsService、然后放到新建的ProviderManager中,通过@Bean注解注入到Spring容器中。39-42行从Spring Security上下文中获取9-19行注入Spring容器的ProviderManager并设置为父AuthenticationManager。 

注:之前看过有的文章说直接通过@Bean注入容器就可以了。但是我自己调试过不行,查看了源码父AuthenticationManager默认是new创建的,并没有从容器中获取的逻辑。如果读者有相关的逻辑和实现方式也请进行指正。

之后启动应用,访问/hello路径,然后尝试输入jake/123、test/test、admin/admin123,应该都能通过。

小结

本文主要讲述了父子AuthenticationManager的机制,并且实现了如何自定义父ProviderManager。其实对于一般的应用是没必要这么搞的,如Spring Security官网所说,主要用在多种不同认证体系下。所以本文主要目的还是学习其内部机制为主。

  • 25
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值