3

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

CaptchaAuthenticationFilter is implemented by mimicking UsernamePasswordAuthenticationFilter . For the same reason, since the configuration of UsernamePasswordAuthenticationFilter is done by FormLoginConfigurer , it should 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> {
 
    // 省略
}    

AbstractAuthenticationFilterConfigurer

FormLoginConfigurer looks a bit complicated, but the inheritance relationship is not complicated, only AbstractAuthenticationFilterConfigurer is inherited.

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

In theory, we imitate and inherit this class, but you will find that this method does not work. Because AbstractAuthenticationFilterConfigurer can only be used internally by Spring Security , customization is not recommended. The reason is that it ends up adding a filter to HttpSecurity using the HttpSecurity.addFilter(Filter) method, which is only available for built-in filters (see FilterOrderRegistration ). After understanding this mechanism, we can only abstract one level higher and transform its parent class AbstractHttpConfigurer .

Retrofit process

AbstractAuthenticationFilterConfigurer<B,T,F> in B is actually HttpSecurity , so this should be reserved;

T refers to its own implementation. We configure CaptchaAuthenticationFilter without descending one layer to the inheritance level of FormLoginConfigurer , and can directly implement it at the inheritance level of AbstractAuthenticationFilterConfigurer . Therefore, T here refers to the need to configure the class itself, and no need to abstract it again. Generalization, so it is not needed; for the same reason, F is also not needed, it is clear that it is CaptchaAuthenticationFilter , and it 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> {
    // 不再泛化  具体化 
    private final CaptchaAuthenticationFilter authFilter;
    // 特定的验证码用户服务
    private CaptchaUserDetailsService captchaUserDetailsService;
    // 验证码处理服务
    private CaptchaService captchaService;
    // 保存认证请求细节的策略 
    private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource;
    // 默认使用保存请求认证成功处理器 
    private SavedRequestAwareAuthenticationSuccessHandler defaultSuccessHandler = new SavedRequestAwareAuthenticationSuccessHandler();
    // 认证成功处理器
    private AuthenticationSuccessHandler successHandler = this.defaultSuccessHandler;
     // 登录认证端点
    private LoginUrlAuthenticationEntryPoint authenticationEntryPoint;
    // 是否 自定义页面 
    private boolean customLoginPage;
    // 登录页面
    private String loginPage;
    // 登录成功url
    private String loginProcessingUrl;
    // 认证失败处理器
    private AuthenticationFailureHandler failureHandler;
    // 认证路径是否放开
    private boolean permitAll;
    //  认证失败的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);
        // 这里禁用默认页面过滤器 如果你想自定义登录页面 可以自行实现 可能和FormLogin冲突
        // initDefaultLoginFilter(http);
        // 把对应的Provider也在init时写入HttpSecurity
        initProvider(http);
    }
     @Override
    public void configure(H http) throws Exception {
        
        //这里改为使用前插过滤器方法
         http.addFilterBefore(filter, LogoutFilter.class);
    }
    
     // 其它方法 同AbstractAuthenticationFilterConfigurer
}  

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

    private void initProvider(H http) {

        ApplicationContext applicationContext = http.getSharedObject(ApplicationContext.class);
        // 没有配置CaptchaUserDetailsService就去Spring IoC获取
        if (captchaUserDetailsService == null) {
            captchaUserDetailsService = getBeanOrNull(applicationContext, CaptchaUserDetailsService.class);
        }
        // 没有配置CaptchaService就去Spring IoC获取
        if (captchaService == null) {
            captchaService = getBeanOrNull(applicationContext, CaptchaService.class);
        } 
        // 初始化 Provider
        CaptchaAuthenticationProvider captchaAuthenticationProvider = this.postProcess(new CaptchaAuthenticationProvider(captchaUserDetailsService, captchaService));
        // 会增加到ProviderManager的注册列表中
        http.authenticationProvider(captchaAuthenticationProvider);
    }

Configure class effects

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()
                // 所有的 AbstractHttpConfigurer 都可以通过apply方法加入HttpSecurity
                .apply(new CaptchaAuthenticationFilterConfigurer<>())
                // 配置验证码处理服务   这里直接true 方便测试
                .captchaService((phone, rawCode) -> true)
                // 通过手机号去拿验证码,这里为了方便直接写死了,实际phone和username做个映射  
                .captchaUserDetailsService(phone -> userDetailsService.loadUserByUsername("felord"))
                // 默认认证成功跳转到/路径  这里改造成把认证信息直接返回json
                .successHandler((request, response, authentication) -> {
                // 这里把认证信息以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, and solves many intractable problems of configuring filters yourself. Learning must imitate, first imitate successfully, then analyze and think about why imitation is successful, and finally form your own creativity. Don't be fooled by some unfamiliar concepts, some transformations do not require in-depth understanding of the details.

Follow the official account: Felordcn for more information

Personal blog: https://felord.cn


码农小胖哥
3.8k 声望8k 粉丝