Spring Security认证器实现过程详解

 更新时间:2022年06月24日 08:44:55   作者:阿弱  
一些权限框架一般都包含认证器和决策器,前者处理登陆验证,后者处理访问资源的控制,这篇文章主要介绍了Spring Security认证器实现过程,需要的朋友可以参考下

一些权限框架一般都包含认证器和决策器,前者处理登陆验证,后者处理访问资源的控制

Spring Security的登陆请求处理如图

下面来分析一下是怎么实现认证器的

拦截请求

首先登陆请求会被UsernamePasswordAuthenticationFilter拦截,这个过滤器看名字就知道是一个拦截用户名密码的拦截器

主要的验证是在attemptAuthentication()方法里,他会去获取在请求中的用户名密码,并且创建一个该用户的上下文,然后在去执行一个验证过程

String username = this.obtainUsername(request);
String password = this.obtainPassword(request);
//创建上下文
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);

可以看看UsernamePasswordAuthenticationToken这个类,他是继承了AbstractAuthenticationToken,然后这个父类实现了Authentication

由这个类的方法和属性可得知他就是存储用户验证信息的,认证器的主要功能应该就是验证完成后填充这个类

回到UsernamePasswordAuthenticationToken中,在上面创建的过程了可以发现

public UsernamePasswordAuthenticationToken(Object principal,Object credentials){
    super(null);
    this.principal=principal;
    this.credentials=credentials;
    //还没认证
    setAuthenticated(false);
}

还有一个super(null)的处理,因为刚进来是还不知道有什么权限的,设置null是初始化一个空的权限

//权限利集合
private final Collection<GrantedAuthority> authorities;
//空的集合
public static final List<GrantedAuthority> NO_AUTHORITIES = Collections.emptyList();
//初始化
if (authorities == null) {
    this.authorities = AuthorityUtils.NO_AUTHORITIES;
    return;
}

那么后续认证完还会把权限设置尽量,此时可以看UsernamePasswordAuthenticationToken的另一个重载构造器

//认证完成
public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
    Collection<? extends GrantedAuthority> authorities) {
    super(authorities);
    this.principal = principal;
    this.credentials = credentials;
    super.setAuthenticated(true); // must use super, as we override
}

在看源码的过程中,注释一直在强调这些上下文的填充和设置都应该是由AuthenticationManager或者AuthenticationProvider的实现类去操作

验证过程

接下来会把球踢给AuthenticationManager,但他只是个接口

/**
 * Attempts to authenticate the passed {@link Authentication} object, returning a
 * fully populated <code>Authentication</code> object (including granted authorities)
 * if successful.
 **/
public interface AuthenticationManager {
    Authentication authenticate(Authentication authentication)
            throws AuthenticationException;
}

注释也写的很清楚了,认证完成后会填充Authentication

接下来会委托给ProviderManager,因为他实现了AuthenticationManager

刚进来看authenticate()方法会发现他先遍历了一个List<AuthenticationProvider>集合

/**
 * Indicates a class can process a specific Authentication 
 **/
public interface AuthenticationProvider {
    Authentication authenticate(Authentication authentication)
            throws AuthenticationException;
    //支不支持特定类型的authentication
    boolean supports(Class<?> authentication);
}

实现这个类就可以处理不同类型的Authentication,比如上边的UsernamePasswordAuthenticationToken,对应的处理类是AbstractUserDetailsAuthenticationProvider,为啥知道呢,因为在这个supports()

public boolean supports(Class<?> authentication) {
		return (UsernamePasswordAuthenticationToken.class
				.isAssignableFrom(authentication));
}

注意到这个是抽象类,实际的处理方法是在他的子类DaoAuthenticationProvider里,但是最重要的authenticate()方法子类好像没有继承,看看父类是怎么实现这个方法的

首先是继续判断Authentication是不是特定的类

 Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
     () -> messages.getMessage(
     "AbstractUserDetailsAuthenticationProvider.onlySupports",
     "Only UsernamePasswordAuthenticationToken is supported"));

查询根据用户名用户,这次就是到了子类的方法了,因为这个方法是抽象的

 user=retrieveUser(username,
     (UsernamePasswordAuthenticationToken)authentication);

接着DaoAuthenticationProvider会调用真正实现查询用户的类UserDetailsService

UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);

UserDetailsService这个类信息就不陌生了,我们一般都会去实现这个类来自定义查询用户的方式,查询完后会返回一个UserDetails,当然也可以继承这个类来扩展想要的字段,主要填充的是权限信息和密码

检验用户,如果获取到的UserDetails是null,则抛异常,不为空则继续校验

//检验用户合法性
preAuthenticationChecks.check(user);
//校验密码
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);

第一个教育是判断用户的合法性,就是判断UserDetails里的几个字段

//账号是否过期
boolean isAccountNonExpired();
//账号被锁定或解锁状态。
boolean isAccountNonLocked();
//密码是否过期
boolean isCredentialsNonExpired();
//是否启用
boolean isEnabled();

第二个则是由子类实现的,判断从数据库获取的密码和请求中的密码是否一致,因为用的登陆方式是根据用户名称登陆,所以有检验密码的步骤

 String presentedPassword = authentication.getCredentials().toString();
 if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
     logger.debug("Authentication failed: password does not match stored value");
     throw new BadCredentialsException(messages.getMessage(
     "AbstractUserDetailsAuthenticationProvider.badCredentials",
     "Bad credentials"));
 }

