Spring Security认证的完整流程记录

 更新时间:2022年01月21日 12:44:55   作者:Pseudocode  
相信大伙对Spring Security这个框架又爱又恨,爱它的强大,恨它的繁琐,下面这篇文章主要给大家介绍了关于Spring Security认证流程的相关资料,文中通过实例代码介绍的非常详细,需要的朋友可以参考下

前言

本文以用户名/密码验证方式为例,讲解 Spring Security 的认证流程,在此之前,需要你了解 Spring Security 用户名/密码认证的基本配置。

Spring Security 是基于过滤器的,通过一层一层的过滤器,处理认证的流程,拦截非法请求。

认证上下文的持久化

处于最前面的过滤器叫做 SecurityContextPersistenceFilter,Spring Security 是通过 Session 来存储认证信息的,这个过滤器的 doFilter 方法在每次请求中只执行一次,作用就是,在请求时,将 Session 中的 SecurityContext 放到当前请求的线程中(如果有),在响应时,检查县城中是否有 SecurityContext,有的话将其放入 Session。可以理解为将 SecurityContext 进行 Session 范围的持久化。

认证信息的封装

接着进入 UsernamePasswordAuthenticationFilter,这是基于用户名/密码认证过程中的主角之一。

默认情况下,这个过滤器会匹配路径为 /login 的 POST 请求,也就是 Spring Security 默认的用户名和密码登录的请求路径。

这里最关键的代码是 attemptAuthentication 方法(由 doFilter 方法调用),源码如下:

@Override
public Authentication attemptAuthentication ( HttpServletRequest request, HttpServletResponse response )
 throws AuthenticationException {
 if ( this.postOnly && !request.getMethod () .equals ( "POST" )) {
 throw new AuthenticationServiceException ( "Authentication method not supported: " + request.getMethod ()) ;
   }
 String username = obtainUsername ( request ) ;
   username = ( username != null ) ? username : "";
   username = username.trim () ;
   String password = obtainPassword ( request ) ;
   password = ( password != null ) ? password : "";
   UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken ( username, password ) ;
   // Allow subclasses to set the "details" property
   setDetails ( request, authRequest ) ;
   return this.getAuthenticationManager () .authenticate ( authRequest ) ;
 }

attemptAuthentication 方法代码的第 12 行,使用从 request 中获取到的用户名和密码,构建了一个 UsernamePasswordAuthenticationToken 对象,我们可以看到这个构造方法的代码,非常简单:

public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
    super((Collection)null);
    this.principal = principal;
    this.credentials = credentials;
    this.setAuthenticated(false);
}

只是保存了用户名和密码的引用,并且将认证状态设置为 false,因为此时只是封装了认证信息,还没有进行认证。

我们再回到 attemptAuthentication 的代码,在方法的最后一行,将创建好的认证信息,传递给了一个 AuthenticationManager 进行认证。这里实际工作的是 AuthenticationManager 的实现类 ProviderManager

查找处理认证的 Provider 类

进入 ProviderManager 可以从源码中找到 authenticate 方法,代码比较长,我就不贴在这里了,你可以自行查找,我简述一下代码中的逻辑。

ProviderManager 本身不执行认证操作,它管理着一个 AuthenticationProvider 列表,当需要对一个封装好的认证信息进行认证操作的时候,它会将认证信息和它管理者的 Provider 们,逐一进行匹配,找到合适的 Provider 处理认证的具体工作。

可以这样理解,ProviderManager 是一个管理者,管理着各种各样的 Provider。当有工作要做的时候,它从来都不亲自去做,而是把不同的工作,分配给不同的 Provider 去操作。

最后,它会将 Provider 的工作成果(已认证成功的信息)返回,或者抛出异常。

那么,它是怎么将一个认证信息交给合适的 Provider 的呢?

在上一部分中,我们说到,认证信息被封装成了一个 UsernamePasswordAuthenticationToken,它是Authentication 的子类,ProviderManager 会将这个认证信息的类型,传递个每个 Provider 的 supports 方法,由 Provider 来告诉 ProviderManager 它是不是支持这个类型的认证信息。

认证逻辑

在 Spring Security 内置的 Provider 中,与 UsernamePasswordAuthenticationToken 对应的 Provider 是 DaoAuthenticationProviderauthenticate 方法在它的父类 AbstractUserDetailsAuthenticationProvider 中。我们来看它的 authenticate 方法:

@Override
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 {
 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;
      }
 // There was a problem, so try again after checking
      // we're using latest data (i.e. not from the cache)
      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;
   if ( this.forcePrincipalAsString ) {
 principalToReturn = user.getUsername () ;
   }
 return createSuccessAuthentication ( principalToReturn, authentication, user ) ;
 }

