软件编程
位置:首页>> 软件编程>> java编程>> Spring Security认证的完整流程记录

Spring Security认证的完整流程记录

作者:Pseudocode  发布时间:2021-12-15 13:04:03 

标签: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 中,供后续使用。

来源:https://juejin.cn/post/7054569250307964958

0
投稿

猜你喜欢

手机版 软件编程 asp之家 www.aspxhome.com