Spring Security configures a filter, and so does the volume

Posted by shrive22 on Mon, 21 Feb 2022 16:46:35 +0100

In the past, Pangge took you to realize verification code authentication with Spring Security filter. Today, let's improve the configuration of verification code authentication, which is more in line with the design style of Spring Security and more internal.

CaptchaAuthenticationFilter is implemented by imitating UsernamePasswordAuthenticationFilter. Similarly, since the configuration of UsernamePasswordAuthenticationFilter is completed by FormLoginConfigurer, it should also be able to imitate FormLoginConfigurer and write a configuration class CaptchaAuthenticationFilterConfigurer to configure CaptchaAuthenticationFilter.

public final class FormLoginConfigurer<H extends HttpSecurityBuilder<H>> extends
        AbstractAuthenticationFilterConfigurer<H, FormLoginConfigurer<H>, UsernamePasswordAuthenticationFilter> {
 
    // ellipsis
}    

AbstractAuthenticationFilterConfigurer

FormLoginConfigurer looks a little complicated, but the inheritance relationship is not complicated. It only inherits AbstractAuthenticationFilterConfigurer.

public abstract class AbstractAuthenticationFilterConfigurer<B extends HttpSecurityBuilder<B>, T extends AbstractAuthenticationFilterConfigurer<B, T, F>, F extends AbstractAuthenticationProcessingFilter>
        extends AbstractHttpConfigurer<T, B> {
}    

In theory, let's imitate and inherit this class, but you'll find that this method doesn't work. Because AbstractAuthenticationFilterConfigurer can only be used internally in Spring Security, customization is not recommended. The reason is that it finally adds a filter to HttpSecurity using HttpSecurity Addfilter (filter) method, which can only be used with built-in filter (see FilterOrderRegistration). After understanding this mechanism, we can only abstract it one more level to transform its parent class abstracthttpconfigurator.

Transformation process

B in abstractauthenticationfilterconfigurer < B, t, f > actually refers to HttpSecurity, so this should be retained;

The configurationcapt class does not need to be configured directly to the level of authenticationcapt, so it does not need to be configured directly to the level of authenticationcapt; The same reason is not required for F. it is clear that CaptchaAuthenticationFilter does not need to be generalized. In this way, the configuration class structure of CaptchaAuthenticationFilter can be defined as follows:

public class CaptchaAuthenticationFilterConfigurer<H extends HttpSecurityBuilder<H>> extends AbstractHttpConfigurer<CaptchaAuthenticationFilterConfigurer<H>, H> {
    // No more generalization and concretization 
    private final CaptchaAuthenticationFilter authFilter;
    // Specific authentication code user service
    private CaptchaUserDetailsService captchaUserDetailsService;
    // Verification code processing service
    private CaptchaService captchaService;
    // Policy for saving authentication request details 
    private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource;
    // By default, the save request authentication successful processor is used 
    private SavedRequestAwareAuthenticationSuccessHandler defaultSuccessHandler = new SavedRequestAwareAuthenticationSuccessHandler();
    // Authentication successful processor
    private AuthenticationSuccessHandler successHandler = this.defaultSuccessHandler;
     // Login authentication endpoint
    private LoginUrlAuthenticationEntryPoint authenticationEntryPoint;
    // Customize page 
    private boolean customLoginPage;
    // Login page
    private String loginPage;
    // Login success url
    private String loginProcessingUrl;
    // Authentication failure processor
    private AuthenticationFailureHandler failureHandler;
    // Whether the authentication path is released
    private boolean permitAll;
    //  Authentication failed url
    private String failureUrl;

    /**
     * Creates a new instance with minimal defaults
     */
    public CaptchaAuthenticationFilterConfigurer() {
        setLoginPage("/login/captcha");
        this.authFilter = new CaptchaAuthenticationFilter();
    }

    public CaptchaAuthenticationFilterConfigurer<H> formLoginDisabled() {
        this.formLoginEnabled = false;
        return this;
    }

    public CaptchaAuthenticationFilterConfigurer<H> captchaUserDetailsService(CaptchaUserDetailsService captchaUserDetailsService) {
        this.captchaUserDetailsService = captchaUserDetailsService;
        return this;
    }

