Spring Security进阶:如何实现和优化AuthenticationProvider

291 阅读4分钟

AuthenticationProvider是Spring Security中的一个核心接口,用于定义验证用户的协议。它主要负责接收Authentication对象(表示用户凭据)并返回已经认证的Authentication对象,如果凭据有效。如果凭据无效,则应该抛出AuthenticationException。 AuthenticationProvider接口包含两个方法:supports.authenticatesupports方法用于判断当前的AuthenticationProvider是否支持提供的Authentication对象,而authenticate方法则用于执行具体的认证逻辑。

在Spring Security的架构中,AuthenticationManager接收来自HTTP过滤器层的请求,并将认证用户的责任委托给AuthenticationProvider。默认情况下,Spring Security使用ProviderManager类,该类会查询配置的多个AuthenticationProvider,以查看是否有任何一个能够执行认证。这意味着可以配置多个AuthenticationProvider,并且它们会被按声明的顺序查询.

Spring Security提供了多种实现AuthenticationProvider的方式,例如:DaoAuthenticationProvider,它通过UserDetailsService和PasswordEncoder来认证用户名和密码。开发者也可以创建自定义的AuthenticationProvider来实现自己的认证逻辑和用户信息来源.

Spring Security认证流程

在Spring Security中,认证流程通常遵循以下步骤:

  1. 认证请求:用户通过登录表单提交用户名和密码等认证信息。
  2. 创建Authentication对象:Spring Security的SecurityContextHolder会根据提交的信息创建一个Authentication对象。
  3. 认证管理器(AuthenticationManagerSecurityContextHolder中的AuthenticationManager负责处理认证逻辑。
  4. AuthenticationProvider的调用AuthenticationManager会根据配置的AuthenticationProvider来验证Authentication对象。

AuthenticationProvider接口定义

AuthenticationProvider接口定义了两个主要的方法:

  • boolean supports(Class<?> authentication):该方法用于确定AuthenticationProvider是否支持给定类型的Authentication对象。
  • Authentication authenticate(Authentication authentication):该方法是认证逻辑的核心,负责验证Authentication对象中的凭据。

认证过程详解

  1. 提交认证信息:用户提交登录信息(如用户名和密码)。
  2. 创建Authentication对象:Spring Security根据提交的信息创建一个Authentication对象,通常是UsernamePasswordAuthenticationToken
  3. 调用AuthenticationManagerauthenticate方法:这个调用会触发认证过程。
  4. AuthenticationManager遍历AuthenticationProviderAuthenticationManager内部维护了一个AuthenticationProvider列表。对于列表中的每一个AuthenticationProviderAuthenticationManager会调用其supports方法来判断是否能够处理传入的Authentication类型。
  5. AuthenticationProviderauthenticate方法:一旦找到支持该类型的AuthenticationProviderAuthenticationManager会调用它的authenticate方法。
  6. 验证凭据:在authenticate方法内,AuthenticationProvider会根据具体的认证策略来验证凭据。例如,对于用户名和密码,可能会查询数据库来确认凭据的有效性。
  7. 返回认证结果:如果凭据验证成功,AuthenticationProvider会返回一个被赋予了权限信息的Authentication对象;如果失败,则抛出AuthenticationException异常。

Security框架中原始的Provider:客户端校验

public final class ClientSecretAuthenticationProvider implements AuthenticationProvider {
    private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-3.2.1";
    private final Log logger = LogFactory.getLog(getClass());
    private final RegisteredClientRepository registeredClientRepository;
    private final CodeVerifierAuthenticator codeVerifierAuthenticator;
    private PasswordEncoder passwordEncoder;

    /**
     * Constructs a {@code ClientSecretAuthenticationProvider} using the provided parameters.
     *
     * @param registeredClientRepository the repository of registered clients
     * @param authorizationService the authorization service
     */
    public ClientSecretAuthenticationProvider(RegisteredClientRepository registeredClientRepository,
          OAuth2AuthorizationService authorizationService) {
       Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");
       Assert.notNull(authorizationService, "authorizationService cannot be null");
       this.registeredClientRepository = registeredClientRepository;
       this.codeVerifierAuthenticator = new CodeVerifierAuthenticator(authorizationService);
       this.passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

    /**
     * Sets the {@link PasswordEncoder} used to validate
     * the {@link RegisteredClient#getClientSecret() client secret}.
     * If not set, the client secret will be compared using
     * {@link PasswordEncoderFactories#createDelegatingPasswordEncoder()}.
     *
     * @param passwordEncoder the {@link PasswordEncoder} used to validate the client secret
     */
    public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
       Assert.notNull(passwordEncoder, "passwordEncoder cannot be null");
       this.passwordEncoder = passwordEncoder;
    }

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
       OAuth2ClientAuthenticationToken clientAuthentication =
             (OAuth2ClientAuthenticationToken) authentication;

       if (!ClientAuthenticationMethod.CLIENT_SECRET_BASIC.equals(clientAuthentication.getClientAuthenticationMethod()) &&
             !ClientAuthenticationMethod.CLIENT_SECRET_POST.equals(clientAuthentication.getClientAuthenticationMethod())) {
          return null;
       }

       String clientId = clientAuthentication.getPrincipal().toString();
       RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId);
       if (registeredClient == null) {
          throwInvalidClient(OAuth2ParameterNames.CLIENT_ID);
       }

       //省略,感兴趣可以自己去看源码...

       return new OAuth2ClientAuthenticationToken(registeredClient,
             clientAuthentication.getClientAuthenticationMethod(), clientAuthentication.getCredentials());
    }

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

    private static void throwInvalidClient(String parameterName) {
       OAuth2Error error = new OAuth2Error(
             OAuth2ErrorCodes.INVALID_CLIENT,
             "Client authentication failed: " + parameterName,
             ERROR_URI
       );
       throw new OAuth2AuthenticationException(error);
    }

}

如何自定义AuthenticationProvider?

开发者可以根据需要实现自定义的AuthenticationProvider,以支持特殊的认证机制,如OAuth、JWT、自定义数据库查询等。自定义AuthenticationProvider需要实现AuthenticationProvider接口,并在Spring Security配置中注册。

Spring Security支持多种不同的认证方式,每种认证方式对应不同的身份类型。为了支持这些不同的认证方式,每个AuthenticationProvider需要实现supports()方法来表明自己支持的认证类型7。例如,如果一个AuthenticationProvider专门用于短信登录,那么它就需要实现该方法来指定它只支持短信登录的认证方式。

在Spring Security的整体架构中,AuthenticationManager接收来自HTTP过滤器层的请求,并将认证用户的责任委托给AuthenticationProvider。如果请求中的用户不是已知的,则需要进行认证。 默认情况下,Spring Security使用ProviderManager类作为AuthenticationManager的实现,它会委托给一系列配置好的AuthenticationProvider去执行认证操作。