Spring Security配置個過濾器也這麼卷

碼農小胖哥發表於2022-02-21

以前胖哥帶大家用Spring Security過濾器實現了驗證碼認證,今天我們來改良一下驗證碼認證的配置方式,更符合Spring Security的設計風格,也更加內卷。

CaptchaAuthenticationFilter是通過模仿UsernamePasswordAuthenticationFilter實現的。同樣的道理,由於UsernamePasswordAuthenticationFilter的配置是由FormLoginConfigurer來完成的,應該也能模仿一下FormLoginConfigurer,寫一個配置類CaptchaAuthenticationFilterConfigurer去配置CaptchaAuthenticationFilter

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

AbstractAuthenticationFilterConfigurer

FormLoginConfigurer看起來有點複雜,不過繼承關係並不複雜,只繼承了AbstractAuthenticationFilterConfigurer

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

理論上我們模仿一下,也繼承一下這個類,但是你會發現這種方式行不通。因為AbstractAuthenticationFilterConfigurer只能Spring Security內部使用,不建議自定義。原因在於它最終向HttpSecurity新增過濾器使用的是HttpSecurity.addFilter(Filter)方法,這個方法只有內建過濾器(參見FilterOrderRegistration)才能使用。瞭解了這個機制之後,我們只能往上再抽象一層,去改造其父類AbstractHttpConfigurer

改造過程

AbstractAuthenticationFilterConfigurer<B,T,F>中的B是實際指的HttpSecurity,因此這個要保留;

T指的是它本身的實現,我們配置CaptchaAuthenticationFilter不需要下沉一層到FormLoginConfigurer這個繼承級別,直接在AbstractAuthenticationFilterConfigurer這個繼承級別實現即可,因此T這裡指的就是需要配置類本身,也不需要再抽象化,因此是不需要的;同樣的原因F也不需要,很明確是CaptchaAuthenticationFilter,不需要再泛化。這樣CaptchaAuthenticationFilter的配置類結構可以這樣定義:

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
}  

其實就是模仿AbstractAuthenticationFilterConfigurer及其實現類的風格把用的配置項實現一邊。這裡值得一提的是CaptchaService的配置也可以從Spring IoC中查詢(參考getBeanOrNull方法,這個方法在Spring Security中隨處可見,建議借鑑),這樣更加靈活,既能從方法配置也能自動注入。

    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);
    }

配置類效果

我們來看看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();
    }

是不是要優雅很多,解決了你自己配置過濾器的很多疑難雜症。學習一定要模仿,先模仿成功,然後再分析思考為什麼會模仿成功,最後形成自己的創造力。千萬不要被一些陌生概念唬住,有些改造是不需要去深入瞭解細節的。

關注公眾號:Felordcn 獲取更多資訊

個人部落格:https://felord.cn

相關文章