需要主要的是请求中的密码是被加密过的,所以从数据库获取到的密码也应该是被加密的

注意到当完成校验的时候会把信息放入缓存

//当没有从缓存中获取到值时,这个字段会被设置成false
if (!cacheWasUsed) {
			this.userCache.putUserInCache(user);
 }
 //下次进来的时候回去获取
 UserDetails user = this.userCache.getUserFromCache(username);

如果是从缓存中获取,也是会走检验逻辑的

最后完成检验,并填充一个完整的Authentication

return createSuccessAuthentication(principalToReturn, authentication, user);

由上述流程来看,Security的检验过程还是比较清晰的,通过AuthenticationManager来委托给ProviderManager,在通过具体的实现类来处理请求,在这个过程中,将查询用户的实现和验证代码分离开来

整个过程看着像是策略模式,后边将变化的部分抽离出来,实现解耦

返回完整的Authentication

前边提到的认证成功会调用createSuccessAuthentication()方法,里边的内容很简单

UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(
     principal, authentication.getCredentials(),
     authoritiesMapper.mapAuthorities(user.getAuthorities()));
     result.setDetails(authentication.getDetails());
public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
        Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        this.credentials = credentials;
        super.setAuthenticated(true); // must use super, as we override
        }

这次往supe里放了权限集合,父类的处理是判断里边的权限有没有空的,没有则转换为只读集合

for (GrantedAuthority a : authorities) {
    if (a == null) {
        throw new IllegalArgumentException(
        "Authorities collection cannot contain any null elements");
    }
}
ArrayList<GrantedAuthority> temp = new ArrayList<>(
authorities.size());
temp.addAll(authorities);
this.authorities = Collections.unmodifiableList(temp);

收尾工作

回到ProviderManager里的authenticate方法,当我们终于从

result = provider.authenticate(authentication);

走出来时,后边还有什么操作

1.将返回的用户信息负责给当前的上下文

  if (result != null) {
   	copyDetails(authentication, result);
   	break;
   }

2.删除敏感信息

((CredentialsContainer) result).eraseCredentials();

这个过程会将一些字段设置为null,可以实现eraseCredentials()方法来自定义需要删除的信息

最后返回到UsernamePasswordAuthenticationFilter中通过过滤

结论

这就是Spring Security实现认证的过程了

通过实现自己的上下文Authentication和处理类AuthenticationProvider以及具体的查询用户的方法就可以自定义自己的登陆实现
具体可以看Spring Security自定义认证器

    到此这篇关于Spring Security认证器实现过程详解的文章就介绍到这了,更多相关Spring Security认证器内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

    相关文章

    • Spring Boot 访问安全之认证和鉴权详解

      Spring Boot 访问安全之认证和鉴权详解

      这篇文章主要介绍了Spring Boot 访问安全之认证和鉴权,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
      2021-11-11
    • java 基础知识之网络通信(TCP通信、UDP通信、多播以及NIO)总结

      java 基础知识之网络通信(TCP通信、UDP通信、多播以及NIO)总结

      这篇文章主要介绍了java 基础知识之网络通信总结的相关资料,包括TCP通信、UDP通信、多播以及NIO,需要的朋友可以参考下
      2017-03-03
    • java基础--自己动手实现一个LRU

      java基础--自己动手实现一个LRU

      这篇文章主要介绍了运用方案如何实现LUR,文章中通过代码讲解的非常详细,对大家的工作或学习有一定的参考价值,感兴趣的朋友可以参考一下
      2021-08-08
    • java.util.Date与java.sql.Date的区别

      java.util.Date与java.sql.Date的区别

      这篇文章主要介绍了java.util.Date与java.sql.Date的区别的相关资料,需要的朋友可以参考下
      2015-07-07
    • Spring Boot2.X国际化文件编写配置

      Spring Boot2.X国际化文件编写配置

      这篇文章主要介绍了Spring Boot2.X国际化文件编写配置,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
      2020-02-02
    • jvm中指定时区信息user.timezone问题及解决方式

      jvm中指定时区信息user.timezone问题及解决方式

      同一份程序使用时间LocalDateTime类型,在国内和国外部署后,返回的时间信息前端使用出问题,这篇文章主要介绍了jvm中指定时区信息user.timezone问题及解决方法,需要的朋友可以参考下
      2023-02-02
    • 值得收藏的2017年Java开发岗位面试题

      值得收藏的2017年Java开发岗位面试题

      这篇文章主要为大家推荐一份值得收藏的2017年Java开发岗位面试题,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
      2017-08-08
    • 带你深入理解MyBatis缓存机制

      带你深入理解MyBatis缓存机制

      缓存是一般的ORM框架都会提供的功能,目的就是提升查询的效率和减少数据库的压力,跟Hibernate 一样,MyBatis 也有一级缓存和二级缓存,并且预留了集成第三方缓存的接口,这篇文章主要给大家介绍了关于MyBatis缓存机制的相关资料,需要的朋友可以参考下
      2021-10-10
    • Spring Boot实现功能的统一详解

      Spring Boot实现功能的统一详解

      这篇文章主要介绍了Spring Boot统一功能的处理,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
      2022-06-06
    • Java Math.round函数详解

      Java Math.round函数详解

      这篇文章主要介绍了Java Math.round函数详解,本篇文章通过简要的案例,讲解了该项技术的了解与使用,以下就是详细内容,需要的朋友可以参考下
      2021-08-08

    最新评论