    public CaptchaAuthenticationFilterConfigurer<H> captchaService(CaptchaService captchaService) {
        this.captchaService = captchaService;
        return this;
    }

    public CaptchaAuthenticationFilterConfigurer<H> usernameParameter(String usernameParameter) {
        authFilter.setUsernameParameter(usernameParameter);
        return this;
    }

    public CaptchaAuthenticationFilterConfigurer<H> captchaParameter(String captchaParameter) {
        authFilter.setCaptchaParameter(captchaParameter);
        return this;
    }

    public CaptchaAuthenticationFilterConfigurer<H> parametersConverter(Converter<HttpServletRequest, CaptchaAuthenticationToken> converter) {
        authFilter.setConverter(converter);
        return this;
    }
    @Override
    public void init(H http) throws Exception {
        updateAuthenticationDefaults();
        updateAccessDefaults(http);
        registerDefaultAuthenticationEntryPoint(http);
        // The default page filter is disabled here. If you want to customize the login page, you can implement it yourself, which may conflict with FormLogin
        // initDefaultLoginFilter(http);
        // Write the corresponding Provider to HttpSecurity during init
        initProvider(http);
    }
     @Override
    public void configure(H http) throws Exception {
        
        //Here, the method of pre inserting filter is used instead
         http.addFilterBefore(filter, LogoutFilter.class);
    }
    
     // Other methods are the same as AbstractAuthenticationFilterConfigurer
}  

In fact, it imitates the style of AbstractAuthenticationFilterConfigurer and its implementation class, and puts the configuration items aside. It is worth mentioning here that the configuration of CaptchaService can also be found in Spring IoC (refer to the getBeanOrNull method, which can be seen everywhere in Spring Security. It is recommended to learn from it). This is more flexible and can be configured from the method and injected automatically.

    private void initProvider(H http) {

        ApplicationContext applicationContext = http.getSharedObject(ApplicationContext.class);
        // Go to Spring IoC to get CaptchaUserDetailsService without configuring it
        if (captchaUserDetailsService == null) {
            captchaUserDetailsService = getBeanOrNull(applicationContext, CaptchaUserDetailsService.class);
        }
        // Go to Spring IoC to get CaptchaService without configuring it
        if (captchaService == null) {
            captchaService = getBeanOrNull(applicationContext, CaptchaService.class);
        } 
        // Initialize Provider
        CaptchaAuthenticationProvider captchaAuthenticationProvider = this.postProcess(new CaptchaAuthenticationProvider(captchaUserDetailsService, captchaService));
        // Will be added to the registration list of ProviderManager
        http.authenticationProvider(captchaAuthenticationProvider);
    }

Configuration class effect

Let's take a look at the configuration effect of CaptchaAuthenticationFilterConfigurer:

    @Bean
    SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http, UserDetailsService userDetailsService) throws Exception {


        http.csrf().disable()
                .authorizeRequests()
                .mvcMatchers("/foo/**").access("hasAuthority('ROLE_USER')")
                .anyRequest().authenticated()
                .and()
                // All abstracthttpconfigurers can join HttpSecurity through the apply method
                .apply(new CaptchaAuthenticationFilterConfigurer<>())
                // Configure the verification code processing service, which is directly true here to facilitate testing
                .captchaService((phone, rawCode) -> true)
                // Get the verification code through the mobile phone number. In order to facilitate the direct writing, the actual phone and username are mapped  
                .captchaUserDetailsService(phone -> userDetailsService.loadUserByUsername("felord"))
                // The default authentication successfully jumps to the / path, where the authentication information is directly returned to json
                .successHandler((request, response, authentication) -> {
                // The authentication information is returned in JSON
                    ServletServerHttpResponse servletServerHttpResponse = new ServletServerHttpResponse(response);
                    MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter();
                           mappingJackson2HttpMessageConverter.write(authentication, MediaType.APPLICATION_JSON,servletServerHttpResponse);
                });

        return http.build();
    }

Is it a lot more elegant, which solves many difficulties and complications of configuring filters yourself. Learning must imitate. First imitate success, then analyze and think about why it is successful, and finally form your own creativity. Don't be fooled by some strange concepts. Some transformations don't need to understand the details in depth.

Pay attention to the official account: Felordcn for more information

Personal blog: https://felord.cn

Topics: Java