以前胖哥帶大家用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 獲取更多資訊