代码比较长,我们说要点:

  • 代码第 12 行,通过 retrieveUser 方法,获得 UserDetails 信息,这个方法的具体实现,可以在 DaoAuthenticationProvider 中找到,主要是通过 UserDetailsService 的 loadUserByUsername 方法,查找系统中的用户信息。
  • 代码第 25 行,通过 preAuthenticationChecks.check 方法,进行了认证前的一些校验。校验的具体实现可以在 DefaultPreAuthenticationChecks 内部类中找到,主要是判断用户是否锁定、是否可用、是否过期。
  • 代码第 26 行,通过 additionalAuthenticationChecks 方法,对用户名和密码进行了校验。具体实现可以在 DaoAuthenticationProvider 中找到。
  • 代码第 39 行,通过 postAuthenticationChecks.check 方法,校验了密码是否过期。具体实现可以在 DefaultPostAuthenticationChecks 内部类中找到。
  • 最后,如果以上校验和认证都没有问题,则通过 createSuccessAuthentication 方法,创建成功的认证信息,并返回。此时,就成功通过了认证。

在最后的 createSuccessAuthentication 方法中,会创建一个新的 UsernamePasswordAuthenticationToken 认证信息,这个新的认证信息的认证状态为 true。表示这是一个已经通过的认证。

这个认证信息会返回到 UsernamePasswordAuthenticationFilter 中,并作为 attemptAuthentication 方法的结果。

doFilter 方法中,会根据认证成功或失败的结果,调用相应的 Handler 类进行后续的处理,最后,认证的信息也会被保存在 SecurityContext 中,供后续使用。

总结

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

相关文章

  • Spring集成事务代码实例

    Spring集成事务代码实例

    这篇文章主要介绍了Spring集成事务代码实例,pring事务的本质其实就是数据库对事务的支持,使用JDBC的事务管理机制,就是利用java.sql.Connection对象完成对事务的提交,需要的朋友可以参考下
    2023-10-10
  • 使用Java发送带附件的附件的示例

    使用Java发送带附件的附件的示例

    这篇文章主要介绍了使用Java发送带附件的附件的方法,使用到了JavaMail这个API,需要的朋友可以参考下
    2015-11-11
  • IntelliJ IDEA进行中文汉化的详细教程(附图文讲解)

    IntelliJ IDEA进行中文汉化的详细教程(附图文讲解)

    今天为大家带来的是 IntelliJ IDEA 中文汉化教程以及中文插件包下载教程,经常收到小伙伴在后台给我留言,问 IDEA 怎么进行中文汉化,因为很多小伙伴是刚入门 Java,看到 IDEA 菜单全英文有些不太适应,需要的朋友可以参考下
    2024-12-12
  • Red Hat 安装JDK与IntelliJ IDEA的详细过程

    Red Hat 安装JDK与IntelliJ IDEA的详细过程

    YUM是基于Red Hat的Linux发行版的一个强大而用户友好的包管理工具,这篇文章主要介绍了Red Hat安装JDK与IntelliJ IDEA,需要的朋友可以参考下
    2023-08-08
  • Java使用正则表达式进行匹配且对匹配结果逐个替换

    Java使用正则表达式进行匹配且对匹配结果逐个替换

    这篇文章主要介绍了Java使用正则表达式进行匹配且对匹配结果逐个替换,文章围绕主题展开详细的内容戒杀,具有一定的参考价值,需要的小伙伴可以参考一下
    2022-09-09
  • XML Web 服务 Eclipse实现sun-jaxws.xml文件的方法

    XML Web 服务 Eclipse实现sun-jaxws.xml文件的方法

    在sun-jaxws.xml文件,可以配置endpoint、handler-chain等内容,在这个文件中配置的内容会覆盖在Java代码中使用注解属性配置的的内容,本文给大家介绍的非常详细,感兴趣的朋友一起看看吧
    2023-11-11
  • Java实现批量导出导入数据及附件文件zip包

    Java实现批量导出导入数据及附件文件zip包

    这篇文章主要为大家详细介绍了Java实现批量导出导入数据及附件文件zip包的方法,文中的示例代码讲解详细,感兴趣的小伙伴可以了解一
    2022-09-09
  • Java实现并查集

    Java实现并查集

    这篇文章主要为大家详细介绍了Java实现并查集,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2020-07-07
  • SpringBoot接口返回数据脱敏(Mybatis、Jackson)

    SpringBoot接口返回数据脱敏(Mybatis、Jackson)

    有时候,我们接口返回的数据需要做一些处理,有一些敏感数据,本文主要介绍了SpringBoot接口返回数据脱敏(Mybatis、Jackson),具有一定的参考价值,感兴趣的可以了解一下
    2024-07-07
  • 基于redis setIfAbsent的使用说明

    基于redis setIfAbsent的使用说明

    这篇文章主要介绍了基于redis setIfAbsent的使用说明,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-01-01

最新评论