前言
- ProviderManager是AuthenticationManager最重要的一个实现类,是整个认证逻辑的入口类
![[SpringSecurity5.6.2源码分析十五]:ProviderManager](https://static.blogweb.cn/article/737ea65c0fb64cc394a697f0d310fe8c.webp)
1. authenticate(...)
- authenticate是ProviderManager的核心方法,也是入口方法
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
//获得认证对象的类型
Class<? extends Authentication> toTest = authentication.getClass();
//通过局部和全局认证管理器认证出现的异常
AuthenticationException lastException = null;
AuthenticationException parentException = null;
//通过局部和全局认证管理器认证,最终保存到安全上下文的认证对象
Authentication result = null;
Authentication parentResult = null;
int currentPosition = 0;
int size = this.providers.size();
for (AuthenticationProvider provider : getProviders()) {
//判断当前认证提供者是否支持这个认证对象
if (!provider.supports(toTest)) {
continue;
}
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.format("Authenticating request with %s (%d/%d)",
provider.getClass().getSimpleName(), ++currentPosition, size));
}
try {
//进行认证
result = provider.authenticate(authentication);
if (result != null) {
//复制详细信息到新的认证对象中
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException | InternalAuthenticationServiceException ex) {
prepareException(ex, authentication);
//如果认证失败是由于无效的帐户状态导致的,则抛出异常,避免轮询执行其他认证提供者
throw ex;
}
catch (AuthenticationException ex) {
lastException = ex;
}
}
//到这就说明局部管理器无法认证,尝试调用父类(全局认证管理器)
if (result == null && this.parent != null) {
try {
//进行认证
parentResult = this.parent.authenticate(authentication);
//两个都有了
result = parentResult;
}
catch (ProviderNotFoundException ex) {
// ignore as we will throw below if no other exception occurred prior to
// calling parent and the parent
// may throw ProviderNotFound even though a provider in the child already
// handled the request
}
catch (AuthenticationException ex) {
parentException = ex;
lastException = ex;
}
}
//如果认证成功
if (result != null) {
//是否在认证成功后清除敏感数据
if (this.eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) {
//比如说清除密码
((CredentialsContainer) result).eraseCredentials();
}
//如果是局部自己就认证成功的,发布一个认证成功事件
if (parentResult == null) {
this.eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}
//如果中途抛出了异常
if (lastException == null) {
//统一包装成一个异常
lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound",
new Object[] { toTest.getName() }, "No AuthenticationProvider found for {0}"));
}
//如果只是局部抛出了,就发布一个认证失败异常
if (parentException == null) {
prepareException(lastException, authentication);
}
throw lastException;
}
- authenticate(...)方法的核心逻辑其实就是一个for循环调用内部的AuthenticationProvider进行认证,如果当前AuthenticationManager中的AuthenticationProvider无法认证,就调用父类(全局认证管理器)进行认证
- 所以说存在子类和父类两个认证管理器
2. AuthenticationProvider
- AuthenticationProvider的实现类比较多,现只介绍默认注册的,其他的会随着对应的过滤器进行介绍
public interface AuthenticationProvider {
/**
* 开始认证
*/
Authentication authenticate(Authentication authentication) throws AuthenticationException;
/**
* 判断是否支持这种认证对象的认证,通常是比较Class对象
*/
boolean supports(Class<?> authentication);
}
2.1 AnonymousAuthenticationProvider
- 由AnonymousConfigurer负责注册,AnonymousConfigurer也是默认注册的配置类
- 分析源码看出无非是判断传入的AnonymousAuthenticationToken中的key是否是正确的
public class AnonymousAuthenticationProvider implements AuthenticationProvider, MessageSourceAware {
/**
* 比较是否是匿名用户过滤器创建的认证对象
*/
private String key;
public AnonymousAuthenticationProvider(String key) {
Assert.hasLength(key, "A Key is required");
this.key = key;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
//判断是否是匿名认证对象
if (!supports(authentication.getClass())) {
return null;
}
//比较key
if (this.key.hashCode() != ((AnonymousAuthenticationToken) authentication).getKeyHash()) {
throw new BadCredentialsException(this.messages.getMessage("AnonymousAuthenticationProvider.incorrectKey",
"The presented AnonymousAuthenticationToken does not contain the expected key"));
}
return authentication;
}
@Override
public boolean supports(Class<?> authentication) {
return (AnonymousAuthenticationToken.class.isAssignableFrom(authentication));
}
}
- AnonymousAuthenticationToken可以理解为认证对象,是在AnonymousAuthenticationFilter中创建的
- 下面就是AnonymousAuthenticationFilter的源码
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
//当前会话没有认证对象的时候,创建一个匿名认证对象
if (SecurityContextHolder.getContext().getAuthentication() == null) {
//创建匿名认证对象
Authentication authentication = createAuthentication((HttpServletRequest) req);
//创建安全上下文
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authentication);
//设置到线程级别的安全上下文策略中
SecurityContextHolder.setContext(context);
if (this.logger.isTraceEnabled()) {
this.logger.trace(LogMessage.of(() -> "Set SecurityContextHolder to "
+ SecurityContextHolder.getContext().getAuthentication()));
}
else {
this.logger.debug("Set SecurityContextHolder to anonymous SecurityContext");
}
}
else {
if (this.logger.isTraceEnabled()) {
this.logger.trace(LogMessage.of(() -> "Did not set SecurityContextHolder since already authenticated "
+ SecurityContextHolder.getContext().getAuthentication()));
}
}
chain.doFilter(req, res);
}
![[SpringSecurity5.6.2源码分析十五]:ProviderManager](https://static.blogweb.cn/article/9bea4766209641ed8a2f1c4d4680a8a4.webp)
- 分析上图和AnonymousAuthenticationFilter的源码就可以得出AnonymousAuthenticationToken是SpringSecurity的一个保底策略
- 确保我们使用SecurityContextHolder.getContext().getAuthentication()至少有一个对象
2.2 DaoAuthenticationProvider
- DaoAuthenticationProvider是借助AuthenticationConfiguration创建的
![[SpringSecurity5.6.2源码分析十五]:ProviderManager](https://static.blogweb.cn/article/f98f010b638449aebe5593cb47f5427b.webp)
- 相比于AnonymousAuthenticationProvider,此类有完整的认证逻辑
2.2.1 authenticate(..)
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
() -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));
String username = determineUsername(authentication);
//标准是否在缓存中,默认是
boolean cacheWasUsed = true;
//尝试从缓存中获取
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
//标记为缓存中没有此用户
cacheWasUsed = false;
try {
//调用UserDetailsService拿到UserDetails
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException ex) {
this.logger.debug("Failed to find user '" + username + "'");
//是否隐藏异常类型
if (!this.hideUserNotFoundExceptions) {
throw ex;
}
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
}
try {
//认证前检查
this.preAuthenticationChecks.check(user);
//进行密码匹配
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException ex) {
if (!cacheWasUsed) {
throw ex;
}
//到这就说明,进行比较的用户是在缓存中的,那么就从持久化(比如数据库)的地方中读取最新的UserDetails
//然后再进行密码匹配
cacheWasUsed = false;
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
this.preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
}
//认证后检查器
this.postAuthenticationChecks.check(user);
//放入缓存中
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
//认证成功后是否将Principal由原来的UserDetails对象转为用户名
if (this.forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
//创建一个认证成功的认证对象
return createSuccessAuthentication(principalToReturn, authentication, user);
}
- 步骤
- 从缓存中读取UserDetails
- 如果缓存中没有就从特定的地方(数据库)拿到UserDetails
- 认证前检查
- 进行密码匹配
- 一旦抛出异常并且UserDetails是缓存中的,那就从数据库读取再进行一次密码匹配
- 认证后检查器
- 放入缓存中
- 创建认证对象
2.2.2 retrieveUser(...)
- retrieveUser(...):从特定的地方拿到UserDetails(比如说数据库) 如果提供的凭据不正确,可以立即抛出AuthenticationException
@Override
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);
}
}
- 其实就是调用UserDetailsService.loadUserByUsername(...)方法
2.2.3 UserDetailsChecker
- authenticate(..)的执行过程中会使用UserDetailsChecker类,进行认证前后的检查
- UserDetailsChecker有三个实现类
- DefaultPreAuthenticationChecks
- DefaultPostAuthenticationChecks
- ...
2.2.3.1 DefaultPreAuthenticationChecks
- DefaultPreAuthenticationChecks:在认证前检查UserDetails是否被锁定,账户是否可用,账户是否过期
private class DefaultPreAuthenticationChecks implements UserDetailsChecker {
public void check(UserDetails user) {
if (!user.isAccountNonLocked()) {
logger.debug("User account is locked");
throw new LockedException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.locked",
"User account is locked"));
}
if (!user.isEnabled()) {
logger.debug("User account is disabled");
throw new DisabledException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.disabled",
"User is disabled"));
}
if (!user.isAccountNonExpired()) {
logger.debug("User account is expired");
throw new AccountExpiredException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.expired",
"User account has expired"));
}
}
}
2.2.3.2 DefaultPostAuthenticationChecks
- DefaultPostAuthenticationChecks:在认证后检查密码是否过期
private class DefaultPostAuthenticationChecks implements UserDetailsChecker {
public void check(UserDetails user) {
if (!user.isCredentialsNonExpired()) {
logger.debug("User account credentials have expired");
throw new CredentialsExpiredException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.credentialsExpired",
"User credentials have expired"));
}
}
}
2.2.4 additionalAuthenticationChecks(...)
- additionalAuthenticationChecks(...):进行密码匹配
/**
* 进行密码匹配
* @param userDetails 从某地地方(比如说数据库)读取到的确定的UserDetails
* @param authentication 通过用户输入的用户名和密码创建的认证对象
*/
@Override
@SuppressWarnings("deprecation")
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"));
}
}
- 本质上是调用PasswordEncoder进行密码匹配
2.2.4.1 DelegatingPasswordEncoder
- PasswordEncoder的默认实现是DelegatingPasswordEncoder
public final class PasswordEncoderFactories {
private PasswordEncoderFactories() {
}
/**
* 创建默认的密码密码编码器
*/
@SuppressWarnings("deprecation")
public static PasswordEncoder createDelegatingPasswordEncoder() {
String encodingId = "bcrypt";
Map<String, PasswordEncoder> encoders = new HashMap<>();
encoders.put(encodingId, new BCryptPasswordEncoder());
encoders.put("ldap", new org.springframework.security.crypto.password.LdapShaPasswordEncoder());
encoders.put("MD4", new org.springframework.security.crypto.password.Md4PasswordEncoder());
encoders.put("MD5", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("MD5"));
encoders.put("noop", org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder());
encoders.put("SHA-1", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-1"));
encoders.put("SHA-256",
new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-256"));
encoders.put("sha256", new org.springframework.security.crypto.password.StandardPasswordEncoder());
encoders.put("argon2", new Argon2PasswordEncoder());
//可以看到默认使用bcrypt作为密码编码器
return new DelegatingPasswordEncoder(encodingId, encoders);
}
}
- 可以看出SpringSecurity默认是bcrypt作为密码加密算法
- 使用DelegatingPasswordEncoder作为默认的PasswordEncoder,有如下三个方面的好处
- 兼容性:可以帮助许多使用旧密码加密的方式的系统顺利的迁移,它允许一个系统有多种不同的加密方式
- 因为密码是有格式的,比如说密码为123,加密方式为bcrypt,那么加密后的样子可能为{bcrypt}awdmzxc
- 那我们拿到原来的密码就知道了原来的机密方式,然后使用BCryptPasswordEncoder进行密码匹配,然后就可以利用UserDetailsPasswordService将密码更新为新的密码格式了
- 便捷性:密码的存储策略不可能一直是某一个数据库,当修改存储策略只需要更改很小一部分就可以实现
- 稳定性:可以方便对密码加密方案进行升级,升级的情况如下
- 更换了加密方案
- 同一个加密方案,比如BCrypt有一个加密强度strength参数,这个发生了改变也会进行升级
2.2.5 createSuccessAuthentication(...)
- 此方法被DaoAuthenticationProvider重写过的,主要就是提供了升级密码的功能
@Override
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication,
UserDetails user) {
//确定是否进行密码升级
boolean upgradeEncoding = this.userDetailsPasswordService != null
&& this.passwordEncoder.upgradeEncoding(user.getPassword());
if (upgradeEncoding) {
String presentedPassword = authentication.getCredentials().toString();
String newPassword = this.passwordEncoder.encode(presentedPassword);
//将密码进行更新
user = this.userDetailsPasswordService.updatePassword(user, newPassword);
}
return super.createSuccessAuthentication(principal, authentication, user);
}
- 然后我们看父类的此方法:
- 此类就是为了创建认证对象,但是出现了一个新的类GrantedAuthoritiesMapper
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication,
UserDetails user) {
//创建认证对象
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(principal,
authentication.getCredentials(),
//这里还会进行权限的映射
this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
//更新认证对象的详细信息,通常是一个WebAuthenticationDetails对象
result.setDetails(authentication.getDetails());
this.logger.debug("Authenticated user");
return result;
}
2.2.6 GrantedAuthoritiesMapper
- 有一种场景哈:比如我们定义A角色有B和C角色,然后管理员用户有A角色,但实际上他是没有B和C角色的
- 针对此场景GrantedAuthoritiesMapper就是应运而生,此类的原理也很简单无非就是将A -> A + B + C
/**
* 权限映射接口
* 比如A角色有B和C的角色,那么{@link org.springframework.security.access.hierarchicalroles.RoleHierarchyAuthoritiesMapper}
* 就负责将A变成 A,B,C然后保存到用户认证对象中
*/
public interface GrantedAuthoritiesMapper {
/**
* 权限转换
*/
Collection<? extends GrantedAuthority> mapAuthorities(Collection<? extends GrantedAuthority> authorities);
}
- 我们就看一个实现RoleHierarchyAuthoritiesMapper
- 这里是利用RoleHierarchy(角色继承器)进行转角色的
public class RoleHierarchyAuthoritiesMapper implements GrantedAuthoritiesMapper {
private final RoleHierarchy roleHierarchy;
public RoleHierarchyAuthoritiesMapper(RoleHierarchy roleHierarchy) {
this.roleHierarchy = roleHierarchy;
}
@Override
public Collection<? extends GrantedAuthority> mapAuthorities(Collection<? extends GrantedAuthority> authorities) {
return this.roleHierarchy.getReachableGrantedAuthorities(authorities);
}
}
- 然后讲下RoleHierarchy的实现RoleHierarchyImpl的转换角色原理
private Map<String, Set<GrantedAuthority>> rolesReachableInOneStepMap = null;
private Map<String, Set<GrantedAuthority>> rolesReachableInOneOrMoreStepsMap = null;
- rolesReachableInOneStepMap:
- A -> B
- B -> C
- C -> D,E,F
- 通过如上的结构就可以得出A有B的角色,B有C的角色,C有D,E,F的角色,所有说A有B,C,D,E,F角色
- rolesReachableInOneOrMoreStepsMap:
- A -> B,C,D,E,F
- B -> C,D,E,F
- C -> D,E,F
- 同理
- 通过这两个Map就可以知道角色继承的角色了