• SpringSecurity - 启动流程分析(七)



    活动地址:CSDN21天学习挑战赛

    前言

    在上篇文章 SpringSecurity - 启动流程分析(六) 中,我们知道了 UsernamePasswordAuthenticationFilter 会通过 DaoAuthenticationProvider 中的 authenticate() 方法完成认证,其实是通过父类 AbstractUserDetailsAuthenticationProviderauthenticate() 方法

    public Authentication authenticate(Authentication authentication)
    			throws AuthenticationException {
    		...
    
    		if (user == null) {
    			cacheWasUsed = false;
    
    			try {
    				// 核心流程,由子类实现
    				user = retrieveUser(username,
    						(UsernamePasswordAuthenticationToken) authentication);
    			}
    			...
    		}
    }
    
    // 由子类 DaoAuthenticationProvider 实现
    protected abstract UserDetails retrieveUser(String username,
    			UsernamePasswordAuthenticationToken authentication)
    			throws AuthenticationException;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    其中又调用了 DaoAuthenticationProvider 实现的 retrieveUser() 方法。

    protected final UserDetails retrieveUser(String username,
    			UsernamePasswordAuthenticationToken authentication)
    			throws AuthenticationException {
    	prepareTimingAttackProtection();
    	try {
    		// 核心流程
    		UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(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);
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    UserDetailsService

    至于是什么时候初始化的这个 UserDetailsService,在上篇文章中有提到这么一个配置类 InitializeUserDetailsBeanManagerConfigurer,其中有一个内部类用于处理 UserDetailsService 的赋值工作:

    class InitializeUserDetailsManagerConfigurer
    			extends GlobalAuthenticationConfigurerAdapter {
    	@Override
    	public void configure(AuthenticationManagerBuilder auth) throws Exception {
    		if (auth.isConfigured()) {
    			return;
    		}
    		// 从容器中获取 UserDetailsService
    		UserDetailsService userDetailsService = getBeanOrNull(
    				UserDetailsService.class);
    		if (userDetailsService == null) {
    			return;
    		}
    
    		PasswordEncoder passwordEncoder = getBeanOrNull(PasswordEncoder.class);
    		UserDetailsPasswordService passwordManager = getBeanOrNull(UserDetailsPasswordService.class);
    
    		// 初始化 DaoAuthenticationProvider
    		DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
    		provider.setUserDetailsService(userDetailsService);
    		if (passwordEncoder != null) {
    			provider.setPasswordEncoder(passwordEncoder);
    		}
    		if (passwordManager != null) {
    			provider.setUserDetailsPasswordService(passwordManager);
    		}
    		provider.afterPropertiesSet();
    
    		auth.authenticationProvider(provider);
    	}
    
    	...
    }
    
    • 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

    查看 spring.factories 中的自动配置类

    所以要实现自定义认证功能,我们只需要实现 UserDetailsService 接口即可,并且 SpringSecurity 默认给我们初始化了一个 UserDetailsService 的实现类在 IoC 容器 中。我们来看一下 spring-boot-autoconfigure 包中的 spring.factories 中关于 SpringSecurity 的自动配置类:

    在这里插入图片描述

    • SecurityAutoConfiguration
    @Configuration(proxyBeanMethods = false)
    @ConditionalOnClass(DefaultAuthenticationEventPublisher.class)
    // 开启配置参数 SecurityProperties
    @EnableConfigurationProperties(SecurityProperties.class)
    // 条件注入 WebSecurityConfigurerAdapter
    @Import({ SpringBootWebSecurityConfiguration.class, WebSecurityEnablerConfiguration.class,
    		SecurityDataConfiguration.class })
    public class SecurityAutoConfiguration {
    
    	@Bean
    	@ConditionalOnMissingBean(AuthenticationEventPublisher.class)
    	public DefaultAuthenticationEventPublisher authenticationEventPublisher(ApplicationEventPublisher publisher) {
    		return new DefaultAuthenticationEventPublisher(publisher);
    	}
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    查看 SecurityProperties

    @ConfigurationProperties(prefix = "spring.security")
    public class SecurityProperties {
    
    	...
    
    	private User user = new User();
    
    	public User getUser() {
    		return this.user;
    	}
    
    	...
    
    	public static class User {
    
    		/**
    		 * Default user name.
    		 */
    		private String name = "user";
    
    		/**
    		 * Password for the default user name.
    		 */
    		private String password = UUID.randomUUID().toString();
    
    		/**
    		 * Granted roles for the default user name.
    		 */
    		private List<String> roles = new ArrayList<>();
    
    		...
    
    	}
    
    }
    
    • 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

    可以看到,如果配置文件中没有配置 spring.security 相关的值,会默认初始化一个 user,用户名为 user,密码自动生成。

    • UserDetailsServiceAutoConfiguration

    从命名上就可以看出,UserDetailsServiceAutoConfigurationSpringBoot 给我们提供的默认的 UserDetailsService 的自动配置类:

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnClass(AuthenticationManager.class)
    @ConditionalOnBean(ObjectPostProcessor.class)
    @ConditionalOnMissingBean(
    		value = { AuthenticationManager.class, AuthenticationProvider.class, UserDetailsService.class },
    		type = { "org.springframework.security.oauth2.jwt.JwtDecoder",
    				"org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector" })
    public class UserDetailsServiceAutoConfiguration {
    
    	private static final String NOOP_PASSWORD_PREFIX = "{noop}";
    
    	// 正则表达式预编译 
    	private static final Pattern PASSWORD_ALGORITHM_PATTERN = Pattern.compile("^\\{.+}.*$");
    
    	private static final Log logger = LogFactory.getLog(UserDetailsServiceAutoConfiguration.class);
    
    	@Bean
    	@ConditionalOnMissingBean(
    			type = "org.springframework.security.oauth2.client.registration.ClientRegistrationRepository")
    	// 同时注意,这里是 懒加载 的,如果我们自定义了,这个就不会初始化到 IoC 容器中了
    	@Lazy
    	public InMemoryUserDetailsManager inMemoryUserDetailsManager(SecurityProperties properties,
    			ObjectProvider<PasswordEncoder> passwordEncoder) {
    		SecurityProperties.User user = properties.getUser();
    		List<String> roles = user.getRoles();
    		// 这里会初始化一个 InMemoryUserDetailsManager 到 IoC 容器中
    		return new InMemoryUserDetailsManager(
    				User.withUsername(user.getName()).password(getOrDeducePassword(user, passwordEncoder.getIfAvailable()))
    						.roles(StringUtils.toStringArray(roles)).build());
    	}
    
    	// 密码加密
    	private String getOrDeducePassword(SecurityProperties.User user, PasswordEncoder encoder) {
    		String password = user.getPassword();
    		if (user.isPasswordGenerated()) {
    			logger.info(String.format("%n%nUsing generated security password: %s%n", user.getPassword()));
    		}
    		if (encoder != null || PASSWORD_ALGORITHM_PATTERN.matcher(password).matches()) {
    			return password;
    		}
    		return NOOP_PASSWORD_PREFIX + password;
    	}
    
    }
    
    • 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
    • SecurityFilterAutoConfiguration

    至于这个自动配置类,是为了指定 springSecurityFilterChain 这个过滤器的顺序和分发类型的,详细的可以查看 DelegatingFilterProxyRegistrationBean

    AbstractUserDetailsAuthenticationProvider

    通过上面的分析,我们知道了 UserDetailsService 的加载过程,接下来我们返回 DaoAuthenticationProviderauthenticate() 方法继续分析:

    public Authentication authenticate(Authentication authentication)
    			throws AuthenticationException {
    	...
    	// 这个 user 就是通过 UserDetailsService 的 loadUserByUsername() 方法返回的 UserDetails 对象
    	Object principalToReturn = user;
    
    	// 可以通过设置这个值使 pricipal 中设置用户名或用户对象
    	if (forcePrincipalAsString) {
    		principalToReturn = user.getUsername();
    	}
    
    	// 创建认证成功的返回对象
    	return createSuccessAuthentication(principalToReturn, authentication, user);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    认证成功会返回一个 Authentication

    protected Authentication createSuccessAuthentication(Object principal,
    			Authentication authentication, UserDetails user) {
    	UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(
    			principal, authentication.getCredentials(),
    			authoritiesMapper.mapAuthorities(user.getAuthorities()));
    	result.setDetails(authentication.getDetails());
    
    	return result;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    注意: 认证方法的入参也是一个 Authentication !!!,Authentication 贯穿全文啊

  • 相关阅读:
    Redis源码篇(6)——主从复制
    3D激光SLAM:ALOAM---后端lasermapping数据处理低延时性保障操作
    java毕业设计便捷式管理系统mybatis+源码+调试部署+系统+数据库+lw
    【JavaWeb】【瑞吉外卖】分页操作&数据传输转换
    webpack
    机器学习(四)——Lasso线性回归预测构建分类模型(matlab)
    【LeetCode】Day129-组合总和 II
    自动售货机
    mysql指令行登录如何添加mysql.sock的配置?(亲测)
    Qt QtableWidget、QtableView表格删除选中行、删除单行、删除多行
  • 原文地址:https://blog.csdn.net/qiaohao0206/article/details/126407448