springboot情操陶冶-web配置(九)
承接前文ofollow,noindex" target="_blank">springboot情操陶冶-web配置(八) ,本文在前文的基礎上深入瞭解下WebSecurity類的運作邏輯
WebSecurityConfigurerAdapter
在剖析WebSecurity 的工作邏輯之前,先預熱下springboot security推薦複寫的抽象類WebSecurityConfigurerAdapter ,觀察下其是如何被例項化的,方便後續的深入理解
1.ApplicationContext環境設定
@Autowired public void setApplicationContext(ApplicationContext context) { this.context = context; ObjectPostProcessor<Object> objectPostProcessor = context.getBean(ObjectPostProcessor.class); LazyPasswordEncoder passwordEncoder = new LazyPasswordEncoder(context); // authenticationBuilder = new DefaultPasswordEncoderAuthenticationManagerBuilder(objectPostProcessor, passwordEncoder); localConfigureAuthenticationBldr = new DefaultPasswordEncoderAuthenticationManagerBuilder(objectPostProcessor, passwordEncoder) { @Override public AuthenticationManagerBuilder eraseCredentials(boolean eraseCredentials) { authenticationBuilder.eraseCredentials(eraseCredentials); return super.eraseCredentials(eraseCredentials); } }; }
同前文的認證管理器建立差不多,但這裡細心的可以看到其內部建立了兩個一模一樣的認證管理器物件,目的應該是為了引用AuthenticationConfiguration 物件中的認證管理器作準備,下文會提及
2.引入前文所提的AuthenticationConfiguration 物件
@Autowired public void setAuthenticationConfiguration( AuthenticationConfiguration authenticationConfiguration) { this.authenticationConfiguration = authenticationConfiguration; }
目的是間接呼叫此類獲取對應的認證管理器AuthenticationManager 物件,具體的讀者可自行閱讀
3.共享變數集合,security板塊引入了共享變數,目的就跟快取一樣
private Map<Class<? extends Object>, Object> createSharedObjects() { Map<Class<? extends Object>, Object> sharedObjects = new HashMap<Class<? extends Object>, Object>(); sharedObjects.putAll(localConfigureAuthenticationBldr.getSharedObjects()); sharedObjects.put(UserDetailsService.class, userDetailsService()); sharedObjects.put(ApplicationContext.class, context); sharedObjects.put(ContentNegotiationStrategy.class, contentNegotiationStrategy); sharedObjects.put(AuthenticationTrustResolver.class, trustResolver); return sharedObjects; }
OK,差不多就這樣,開始我們的主角之旅把
WebSecurityConfiguration
根據前文可知,WebSecurity物件的建立與運作都是通過WebSecurityConfiguration 來的,筆者在此處再作下簡單的歸納
1.例項化WebSecurity物件並註冊至ApplicationContext上下文物件中
2.獲取ApplicationContext物件上註冊的型別為WebSecurityConfigurer 的介面集合,存放至WebSecurity的父類AbstractConfiguredSecurityBuilder 的內部屬性configurers(hashmap) 中
3.執行WebSecurity的build() 方法建立Filter過濾鏈
基於上述的結論我們再著重看下第三點的建立過濾鏈是如何完成的,本文側重點也在於此
WebSecurity#build()
build() 方法是由其父類AbstractSecurityBuilder 來完成的,程式碼很簡單
public final O build() throws Exception { // 確保只會被build一次 if (this.building.compareAndSet(false, true)) { this.object = doBuild(); return this.object; } throw new AlreadyBuiltException("This object has already been built"); }
接著跟蹤doBuild() 模板方法,發現是由其子類抽象類AbstractConfiguredSecurityBuilder 來操作的
@Override protected final O doBuild() throws Exception { synchronized (configurers) { buildState = BuildState.INITIALIZING; // init beforeInit(); init(); buildState = BuildState.CONFIGURING; // configure beforeConfigure(); configure(); buildState = BuildState.BUILDING; // build O result = performBuild(); buildState = BuildState.BUILT; return result; } }
大致可以分為三步走,下面筆者就按照上述的三步一一分開剖析
初始化階段
分為beforeInit() 和init() 兩個操作
beforeInit()
beforeInit()初始化前的操作,預設是空的,可供使用者去複寫
init()
init()初始化方法比較有意思了,看下其原始碼
private void init() throws Exception { // WebSecurity內的configurer是WebSecurityConfigurer型別的 Collection<SecurityConfigurer<O, B>> configurers = getConfigurers(); // 遍歷呼叫公用的init()方法,關注此處的this指代WebSecurity物件 for (SecurityConfigurer<O, B> configurer : configurers) { configurer.init((B) this); } // 候補 for (SecurityConfigurer<O, B> configurer : configurersAddedInInitializing) { configurer.init((B) this); } }
好,筆者來看下configurer#init() 方法,此處只針對WebSecurityConfigurer 介面的初始化。即使使用者沒有去實現該介面,springboot也會預設有個類去實現
@ConditionalOnClass(WebSecurityConfigurerAdapter.class) @ConditionalOnMissingBean(WebSecurityConfigurerAdapter.class) @ConditionalOnWebApplication(type = Type.SERVLET) public class SpringBootWebSecurityConfiguration { @Configuration @Order(SecurityProperties.BASIC_AUTH_ORDER) static class DefaultConfigurerAdapter extends WebSecurityConfigurerAdapter { } }
很明顯,就是繼承WebSecurityConfigurerAdapter 類來實現的,那我們看下其init() 方法的操作
public void init(final WebSecurity web) throws Exception { // important final HttpSecurity http = getHttp(); // configure interceptor? web.addSecurityFilterChainBuilder(http).postBuildAction(new Runnable() { public void run() { FilterSecurityInterceptor securityInterceptor = http .getSharedObject(FilterSecurityInterceptor.class); web.securityInterceptor(securityInterceptor); } }); }
上述的程式碼比較關鍵,筆者按兩個小步驟講述一下
1.建立HttpSecurity物件,貼出原始碼仔細分析下
protected final HttpSecurity getHttp() throws Exception { if (http != null) { return http; } // 配置事件發行器 DefaultAuthenticationEventPublisher eventPublisher = objectPostProcessor .postProcess(new DefaultAuthenticationEventPublisher()); localConfigureAuthenticationBldr.authenticationEventPublisher(eventPublisher); // 獲取父級認證管理器,涉及configure(AuthenticationManagerBuilder auth)方法複寫 AuthenticationManager authenticationManager = authenticationManager(); authenticationBuilder.parentAuthenticationManager(authenticationManager); Map<Class<? extends Object>, Object> sharedObjects = createSharedObjects(); // 建立HttpSecurity物件 http = new HttpSecurity(objectPostProcessor, authenticationBuilder, sharedObjects); // 是否遮蔽預設配置,預設為false if (!disableDefaults) { // @formatter:off http .csrf().and() .addFilter(new WebAsyncManagerIntegrationFilter()) .exceptionHandling().and() .headers().and() .sessionManagement().and() .securityContext().and() .requestCache().and() .anonymous().and() .servletApi().and() .apply(new DefaultLoginPageConfigurer<>()).and() .logout(); // @formatter:on ClassLoader classLoader = this.context.getClassLoader(); // 載入spring.factories中以AbstractHttpConfigurer作為Key的型別集合 List<AbstractHttpConfigurer> defaultHttpConfigurers = SpringFactoriesLoader.loadFactories(AbstractHttpConfigurer.class, classLoader); for(AbstractHttpConfigurer configurer : defaultHttpConfigurers) { http.apply(configurer); } } // 供使用者自定義配置HTTP安全配置,預設支援表單和Basic方式提交認證資訊 configure(http); return http; }
程式碼資訊過多,不一一分析了,springboot推薦使用者複寫以下兩個方法
- configure(AuthenticationManagerBuilder auth) 配置認證管理器,使用者資訊讀取方式、加密方式均可通過此方法配置
- configure(HttpSecurity http) 配置http服務,路徑攔截、csrf保護等等均可通過此方法配置
2.將HttpSecurity放入WebSecurity中,細看發現HttpSecurity是用來建立FilterChain過濾鏈的。並嘗試塞入一個FilterSecurityInterceptor 攔截器,預設一般都是會配置的。
配置階段
分為beforeConfigure() 和configure() 階段
beforeConfigure()
配置前的操作,供使用者去複寫
configure()
遍歷其下的所有的WebSecurityConfigurerAdapter 的實現類,統一呼叫configure(WebSecurity web) 配置。很明顯,使用者也可以複寫方法來配置,比如對HttpSecurity預設的配置不滿意,也可以通過此類來複寫之;遮蔽一些URL的訪問等等。
performBuild()
真正的建立Filter物件,別看原始碼很長,其實就一個意思,通過HttpSecurity物件來建立Filter過濾鏈
@Override protected Filter performBuild() throws Exception { Assert.state( !securityFilterChainBuilders.isEmpty(), "At least one SecurityBuilder<? extends SecurityFilterChain> needs to be specified. Typically this done by adding a @Configuration that extends WebSecurityConfigurerAdapter. More advanced users can invoke " + WebSecurity.class.getSimpleName() + ".addSecurityFilterChainBuilder directly"); int chainSize = ignoredRequests.size() + securityFilterChainBuilders.size(); List<SecurityFilterChain> securityFilterChains = new ArrayList<>( chainSize); // 使用者可通過呼叫websecurity.ignoring()方法來遮蔽一些訪問路徑 for (RequestMatcher ignoredRequest : ignoredRequests) { securityFilterChains.add(new DefaultSecurityFilterChain(ignoredRequest)); } for (SecurityBuilder<? extends SecurityFilterChain> securityFilterChainBuilder : securityFilterChainBuilders) { // HttpSecurity#build()會被執行,執行邏輯就跟本文的WebSecurity一模一樣 securityFilterChains.add(securityFilterChainBuilder.build()); } FilterChainProxy filterChainProxy = new FilterChainProxy(securityFilterChains); // 防火牆配置 if (httpFirewall != null) { filterChainProxy.setFirewall(httpFirewall); } filterChainProxy.afterPropertiesSet(); Filter result = filterChainProxy; postBuildAction.run(); return result; }
具體的使用者可自行去閱讀
小結
本文主要講述了WebSecurity與HttpSecurity之間的關係以及如何被建立,其實都是一樣的,它們都是AbstractConfiguredSecurityBuilder 的複寫類,核心都是會執行其中的build() 模板方法來建立過濾鏈。筆者或者讀者只需要複寫其中的幾個重要方法便可實現簡單的安全配置。希望本文對大家有用