1. 程式人生 > 實用技巧 >spring security原始碼分析

spring security原始碼分析

目錄

1.spring security的啟動機制

https://gitee.com/yulewo123/MySecurityDemo.git

為例

1.1.框架介面設計

SecurityBuilder 安全構造者

SecurityConfigurer 安全配置者

他們的結構如下圖所示

​ security框架圖

框架的用法就是通過配置器(SecurityConfigurer)對建造者(SecurityBuilder)進行配置
框架用法是寫一個自定義配置類,繼承WebSecurityConfigurerAdapter,重寫幾個configure()方法


WebSecurityConfigurerAdapter就是Web安全配置器的介面卡物件

// 安全構造者
// 是一個builder構造器,建立並返回一個型別為O的物件
public interface SecurityBuilder<O> {
    O build() throws Exception;
}

//抽象安全構造者
public abstract class AbstractSecurityBuilder<O> implements SecurityBuilder<O> {
	private AtomicBoolean building = new AtomicBoolean();
	private O object;
	public final O build() throws Exception {
		if (this.building.compareAndSet(false, true)) {//限制build()只會執行一次
			this.object = doBuild();
			return this.object;
		}
		throw new AlreadyBuiltException("This object has already been built");
	}
	protected abstract O doBuild() throws Exception;//子類要重寫doBuild()方法
}

//
public abstract class AbstractConfiguredSecurityBuilder<O, B extends SecurityBuilder<O>>
		extends AbstractSecurityBuilder<O> {
    @Override
	protected final O doBuild() throws Exception {
		synchronized (configurers) {
			buildState = BuildState.INITIALIZING;

			beforeInit();//初始化前置處理,protected方法,子類可重寫
			init();//初始化

			buildState = BuildState.CONFIGURING;

			beforeConfigure();//配置前置處理,protected方法,子類可重寫
			configure();//使用SecurityConfigurer對securityBuilder進行配置

			buildState = BuildState.BUILDING;

			O result = performBuild();//執行構造,子類必須要重寫該方法

			buildState = BuildState.BUILT;

			return result;
		}
	}
    //子類必須重寫,可見WebSecurity和HttpSecurity
    protected abstract O performBuild() throws Exception;
    
    //遍歷構造者SecurityBuilder的配置者SecurityConfigurer,執行初始化
    private void init() throws Exception {
		Collection<SecurityConfigurer<O, B>> configurers = getConfigurers();

		for (SecurityConfigurer<O, B> configurer : configurers) {
			configurer.init((B) this);
		}

		for (SecurityConfigurer<O, B> configurer : configurersAddedInInitializing) {
			configurer.init((B) this);
		}
	}
    
    //遍歷構造者SecurityBuilder的配置者SecurityConfigurer,執行配置
	private void configure() throws Exception {
		Collection<SecurityConfigurer<O, B>> configurers = getConfigurers();

		for (SecurityConfigurer<O, B> configurer : configurers) {
			configurer.configure((B) this);
		}
	}
}
//從init()和configure()兩個方法都是遍歷進行處理,發現關鍵是getConfigurers()的來源,即集合屬性AbstractConfiguredSecurityBuilder.configurers的來源,即來源於SecurityConfigurer的配置,具體來源於WebSecurityConfigurerAdapter

//WebSecurity用於構造springSecurityFilterChain->FilterChainProxy
public final class WebSecurity extends
		AbstractConfiguredSecurityBuilder<Filter, WebSecurity> implements
		SecurityBuilder<Filter>, ApplicationContextAware {
    //實現了performBuild方法,用於構造FilterChainProxy
}

//HttpSecurity用於構造FilterChainProxy內的filterchain
public final class HttpSecurity extends
		AbstractConfiguredSecurityBuilder<DefaultSecurityFilterChain, HttpSecurity>
		implements SecurityBuilder<DefaultSecurityFilterChain>,
		HttpSecurityBuilder<HttpSecurity> {
        //實現了performBuild方法,用於構造DefaultSecurityFilterChain
        //DefaultSecurityFilterChain.filters就是filterchain
        //FilterChainProxy.filterChains就是DefaultSecurityFilterChain集合
}
//安全裝配置者
public interface SecurityConfigurer<O, B extends SecurityBuilder<O>> {
	//初始化SecurityBuilder
    void init(B builder) throws Exception;
	//配置SecurityBuilder
	void configure(B builder) throws Exception;
}

//web安全裝配置者,只是介面標識而已並無方法,標識為是web安全
public interface WebSecurityConfigurer<T extends SecurityBuilder<Filter>> extends
		SecurityConfigurer<Filter, T> {
}

//
public abstract class WebSecurityConfigurerAdapter implements
		WebSecurityConfigurer<WebSecurity> {
    //初始化
    @Override
    public void init(final WebSecurity web) throws Exception {
		final HttpSecurity http = getHttp();//獲取HttpSecurity
		web.addSecurityFilterChainBuilder(http).postBuildAction(new Runnable() {
			public void run() {
				FilterSecurityInterceptor securityInterceptor = http
						.getSharedObject(FilterSecurityInterceptor.class);
				web.securityInterceptor(securityInterceptor);
			}
		});
	}
    
    //實現並無具體功能,可以自己重寫
    @Override
    public void configure(WebSecurity web) throws Exception {
	}
    
    //開發者通常要重寫該方法,用於配置filter
    protected void configure(HttpSecurity http) throws Exception {
		http
			.authorizeRequests()
				.anyRequest().authenticated()
				.and()
			.formLogin().and()
			.httpBasic();
	}
    
    //開發者通常要重寫該方法,用於時認證來源(資料庫、記憶體驗證。。。)
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		this.disableLocalConfigureAuthenticationBldr = true;
	}
    
}    

總結:
看到構造者,就看build(); doBuild(); init(); configure(); performBuild();
看到配置者,就看init(); config();

結構圖如前面的【security框架圖】

1.2.spring security的啟動流程

入口是@EnableWebSecurity,該註解在WebSecurityConfigurerAdapter的子類上。

@EnableWebSecurity引入了WebSecurityConfiguration

WebSecurityConfiguration是個配置bean,下面看其方法,方法按照執行順序來寫

@Configuration
public class WebSecurityConfiguration implements ImportAware, BeanClassLoaderAware {
    //建立bean AutowiredWebSecurityConfigurersIgnoreParents
    @Bean
	public static AutowiredWebSecurityConfigurersIgnoreParents autowiredWebSecurityConfigurersIgnoreParents(
			ConfigurableListableBeanFactory beanFactory) {
		return new AutowiredWebSecurityConfigurersIgnoreParents(beanFactory);
	}
    
    //@Value("#{@autowiredWebSecurityConfigurersIgnoreParents.getWebSecurityConfigurers()}"的意思就是執行AutowiredWebSecurityConfigurersIgnoreParents.getWebSecurityConfigurers()獲取型別是WebSecurityConfigurer的bean,因此就是WebSecurityConfigurerAdapter的子類
    //該方法就是注入,給WebSecurityConfiguration.webSecurity賦值為WebSecurity,給WebSecurityConfiguration.webSecurityConfigurers賦值為bean型別是WebSecurityConfigurer的bean,即就是//WebSecurityConfigurerAdapter的子類
    @Autowired(required = false)
	public void setFilterChainProxySecurityConfigurer(
			ObjectPostProcessor<Object> objectPostProcessor,
			@Value("#{@autowiredWebSecurityConfigurersIgnoreParents.getWebSecurityConfigurers()}") List<SecurityConfigurer<Filter, WebSecurity>> webSecurityConfigurers)
			throws Exception {
		webSecurity = objectPostProcessor
				.postProcess(new WebSecurity(objectPostProcessor));
		//省略其它程式碼
		for (SecurityConfigurer<Filter, WebSecurity> webSecurityConfigurer : webSecurityConfigurers) {
			webSecurity.apply(webSecurityConfigurer);//apply方法,即把WebSecurity.configurers賦值為WebSecurityConfigurerAdapter的子類
		}
		this.webSecurityConfigurers = webSecurityConfigurers;//WebSecurityConfigurerAdapter的子類
	}
    
    //構造名稱為springSecurityFilterChain,型別為FilterChainProxy的bean,是個filter,也是spring security的核心過濾器
    @Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)
	public Filter springSecurityFilterChain() throws Exception {
		boolean hasConfigurers = webSecurityConfigurers != null
				&& !webSecurityConfigurers.isEmpty();
		if (!hasConfigurers) {//不執行
			WebSecurityConfigurerAdapter adapter = objectObjectPostProcessor
					.postProcess(new WebSecurityConfigurerAdapter() {
					});
			webSecurity.apply(adapter);
		}
		return webSecurity.build();//執行webSecurity構建,根據security框架圖,依次執行的是builder()->doBuilder()->init()-configure()->configure(B builder)
	}
    
}

下面就看webSecurity.build(),執行堆疊如下:

AbstractSecurityBuilder.build()
AbstractConfiguredSecurityBuilder.doBuild()
AbstractConfiguredSecurityBuilder.init() 下面分析
AbstractConfiguredSecurityBuilder.configure() 下面分析

WebSecurity.performBuild() 下面分析

接著分析webSecurity.init(),即AbstractConfiguredSecurityBuilder.init()

private void init() throws Exception {
		Collection<SecurityConfigurer<O, B>> configurers = getConfigurers();//獲取屬性configurers,對於WebSecuriy來說該屬性值在WebSecurityConfiguration.setFilterChainProxySecurityConfigurer(ObjectPostProcessor<Object>, List<SecurityConfigurer<Filter, WebSecurity>>)方法內已經被賦值為WebSecurityConfigurerAdapter的子類

		for (SecurityConfigurer<O, B> configurer : configurers) {//WebSecurityConfigurerAdapter是個securityConfigure配置類,執行SecurityConfigurer.init()方法
			configurer.init((B) this);//@1,執行開發者實現的WebSecurityConfigurerAdapter的類的Init方法
		}

		for (SecurityConfigurer<O, B> configurer : configurersAddedInInitializing) {//屬性configurersAddedInInitializing通常空,忽略
			configurer.init((B) this);
		}
	}

//分析@1處程式碼,即執行WebSecurityConfigurerAdapter.init(WebSecurity)
public void init(final WebSecurity web) throws Exception {
		final HttpSecurity http = getHttp();//@2,下面看這裡
		web.addSecurityFilterChainBuilder(http).postBuildAction(new Runnable() {//addSecurityFilterChainBuilder就是把該HttpSecurity新增到WebSecurity.securityFilterChainBuilders集合
			public void run() {
				FilterSecurityInterceptor securityInterceptor = http
						.getSharedObject(FilterSecurityInterceptor.class);
				web.securityInterceptor(securityInterceptor);
			}
		});
	}

//分析@2處程式碼,WebSecurityConfigurerAdapter.getHttp()
protected final HttpSecurity getHttp() throws Exception {
		if (http != null) {
			return http;
		}

		DefaultAuthenticationEventPublisher eventPublisher = objectPostProcessor
				.postProcess(new DefaultAuthenticationEventPublisher());
		localConfigureAuthenticationBldr.authenticationEventPublisher(eventPublisher);//屬性localConfigureAuthenticationBldr是在setApplicationContext方內被賦值為DefaultPasswordEncoderAuthenticationManagerBuilder,是個AuthenticationManagerBuilder,也是個securityBuilder

		AuthenticationManager authenticationManager = authenticationManager();//@3,如果WebSecurityConfigurerAdapter子類重寫了void configure(AuthenticationManagerBuilder auth)方法,則會去執行子類的該方法,下面會去分析
		authenticationBuilder.parentAuthenticationManager(authenticationManager);//屬性authenticationBuilder是在setApplicationContext方內被賦值為DefaultPasswordEncoderAuthenticationManagerBuilder,是個AuthenticationManagerBuilder,也是個securityBuilder
		authenticationBuilder.authenticationEventPublisher(eventPublisher);
		Map<Class<? extends Object>, Object> sharedObjects = createSharedObjects();

		http = new HttpSecurity(objectPostProcessor, authenticationBuilder,
				sharedObjects);//建立HttpSecurity物件,該物件是個securityBuilder
		if (!disableDefaults) {//執行
			// @formatter:off
			http
				.csrf().and()//建立CsrfConfigurer這個SecurityConfigurer安全配置類,並快取到HttpSecurity.configurers集合屬性上
				.addFilter(new WebAsyncManagerIntegrationFilter())//建立filter WebAsyncManagerIntegrationFilter並快取到HttpSecurity.filters集合屬性上
				.exceptionHandling().and()//建立ExceptionHandlingConfigurer並快取到HttpSecurity.configurers集合屬性上
				.headers().and()//HeadersConfigurer
				.sessionManagement().and()//SessionManagementConfigurer
				.securityContext().and()//SecurityContextConfigurer
				.requestCache().and()//RequestCacheConfigurer
				.anonymous().and()//AnonymousConfigurer
				.servletApi().and()//ServletApiConfigurer
				.apply(new DefaultLoginPageConfigurer<>()).and()//DefaultLoginPageConfigurer
				.logout();//LogoutConfigurer
			// @formatter:on
			ClassLoader classLoader = this.context.getClassLoader();
			List<AbstractHttpConfigurer> defaultHttpConfigurers =
					SpringFactoriesLoader.loadFactories(AbstractHttpConfigurer.class, classLoader);//載入META-INF/spring.factories檔案內org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer,並快取到HttpSecurity.configurers集合屬性上
            //這裡有個疑問:通過spring.factories檔案可以向FilterChainProxy加入filter,通過HttpSecurity.addFilter(Filter)也可以向FilterChainProxy加入filter,兩者的區別是什麼呢?通常來說個人專案中直接用addFilter方式新增filter,但是如果是提供一個基礎框架,那麼就用的是spring.factories方式,兩者效果一樣。

			for (AbstractHttpConfigurer configurer : defaultHttpConfigurers) {
				http.apply(configurer);//快取到HttpSecurity.configurers集合屬性上
			}
		}
		configure(http);//WebSecurityConfigurerAdapter子類通常會實現void configure(HttpSecurity http),用於配置HttpSecurity,這裡執行子類的方法
		return http;//返回配置好的HttpSecurity,即就是對其屬性賦值了,此時的HttpSecurity屬性configurers都是安全配置類集合,filters就是filter集合
	}

//分析@3處程式碼
protected AuthenticationManager authenticationManager() throws Exception {
		if (!authenticationManagerInitialized) {
			configure(localConfigureAuthenticationBldr);//@5 執行開發者實現的WebSecurityConfigurerAdapter.configure(AuthenticationManagerBuilder)
			if (disableLocalConfigureAuthenticationBldr) {
				authenticationManager = authenticationConfiguration
						.getAuthenticationManager();
			}
			else {
				authenticationManager = localConfigureAuthenticationBldr.build();//@6 構建authenticationManager
			}
			authenticationManagerInitialized = true;
		}
		return authenticationManager;
	}

//分析程式碼@5處,此處是使用者實現 com.zzz.config.MyWebSecurityConfig.configure(AuthenticationManagerBuilder)
//傳入的引數是WebSecurityConfigurerAdapter.localConfigureAuthenticationBldr,即DefaultPasswordEncoderAuthenticationManagerBuilder,是個ProviderManagerBuilder,用於建立ProviderManager
@Override
protected void configure(AuthenticationManagerBuilder builder) throws Exception{
    builder.userDetailsService(dbUserDetailsService);//DefaultPasswordEncoderAuthenticationManagerBuilder.userDetailsService(T)
}
//DefaultPasswordEncoderAuthenticationManagerBuilder.userDetailsService(T),DefaultPasswordEncoderAuthenticationManagerBuilder是個把使用者實現的型別是UserDetailsService的bean儲存到AuthenticationManagerBuilder
//UserDetailsService介面是用於載入指定的資料來源,即驗證的資料來源
@Override
public <T extends UserDetailsService> DaoAuthenticationConfigurer<AuthenticationManagerBuilder, T> userDetailsService(
    T userDetailsService) throws Exception {
    return super.userDetailsService(userDetailsService)//把使用者實現的型別是UserDetailsService的bean儲存到AuthenticationManagerBuilder.defaultUserDetailsService,併為AuthenticationManagerBuilder新增securityConfigurer DaoAuthenticationConfigurer
        .passwordEncoder(this.defaultPasswordEncoder);//設定密碼編碼方式
}

//分析程式碼@6處
//WebSecurityConfigurerAdapter.localConfigureAuthenticationBldr型別是DefaultPasswordEncoderAuthenticationManagerBuilder,是個AuthenticationManagerBuilder,即就是個SecurityBuilder,AuthenticationManagerBuilder用於建立AuthenticationManager、ProviderManager
//那麼localConfigureAuthenticationBldr.build()執行就是build()->doBuild()->init()->configure()->performBuild()
//其中在init()和configure()階段執行的就是DaoAuthenticationConfigurer.init(),DaoAuthenticationConfigurer.configure(AuthenticationManagerBuilder)
//DaoAuthenticationConfigurer.init()無功能
//DaoAuthenticationConfigurer.configure(AuthenticationManagerBuilder)功能就是把DaoAuthenticationConfigurer.provider,即DaoAuthenticationProvider新增到AuthenticationManagerBuilder.authenticationProviders集合儲存。

由此可見,webSecurity.init()方法就是建立HttpSecurity並且對HttpSecurity進行配置,配置HttpSecurity屬性configurers,filters

接著分析webSecurity.configure(),即AbstractConfiguredSecurityBuilder.configure()

//webSecurity配置方法,即AbstractConfiguredSecurityBuilder.configure()
private void configure() throws Exception {
		Collection<SecurityConfigurer<O, B>> configurers = getConfigurers();//獲取屬性configurers,對於WebSecuriy來說該屬性值在WebSecurityConfiguration.setFilterChainProxySecurityConfigurer(ObjectPostProcessor<Object>, List<SecurityConfigurer<Filter, WebSecurity>>)方法內已經被賦值為WebSecurityConfigurerAdapter的子類

		for (SecurityConfigurer<O, B> configurer : configurers) {//WebSecurityConfigurerAdapter是個securityConfigure配置類,執行configure(WebSecurity web)方法
			configurer.configure((B) this);//@1,執行開發者實現的WebSecurityConfigurerAdapter的類的configure(WebSecurity web)方法。通常WebSecurityConfigurerAdapter的子類不會重寫configure(WebSecurity web)方法,因此方法具體沒功能
		}
	}

接著分析WebSecurity.performBuild()

@Override
	protected Filter performBuild() throws Exception {
		int chainSize = ignoredRequests.size() + securityFilterChainBuilders.size();
		List<SecurityFilterChain> securityFilterChains = new ArrayList<>(
				chainSize);
		for (RequestMatcher ignoredRequest : ignoredRequests) {//ignoredRequests是配置的忽略請求
			securityFilterChains.add(new DefaultSecurityFilterChain(ignoredRequest));//如果有配置的忽略請求,則新增DefaultSecurityFilterChain
		}
		for (SecurityBuilder<? extends SecurityFilterChain> securityFilterChainBuilder : securityFilterChainBuilders) {//securityFilterChainBuilders儲存的就是HttpSecurity
			securityFilterChains.add(securityFilterChainBuilder.build());//@4 執行HttpSecurity.build()建立DefaultSecurityFilterChain
		}//如果有多個HttpSecurity,那麼securityFilterChainBuilder.build()建立多個DefaultSecurityFilterChain,每個DefaultSecurityFilterChain包含HttpSecurity配置的一系列filters
		FilterChainProxy filterChainProxy = new FilterChainProxy(securityFilterChains);//建立FilterChainProxy
		if (httpFirewall != null) {
			filterChainProxy.setFirewall(httpFirewall);
		}
		filterChainProxy.afterPropertiesSet();

		Filter result = filterChainProxy;
		if (debugEnabled) {
			logger.warn("\n\n"
					+ "********************************************************************\n"
					+ "**********        Security debugging is enabled.       *************\n"
					+ "**********    This may include sensitive information.  *************\n"
					+ "**********      Do not use in a production system!     *************\n"
					+ "********************************************************************\n\n");
			result = new DebugFilter(filterChainProxy);
		}
		postBuildAction.run();
		return result;
	}

//程式碼@4處分析
//執行就的就是HttpSecurity.build(),HttpSecurity也是個SecurityBuilder,那麼執行HttpSecurity.build()->HttpSecurity.doBuild()->HttpSecurity.init()->HttpSecurity.configure()->WebSecurity.performBuild(),真正有功能就是init、configure、performBuild,下面分析這3個
//HttpSecurity.init()即AbstractConfiguredSecurityBuilder.init()
private void init() throws Exception {
		Collection<SecurityConfigurer<O, B>> configurers = getConfigurers();//獲取HttpSecurity.configurers,HttpSecurity的配置器集合是在WebSecurity.init()內新增的內部SecurityConfigurer和使用者實現WebSecurityConfigurerAdapter.configure(HttpSecurity http)自定義新增的SecurityConfigurer

		for (SecurityConfigurer<O, B> configurer : configurers) {//對每個SecurityConfigurer執行其init(B)方法
			configurer.init((B) this);//SecurityConfigurer.init(B)
            /*
            *spring security預設新增的內部SecurityConfigurer是CsrfConfigurer,ExceptionHandlingConfigurer,HeadersConfigurer,
            SessionManagementConfigurer,SecurityContextConfigurer,RequestCacheConfigurer,
            AnonymousConfigurer,ServletApiConfigurer,DefaultLoginPageConfigurer,LogoutConfigurer
            這些SecurityConfigurer每個都執行init方法進行初始化
            */
		}

		for (SecurityConfigurer<O, B> configurer : configurersAddedInInitializing) {
			configurer.init((B) this);
		}
	}
//接著看HttpSecurity.configure()即AbstractConfiguredSecurityBuilder.configure()
private void configure() throws Exception {
		Collection<SecurityConfigurer<O, B>> configurers = getConfigurers();////獲取HttpSecurity.configurers

		for (SecurityConfigurer<O, B> configurer : configurers) {
			configurer.configure((B) this);//執行每個安全配置器SecurityConfigurer.configure(B),每個SecurityConfigurer都會建立一個filter,這些filter都會快取到HttpSecurity.filters集合內,用於filterchain
		}
	}
//接著看HttpSecurity.performBuild()
protected DefaultSecurityFilterChain performBuild() throws Exception {
		Collections.sort(filters, comparator);
		return new DefaultSecurityFilterChain(requestMatcher, filters);//建立DefaultSecurityFilterChain物件,該物件包裝了filters
	}

經過上面分析,最終WebSecurityConfiguration.springSecurityFilterChain()建立了名稱為springSecurityFilterChain型別是FilterChainProxy的filter,這樣就可以在web容器內執行了,執行流程如下圖:

啟動流程圖如圖

1.3.security介面總結

總結如下:

SecurityBuilder是安全構建器,用於構建物件

​ WebSecurity用於構建FilterChainProxy

​ HttpSecurity用於構建DefaultSecurityFilterChain

​ AuthenticationManagerBuilder用於構建AuthenticationManager,預設是ProviderManager

SecurityConfigurer是安全配置器,用於配置SecurityBuilder

​ WebSecurityConfigurerAdapter用於配置HttpSecurity

​ SecurityConfigurerAdapter是安全配置器,有多個配置器,組成filterchain,其實現是AbstractHttpConfigurer,使用者通常要定義配置器和filter,要實現 AbstractHttpConfigurer抽象http安全配置器

AuthenticationManager是認證管理器,預設是ProviderManager,不需要過多關注

AuthenticationProvider是認證provider,預設是DaoAuthenticationProvider,聚合了UserDetailsService

UserDetailsService是資料驗證的來源,比如jdbc,記憶體等,使用者需要實現該類UserDetails loadUserByUsername(String username),用於獲取使用者資訊

UserDetails是使用者資訊,預設實現是User,用於獲取使用者資訊

類之間的大致關係:

WebSecurity聚合了WebSecurityConfigurerAdapter、HttpSecurity

HttpSecurity聚合了一系列SecurityConfigurer,這一系列的SecurityConfigurer用於建立filter從而構成filterchain

WebSecurityConfigurerAdapter聚合了AuthenticationManager,即ProviderManager

ProviderManager聚合了AuthenticationProvider,即DaoAuthenticationProvider

DaoAuthenticationProvider聚合了UserDetailsService,指定了認證資料的來源

2.spring security執行流程

也可以說是FilterChainProxy的執行過程

spring security就是建立了一系列filter,然後執行的時候就是web容器去執行filterchain了,如下圖

2.2.核心過濾器講解

比如例子 https://gitee.com/yulewo123/MySecurityDemo.git 中的configure(HttpSecurity http)實現如下,對應建立的filter加了註釋

@Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()//FilterSecurityInterceptor
                .antMatchers("/").permitAll()
                .antMatchers("/user/**").hasAuthority("USER")
                .and()
                .formLogin().loginPage("/login").defaultSuccessUrl("/user")//UsernamePasswordAuthenticationFilter
                .and()
                .logout().logoutUrl("/logout").logoutSuccessUrl("/login");//LogoutFilter
    }

那麼總計filterchain是

WebAsyncManagerIntegrationFilter	內部filter,直接新增,不通過SecurityConfigurer型別配置器建立
SecurityContextPersistenceFilter  內部filter 建立SecurityContext通過ThreadLocal儲存到當前執行緒。由SecurityContextConfigurer建立。
HeaderWriterFilter	內部filter 由HeadersConfigurer建立
CsrfFilter	內部filter	由CsrfConfigurer建立
LogoutFilter 使用者新增	由LogoutConfigurer建立
UsernamePasswordAuthenticationFilter 使用者新增  功能用於認證 formLogin() FormLoginConfigurer新增
RequestCacheAwareFilter	內部filte  由RequestCacheConfigurer建立
SecurityContextHolderAwareRequestFilter	內部filter	由ServletApiConfigurer建立
AnonymousAuthenticationFilter	內部filter  由AnonymousConfigurer建立
SessionManagementFilter	內部filter  由SessionManagementConfigurer建立
ExceptionTranslationFilter	內部filter。由ExceptionHandlingConfigurer建立
FilterSecurityInterceptor 使用者新增 功能用於授權 由ExpressionUrlAuthorizationConfigurer建立

除了使用者新增字樣的filter是例子內configure(HttpSecurity http)新增的,其它都是spring security內部新增的filter,這些基本上都核心過濾器,下面一個個來說這些filter

WebAsyncManagerIntegrationFilter:工作中沒用到過,忽略。

SecurityContextPersistenceFilter:本質就是對每個request,建立一個SecurityContext 物件,該物件(通過ThreadLocale)儲存到當前執行緒上,這樣在該請求中,就可以隨處使用SecurityContext 。程式碼簡單,就不具體分析了,類圖關係如下:

HeaderWriterFilter:用來給http響應新增一些Header,比如X-Frame-Options, X-XSS-Protection*,X-Content-Type-Options。對我們開發人員來說也不是很重要,忽略。
CsrfFilter:用於防止csrf攻擊,許多前後端分離專案,後端(公司內部)應用都是直接禁用csft, httpSecurity.csrf().disable()
LogoutFilter:登入退出的過濾器,在登入退出後清除session和認證資訊,以及重定向到指定頁面

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
			throws IOException, ServletException {
		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) res;

		if (requiresLogout(request, response)) {//請求uri匹配/logout,則進入執行
			Authentication auth = SecurityContextHolder.getContext().getAuthentication();//獲取當前請求執行緒上繫結的認證
            //this.handler是CompositeLogoutHandler[SecurityContextLogoutHandler]
			this.handler.logout(request, response, auth);//執行SecurityContextLogoutHandler.logout方法,在登入退出的時候清除session、清除認證資訊
			//this.logoutSuccessHandler是SimpleUrlLogoutSuccessHandler
			logoutSuccessHandler.onLogoutSuccess(request, response, auth);//重定向到登入頁面
			return;
		}

		chain.doFilter(request, response);//請求uri不匹配/logout,執行下一個filter
	}

UsernamePasswordAuthenticationFilter:認證filter,這裡是進行認證,spring securtiy的核心過濾器,後面詳細寫。
RequestCacheAwareFilter:內部維護了一個RequestCache,用於快取request請求。不是關注重點,忽略
SecurityContextHolderAwareRequestFilter:此過濾器對ServletRequest進行了一次包裝(裝飾),使得request具有更加豐富的API。不是關注重點,忽略
AnonymousAuthenticationFilter:設定匿名認證儲存到當前執行緒,這個很重要,應該和UsernamePasswordAuthenticationFilter一起比較,spring security為了相容未登入的訪問,也走了一套認證流程,只不過是一個匿名的身份。
SessionManagementFilter:跟session有關的過濾器,個人認為不需要過多關注。忽略
ExceptionTranslationFilter:異常處理過濾器,用於捕捉FilterSecurityInterceptor丟擲的異常,這個重要,需要關注捕捉到異常後都做了什麼邏輯。程式碼如下

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
			throws IOException, ServletException {
		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) res;

		try {
			chain.doFilter(request, response);
		}
		catch (IOException ex) {//IOException直接向上丟擲,為什麼IO異常特殊呢?因為是網路傳輸,IO異常比如對方連線斷開,這個不屬於業務異常
			throw ex;
		}
		catch (Exception ex) {
			// Try to extract a SpringSecurityException from the stacktrace
			Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);//返回堆疊異常集合(從堆疊從上到下的每個異常)
			RuntimeException ase = (AuthenticationException) throwableAnalyzer
					.getFirstThrowableOfType(AuthenticationException.class, causeChain);//獲取堆疊異常中第一個認證異常AuthenticationException

			if (ase == null) {
				ase = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType(
						AccessDeniedException.class, causeChain);//認證異常為null則獲取授權異常(訪問控制異常)
			}

			if (ase != null) {//存在認證異常或授權異常
				if (response.isCommitted()) {//訊息未返回給前端
					throw new ServletException("Unable to handle the Spring Security Exception because the response is already committed.", ex);
				}
				handleSpringSecurityException(request, response, chain, ase);//程式碼@1 處理spring security異常。spring security定義了認證AuthenticationException和授權異常AccessDeniedException,根據該異常,可以做一些處理,比如跳轉到登入頁面
			}
			else {//非spring security異常,則直接丟擲異常,比如業務丟擲了NullPointerException,則直接向上拋
				// Rethrow ServletExceptions and RuntimeExceptions as-is
				if (ex instanceof ServletException) {
					throw (ServletException) ex;
				}
				else if (ex instanceof RuntimeException) {
					throw (RuntimeException) ex;
				}

				// Wrap other Exceptions. This shouldn't actually happen
				// as we've already covered all the possibilities for doFilter
				throw new RuntimeException(ex);
			}
		}
	}

//程式碼@1是處理spring security異常,即認證異常和授權異常
private void handleSpringSecurityException(HttpServletRequest request,
			HttpServletResponse response, FilterChain chain, RuntimeException exception)
			throws IOException, ServletException {
		if (exception instanceof AuthenticationException) {//認證異常
			sendStartAuthentication(request, response, chain,
					(AuthenticationException) exception);//程式碼@2,認證失敗重定向到登入頁面
		}
		else if (exception instanceof AccessDeniedException) {//授權異常
			Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
			if (authenticationTrustResolver.isAnonymous(authentication) || authenticationTrustResolver.isRememberMe(authentication)) {//匿名認證丟擲了授權異常
				sendStartAuthentication(
						request,
						response,
						chain,
						new InsufficientAuthenticationException(
							messages.getMessage(
								"ExceptionTranslationFilter.insufficientAuthentication",
								"Full authentication is required to access this resource")));//程式碼@2
			}
			else {
				accessDeniedHandler.handle(request, response,
						(AccessDeniedException) exception);// 返回403禁止錯誤,同時頁面跳轉到錯誤頁面
			}
		}
	}

//程式碼@2,處理認證異常,protected方法,可以重寫
protected void sendStartAuthentication(HttpServletRequest request,
			HttpServletResponse response, FilterChain chain,
			AuthenticationException reason) throws ServletException, IOException {
		// SEC-112: Clear the SecurityContextHolder's Authentication, as the
		// existing Authentication is no longer considered valid
		SecurityContextHolder.getContext().setAuthentication(null);//清除認證
		requestCache.saveRequest(request, response);//快取請求
		logger.debug("Calling Authentication entry point.");
		authenticationEntryPoint.commence(request, response, reason);//程式碼@3重點關注
	}
//程式碼@3分析
AuthenticationEntryPoint是身份驗證入口點,那麼該物件是如何在啟動過程注入到ExceptionTranslationFilter的呢?是在FormLoginConfigurer.init(H)內載入的,ExceptionTranslationFilter.authenticationEntryPoint 被賦值為FormLoginConfigurer.authenticationEntryPoint,那麼FormLoginConfigurer.authenticationEntryPoint是哪裡注入實現的呢?是在FormLoginConfigurer.setLoginPage(String)注入的,該方法是在使用者實現WebSecurityConfigurerAdapter.configure(HttpSecurity http)方法內的formLogin().loginPage("/login")賦值的,最終該物件就是LoginUrlAuthenticationEntryPoint。為什麼重點分析這個身份驗證入口點呢?使用者可以自定義實現,如果認證失敗,可以實現AuthenticationEntryPoint,讓頁面重定向到sso登入頁面(通常公司都會有個sso系統)。ExceptionTranslationFilter是spring security內部filter,如何設定其屬性ExceptionTranslationFilter.authenticationEntryPoint的值呢?在WebSecurityConfigurerAdapter.configure(HttpSecurity http)內HttpSecurity.exceptionHandling().authenticationEntryPoint(自定義的AuthenticationEntryPoint)即可。
總結:AuthenticationEntryPoint介面就類似個spring security異常的後置處理器,在認證或授權失敗後,由該介面做一些後置處理。

FilterSecurityInterceptor:功能是授權,核心filter,後續詳細分析

由上面分析可知,實際上,使用者只需要主要關注認證授權即可,其它基本不需要關注。

2.2.認證過程

認證通常預設是在UsernamePasswordAuthenticationFilter內處理的,下面分析這個filter

先說下介面

AuthenticationManager是認證管理器,方法就是認證,預設實現是ProviderManager,包含了一組AuthenticationProvider,每個就是一個具體的認證提供者。

AuthenticationProvider是認證提供者,實際上是由它進行認證的。預設實現是DaoAuthenticationProvider。

UserDetailsService是認證方式的抽象,有記憶體、資料庫等認證方式。

Authentication是認證介面,預設實現是UsernamePasswordAuthenticationToken,使用使用者密碼進行認證。

這些介面之間的關係是:

AuthenticationManager使用AuthenticationProvider提供者採用UserDetailsService的方式進行認證,生成Authentication。

認證核心程式碼分析如下

//UsernamePasswordAuthenticationFilter.doFilterr(ServletRequest, ServletResponse, FilterChain)方法
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
			throws IOException, ServletException {
		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) res;

		if (!requiresAuthentication(request, response)) {//請求路徑不匹配/login,則不要求認證
			chain.doFilter(request, response);
			return;
		}
		Authentication authResult;
		try {
			authResult = attemptAuthentication(request, response);//@11 認證核心方法
			if (authResult == null) {
				// return immediately as subclass has indicated that it hasn't completed
				// authentication
				return;
			}
            //sessionStrategy是CompositeSessionAuthenticationStrategy [ChangeSessionIdAuthenticationStrategy, CsrfAuthenticationStrategy]
			sessionStrategy.onAuthentication(authResult, request, response);
		}
		catch (InternalAuthenticationServiceException failed) {
			unsuccessfulAuthentication(request, response, failed);//認證失敗,則使用failureHandler失敗處理器來處理,比如重定向到錯誤頁面
			return;
		}
		catch (AuthenticationException failed) {
			// Authentication failed
			unsuccessfulAuthentication(request, response, failed);//認證失敗,則使用failureHandler失敗處理器來處理,比如重定向到錯誤頁面
			return;
		}
		// Authentication success
		if (continueChainBeforeSuccessfulAuthentication) {//false
			chain.doFilter(request, response);
		}
		successfulAuthentication(request, response, chain, authResult);//認證成功,使用認證成功的處理器successHandler來處理認證結果,把認證結果儲存到當前請求執行緒,重定向到登入成功頁面
	}

//程式碼@11分析,該方法是嘗試認證
public Authentication attemptAuthentication(HttpServletRequest request,
			HttpServletResponse response) throws AuthenticationException {
		String username = obtainUsername(request);
		String password = obtainPassword(request);
		if (username == null) {
			username = "";
		}
		if (password == null) {
			password = "";
		}
		username = username.trim();
		UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
				username, password);//獲取請求中的使用者和密碼,包裝為認證物件UsernamePasswordAuthenticationToken
		// Allow subclasses to set the "details" property
		setDetails(request, authRequest);//儲存detail
		return this.getAuthenticationManager().authenticate(authRequest);//程式碼@12
	}
//程式碼@12分析
/*
*this.getAuthenticationManager()返回的是AuthenticationManager物件,即ProviderManager,這個ProviderManager.parent是另外一個ProviderManager,providers是[AnonymousAuthenticationProvider],ProviderManager.parent.parent是null,ProviderManager.parent.parent是[DaoAuthenticationProvider]。
UsernamePasswordAuthenticationFilter.authenticationManager是在HttpSecurity.beforeConfigure()內賦值的
*/

public Authentication authenticate(Authentication authentication)
			throws AuthenticationException {
		Class<? extends Authentication> toTest = authentication.getClass();
		AuthenticationException lastException = null;
		AuthenticationException parentException = null;
		Authentication result = null;
		Authentication parentResult = null;
		boolean debug = logger.isDebugEnabled();
		for (AuthenticationProvider provider : getProviders()) {//getProviders()返回是[AnonymousAuthenticationProvider]
			if (!provider.supports(toTest)) {//AnonymousAuthenticationProvider不支援UsernamePasswordAuthenticationToken.class
				continue;
			}

			try {
				result = provider.authenticate(authentication);

				if (result != null) {
					copyDetails(authentication, result);
					break;
				}
			}
			catch (AccountStatusException e) {
				prepareException(e, authentication);
				// SEC-546: Avoid polling additional providers if auth failure is due to
				// invalid account status
				throw e;
			}
			catch (InternalAuthenticationServiceException e) {
				prepareException(e, authentication);
				throw e;
			}
			catch (AuthenticationException e) {
				lastException = e;
			}
		}//for end

		if (result == null && parent != null) {
			// Allow the parent to try.
			try {
				result = parentResult = parent.authenticate(authentication);//程式碼@13 parent是ProviderManager,這個ProviderManager的providers是[DaoAuthenticationProvider]
			}
			catch (ProviderNotFoundException e) {
				// ignore as we will throw below if no other exception occurred prior to
				// calling parent and the parent
				// may throw ProviderNotFound even though a provider in the child already
				// handled the request
			}
			catch (AuthenticationException e) {
				lastException = parentException = e;
			}
		}

		if (result != null) {
			if (eraseCredentialsAfterAuthentication
					&& (result instanceof CredentialsContainer)) {
				// Authentication is complete. Remove credentials and other secret data
				// from authentication
				((CredentialsContainer) result).eraseCredentials();
			}

			// If the parent AuthenticationManager was attempted and successful than it will publish an AuthenticationSuccessEvent
			// This check prevents a duplicate AuthenticationSuccessEvent if the parent AuthenticationManager already published it
			if (parentResult == null) {
				eventPublisher.publishAuthenticationSuccess(result);//鉤子方法,事件監聽器
			}
			return result;
		}

		// Parent was null, or didn't authenticate (or throw an exception).

		if (lastException == null) {//如果認證結果為null,且沒異常,則建立ProviderNotFoundException
			lastException = new ProviderNotFoundException(messages.getMessage(
					"ProviderManager.providerNotFound",
					new Object[] { toTest.getName() },
					"No AuthenticationProvider found for {0}"));
		}

		// If the parent AuthenticationManager was attempted and failed than it will publish an AbstractAuthenticationFailureEvent
		// This check prevents a duplicate AbstractAuthenticationFailureEvent if the parent AuthenticationManager already published it
		if (parentException == null) {//如果認證結果為null,且沒異常,
			prepareException(lastException, authentication);//事件機制,廣播
		}

		throw lastException;
	}
//總結:1.如果認證結果為null,且沒異常,則返回ProviderNotFoundException。2.如果丟擲了異常,則返回丟擲的異常。3.如果認證結果非null,沒丟擲異常,說明認證成功了,返回認證結果

//程式碼@13,先執行ProviderManager[providers=[DaoAuthenticationProvider]]的authenticate(Authentication authentication)認證方法,接著執行DaoAuthenticationProvider的認證方法,該方法邏輯是先根據使用者名稱到資料庫查詢出使用者,然後比對密碼是否相同additionalAuthenticationChecks,建立認證物件,設定認證成功true.
//DaoAuthenticationProvider.authenticate(Authentication)方法主要是執行org.springframework.security.authentication.dao.DaoAuthenticationProvider.retrieveUser(String, UsernamePasswordAuthenticationToken)和DaoAuthenticationProvider.createSuccessAuthentication(Object, Authentication, UserDetails)
protected final UserDetails retrieveUser(String username,
			UsernamePasswordAuthenticationToken authentication)
			throws AuthenticationException {//查詢使用者資訊
		prepareTimingAttackProtection();
		try {
            //this.getUserDetailsService()返回物件UserDetailsService是在使用者重寫的WebSecurityConfigurerAdapter.configure(AuthenticationManagerBuilder)方法內賦值的,賦值為使用者開發的UserDetailsService,實現loadUserByUsername,從資料庫查詢資料
			UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
			if (loadedUser == null) {
				throw new InternalAuthenticationServiceException(
						"UserDetailsService returned null, which is an interface contract violation");
			}
			return loadedUser;
		}
		catch (UsernameNotFoundException ex) {
			mitigateAgainstTimingAttack(authentication);
			throw ex;
		}
		catch (InternalAuthenticationServiceException ex) {
			throw ex;
		}
		catch (Exception ex) {
			throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
		}
	}
//DaoAuthenticationProvider.createSuccessAuthentication(Object, Authentication, UserDetails)建立成功的認證結果
protected Authentication createSuccessAuthentication(Object principal,
			Authentication authentication, UserDetails user) {//建立認證成功物件
		// Ensure we return the original credentials the user supplied,
		// so subsequent attempts are successful even with encoded passwords.
		// Also ensure we return the original getDetails(), so that future
		// authentication events after cache expiry contain the details
		UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(
				principal, authentication.getCredentials(),
				authoritiesMapper.mapAuthorities(user.getAuthorities()));//建立認證結果,標識true說明已經認證,
		result.setDetails(authentication.getDetails());

		return result;
	}

總結:UsernamePasswordAuthenticationFilter是認證filter,它的基本功能還是判斷登入,比對密碼正確,說明認證成功。spring security的設計是使用認證管理器AuthenticationManager建立認證Authentication。一個AuthenticationManager有多個AuthenticationProvider認證提供者可以使用來進行認證,如果一個AuthenticationProvider認證提供者通過,則認為認證通過。每個AuthenticationProvider可以使用不同的認證方式UserDetailsService進行認證。認證類圖關係如下

2.3.授權過程

授權是在FilterSecurityInterceptor這個filter處理的,這個filter是http.authorizeRequests()授權請求新增的。

先看FilterSecurityInterceptor結構

FilterSecurityInterceptor是由配置類ExpressionUrlAuthorizationConfigurer建立的。

屬性securityMetadataSource是ExpressionBasedFilterInvocationSecurityMetadataSource,包裝了訪問路徑,即antMatchers("/").permitAll().antMatchers("/user/**")

屬性accessDecisionManager是訪問決定管理器,用於決定哪些資源可以訪問。在啟動的配置階段賦值為AffirmativeBased

屬性afterInvocationManager對FilterSecurityInterceptor來說就是null

屬性authenticationManager 在配置階段賦值為ProviderManager

security自帶的鑑權filter FilterSecurityInterceptor的流程:

step1:包裝http request response filterchain建立FilterInvocation,然後執行呼叫

public void doFilter(ServletRequest request, ServletResponse response,
			FilterChain chain) throws IOException, ServletException {
		FilterInvocation fi = new FilterInvocation(request, response, chain);
		invoke(fi);
	}

step2:

public void invoke(FilterInvocation fi) throws IOException, ServletException {
		if ((fi.getRequest() != null)
				&& (fi.getRequest().getAttribute(FILTER_APPLIED) != null)
				&& observeOncePerRequest) {
			// filter already applied to this request and user wants us to observe
			// once-per-request handling, so don't re-do security checking
			fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
		}
		else {
			// first time this request being called, so perform security checking
			if (fi.getRequest() != null && observeOncePerRequest) {
				fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);//程式碼@1
			}

			InterceptorStatusToken token = super.beforeInvocation(fi);//程式碼@2 核心

			try {
				fi.getChain().doFilter(fi.getRequest(), fi.getResponse());//程式碼@3
			}
			finally {
				super.finallyInvocation(token);//程式碼@4
			}

			super.afterInvocation(token, null);//程式碼@5
		}
	}

該程式碼的執行流程是:

程式碼@1:設定attribute為true,防止重複呼叫

程式碼@2:核心方法,這裡是進行鑑權邏輯

程式碼@3:鑑權結束,繼續執行filterchain

程式碼@4:通過threadlocal設定當前執行緒的SecurityContext

程式碼@5:FilterSecurityInterceptor.afterInvocationManager非null情況下執行,預設是不執行的,忽略。

先來分析下FilterSecurityInterceptor建立過程和幾個屬性的來源

在WebSecurityConfigurerAdapter的子類configure(HttpSecurity)方法中httpSecurity.authorizeRequests()建立SecurityConfigurer物件,即ExpressionUrlAuthorizationConfigurer,SecurityConfigurer物件的作用是用於生成filter並加入到HttpSecurity(最終加入到FilterChainProxy作為security的過濾器),ExpressionUrlAuthorizationConfigurer建立鑑權filter FilterSecurityInterceptor,

//ExpressionUrlAuthorizationConfigurer.configure(H) -> ExpressionUrlAuthorizationConfigurer.createFilterSecurityInterceptor(H, FilterInvocationSecurityMetadataSource, AuthenticationManager) 建立FilterSecurityInterceptor
private FilterSecurityInterceptor createFilterSecurityInterceptor(H http,
			FilterInvocationSecurityMetadataSource metadataSource,
			AuthenticationManager authenticationManager) throws Exception {
		FilterSecurityInterceptor securityInterceptor = new FilterSecurityInterceptor();
		securityInterceptor.setSecurityMetadataSource(metadataSource);//屬性securityMetadataSource是ExpressionBasedFilterInvocationSecurityMetadataSource
		securityInterceptor.setAccessDecisionManager(getAccessDecisionManager(http));//屬性accessDecisionManager是AffirmativeBased
		securityInterceptor.setAuthenticationManager(authenticationManager);//屬性authenticationManager是ProviderManager
		securityInterceptor.afterPropertiesSet();
		return securityInterceptor;
	}

因此幾個屬性:

FilterInvocationSecurityMetadataSource securityMetadataSource 是屬性securityMetadataSource是ExpressionBasedFilterInvocationSecurityMetadataSource,是安全元資料來源,
AccessDecisionManager accessDecisionManager 是AffirmativeBased,訪問決定管理器,用於決定哪些資源可以訪問
AfterInvocationManager afterInvocationManager 沒使用過,預設null
AuthenticationManager authenticationManager 是ProviderManager
RunAsManager runAsManager = new NullRunAsManager() 預設值NullRunAsManager

知道了屬性的來源,接著分析下鑑權的核心邏輯,即程式碼@2 super.beforeInvocation(fi),忽略了不重要程式碼

protected InterceptorStatusToken beforeInvocation(Object object) {

		Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
				.getAttributes(object);//程式碼@6 核心

		if (attributes == null || attributes.isEmpty()) {
			if (rejectPublicInvocations) {
				throw new IllegalArgumentException(
						"Secure object invocation "
								+ object
								+ " was denied as public invocations are not allowed via this interceptor. "
								+ "This indicates a configuration error because the "
								+ "rejectPublicInvocations property is set to 'true'");
			}
			return null; // no further work post-invocation
		}

		Authentication authenticated = authenticateIfRequired();

		// Attempt authorization
		try {
			this.accessDecisionManager.decide(authenticated, object, attributes);//程式碼@7 核心
		}
		catch (AccessDeniedException accessDeniedException) {
			publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,
					accessDeniedException));
			throw accessDeniedException;
		}

		// Attempt to run as a different user
		Authentication runAs = this.runAsManager.buildRunAs(authenticated, object,
				attributes);//預設返回null

		if (runAs == null) {//執行這裡
			// no further work post-invocation
			return new InterceptorStatusToken(SecurityContextHolder.getContext(), false,
					attributes, object);//返回InterceptorStatusToken
		}
	}

只分析核心程式碼

程式碼@6:執行的是ExpressionBasedFilterInvocationSecurityMetadataSource.getAttributes(Object),程式碼如下

public Collection<ConfigAttribute> getAttributes(Object object) {
		final HttpServletRequest request = ((FilterInvocation) object).getRequest();
		for (Map.Entry<RequestMatcher, Collection<ConfigAttribute>> entry : requestMap
				.entrySet()) {
			if (entry.getKey().matches(request)) {
				return entry.getValue();
			}
		}
		return null;
	}

該方法的功能就是判斷本次請求request的uri是否和集合requestMap內的是否匹配(允許訪問),不允許則返回null。那麼核心就是ExpressionBasedFilterInvocationSecurityMetadataSource.requestMap的資料來源了,該集合的內容例子如下

{Ant [pattern='/swagger-ui.html']=[permitAll], 
Ant [pattern='/swagger-resources/**']=[permitAll], 
Ant [pattern='/swagger/**']=[permitAll], 
Ant [pattern='/**/v2/api-docs']=[permitAll], 
Ant [pattern='/**/*.js']=[permitAll], 
Ant [pattern='/**/*.css']=[permitAll], 
Ant [pattern='/**/*.png']=[permitAll], 
Ant [pattern='/**/*.ico']=[permitAll], 
Ant [pattern='/webjars/springfox-swagger-ui/**']=[permitAll], 
Ant [pattern='/actuator/**']=[permitAll], 
Ant [pattern='/druid/**']=[permitAll], 
Ant [pattern='/admin/login']=[permitAll], 
Ant [pattern='/admin/register']=[permitAll], 
Ant [pattern='/admin/info']=[permitAll], 
Ant [pattern='/admin/logout']=[permitAll], 
Ant [pattern='/**', OPTIONS]=[permitAll], any request=[authenticated]}

這個集合內的資料是如何來的呢?是在ExpressionBasedFilterInvocationSecurityMetadataSource構造器賦值,而賦值的來源是ExpressionInterceptUrlRegistry.urlMappings,最終就落在了ExpressionInterceptUrlRegistry.urlMappings的來源了。那麼ExpressionInterceptUrlRegistry.urlMappings值是如何得來的呢?是在WebSecurityConfigurerAdapter子類的configure(HttpSecurity)方法內設定permit請求資源時候賦值的,如圖紅框內的程式碼進行設定請求資源的許可(實際就是授權設定,比如permitAll(), hasAuthority(XXX), hasRole(XXX)設定)。

因此程式碼@6返回的就是匹配的資源,但是,此時還沒用進行鑑權,判斷是否有訪問許可權是在程式碼@7進行的。

程式碼@7:

//接著看核心方法decide
public void decide(Authentication authentication, Object object,
			Collection<ConfigAttribute> configAttributes) throws AccessDeniedException {
		int deny = 0;
		//getDecisionVoters()返回的是AffirmativeBased.decisionVoters集合,即[WebExpressionVoter@8a9c7cf]
    //AffirmativeBased.decisionVoters屬性是在ExpressionUrlAuthorizationConfigurer配置階段賦值的
		for (AccessDecisionVoter voter : getDecisionVoters()) {
			int result = voter.vote(authentication, object, configAttributes);//程式碼@8 核心方法 WebExpressionVoter.vote方法,進行認證投票,投票數>1,則認為可以授權。該方法判斷使用者角色達到要求,然後授權
			switch (result) {
			case AccessDecisionVoter.ACCESS_GRANTED://結果1,授權通過
				return;
			case AccessDecisionVoter.ACCESS_DENIED://結果-1,授權失敗
				deny++;
				break;
			default:
				break;
			}
		}
		if (deny > 0) {
			throw new AccessDeniedException(messages.getMessage(
			   "AbstractAccessDecisionManager.accessDenied", "Access is denied"));//認證失敗,丟擲認證異常,由異常filter進行捕捉處理
		}
		// To get this far, every AccessDecisionVoter abstained
		checkAllowIfAllAbstainDecisions();
	}

程式碼@8是核心方法,進行投票,這個方法是進行鑑權的核心,鑑權的程式碼很複雜,通過debug發現原來是通過反射呼叫了SecurityExpressionRoot類的hasAuthority方法:

繼而最終執行的是hasAnyAuthorityName

本質就是判斷請求經過授權後所攜帶的許可權資訊是否包含設定允許訪問的路徑,包含,則認為鑑權通過。

getAuthoritySet()方法就是獲取該請求經過認證後的許可權資訊。

authentication.getAuthorities()獲取的就是使用者所攜帶的許可權資訊,就是UsernamePasswordAuthenticationToken.authorities欄位,該欄位是在認證filter UsernamePasswordAuthenticationFilter賦值的,呼叫處是

AbstractUserDetailsAuthenticationProvider.createSuccessAuthentication(Object, Authentication, UserDetails) 建立認證物件Authentication,即UsernamePasswordAuthenticationToken,程式碼如下

紅框處就是設定許可權地方,來源就是UserDetails,呼叫處是DaoAuthenticationProvider.retrieveUser(String, UsernamePasswordAuthenticationToken),loadUserByUsername就是我們實現的認證來源了,通常是資料庫,從資料庫查詢,程式碼例子如下

其中紅框內就是設定使用者的許可權了。

從原始碼可以看到,框架認為系統所具有的許可權為從登陸資訊中的authorities欄位,是一個GrantedAuthority型別的List。
而框架中預設使用的GrantedAuthority則是SimpleGrantedAuthority,其實就是一個字串的包裝類,現在我們已經知道如何去給使用者賦予許可權了了,只需要將許可權字串設定到其登陸資訊的authorities欄位即可。(在本例中,把許可權賦給了UserDetails物件的具體實現者org.springframework.security.core.userdetails.User.authorities,當然我們可以自行實現UserDetails)

鑑權很簡單,只要使用者所具有的角色和url中授予的角色任一匹配,則表示鑑權通過。不過有一點迷惑,為什麼這裡是呼叫的hasAuthority而不是hasRole函式呢?回過頭來看下我們的配置內容:

對於相同的url,後面的配置會覆蓋前面的所以只有hasAuthority生效了。如果我們來調換一下這兩個配置的位置:

就是hasRole生效了,使用的role角色進行鑑權。

注意:鑑權的具體實現是在SecurityExpressionRoot執行的,如果要debug,斷點要打在org.springframework.security.access.expression.SecurityExpressionRoot.hasAuthority(String)

org.springframework.security.access.expression.SecurityExpressionRoot.hasRole(String)

org.springframework.security.access.expression.SecurityExpressionRoot.isAuthenticated()

分別對應configure(HttpSecurity http)方法內許可權設定使用的是hasAuthority、hasRole、兩者都沒用的情況。

授權結論:

總結授權過程就判斷資源要訪問的角色是否在使用者攜帶的認證資訊內的授權列表內,則認為授權通過。

通過程式碼分析可知,涉及到2個介面

AccessDecisionManager 授權決策管理器,有三個實現

​ AffirmativeBased(spring security預設使用)

​ 只要有投通過(ACCESS_GRANTED=1)票,則直接判為通過。如果沒有投通過票且反對(ACCESS_DENIED=-1)票在1個及其以上的,則直接判為不通過。
ConsensusBased(少數服從多數)
​ 通過的票數大於反對的票數則判為通過;通過的票數小於反對的票數則判為不通過;通過的票數和反對的票數相等,則可根據配置allowIfEqualGrantedDeniedDecisions(預設為true)進行判斷是否通過。

UnanimousBased(反對票優先)
無論多少投票者投了多少通過(ACCESS_GRANTED)票,只要有反對票(ACCESS_DENIED),那都判為不通過;如果沒有反對票且有投票者投了通過票,那麼就判為通過。

AccessDecisionVoter 授權決策投票,用於返回投票的個數。

這些介面之間的關係是FilterSecurityInterceptor聚合AccessDecisionManager,AccessDecisionManager聚合AccessDecisionVoter。


授權的大體流程是:FilterSecurityInterceptor從SecurityContextHolder中獲取Authentication認證物件,然後比對使用者擁有的許可權和資源所需的許可權。使用者擁有的許可權可以通過Authentication物件直接獲得,而資源所需的許可權則需要引入兩個介面:SecurityMetadataSource,AccessDecisionManager。

SecurityMetadataSource:許可權元資料,它的預設實現是DefaultFilterInvocationSecurityMetadataSource,屬性requestMap儲存的就是資源的許可權,哪些可以訪問。

AccessDecisionManager:授權管理器,預設實現AffirmativeBased,它委託AccessDecisionVoter授權投票進行投票來決定是否授權,和AuthenticationManager委託ProviderManager很類似。

2.3.1.hasAuthority vs hasRole不同

hasAuthority、hasRole有什麼不同呢?先分析原始碼SecurityExpressionRoot

//hasAuthority原始碼如下:
public final boolean hasAuthority(String authority) {
    return hasAnyAuthority(authority);
}
public final boolean hasAnyAuthority(String... authorities) {
    return hasAnyAuthorityName(null, authorities);
}
//hasRole原始碼如下:
public final boolean hasRole(String role) {
		return hasAnyRole(role);
	}

	public final boolean hasAnyRole(String... roles) {
		return hasAnyAuthorityName(defaultRolePrefix, roles);
	}
//兩者最終都呼叫的是同一個方法hasAnyAuthorityName
private boolean hasAnyAuthorityName(String prefix, String... roles) {
    Set<String> roleSet = getAuthoritySet();//獲取認證資訊內的許可權集合

    for (String role : roles) {
        String defaultedRole = getRoleWithDefaultPrefix(prefix, role);
        if (roleSet.contains(defaultedRole)) {//使用者攜帶的許可權集合內包含設定的許可權,鑑權通過
            return true;
        }
    }

    return false;
}

通過原始碼分析,兩者功能是相同的,最終都呼叫的同一個方法hasAnyAuthorityName,不同的是hasAuthority傳入的prefix是null,hasRole傳入的prefix是ROLE_,因此對於這兩種鑑權方式來說,使用hasAuthority的時候資料庫儲存的角色就是USER,而使用hasRole方式鑑權資料庫儲存的角色欄位是ROLE_USER。

單純從原始碼角度來看,hasRole 和 hasAuthority 這兩個功能似乎一模一樣,除了字首之外就沒什麼區別了。

那麼 Spring Security 設計者為什麼要搞兩個看起來一模一樣的東西呢?

從設計上來說,這是兩個不同的東西。同時提供 role 和 authority 就是為了方便開發者從兩個不同的維度去設計許可權,所以並不衝突。

authority 描述的的是一個具體的許可權,例如針對某一項資料的查詢或者刪除許可權,它是一個 permission,例如 read_user、delete_user、update_user 之類的,這些都是具體的許可權,相信大家都能理解。

role 則是一個 permission 的集合,它的命名約定就是以 ROLE_ 開始,例如我們定義的 ROLE 是 ROLE_ADMIN、ROLE_USER 等等。

在專案中,我們可以將使用者和角色關聯,角色和許可權關聯,許可權和資源關聯。

反映到程式碼上,就是下面這樣:

假設用 Spring Security 提供的 SimpleGrantedAuthority 的代表 authority,然後我們自定義一個 Role,如下:

@lombok.Data
public class Role implements GrantedAuthority {
    private String name;

    private List<SimpleGrantedAuthority> allowedOperations = new ArrayList<>();

    @Override
    public String getAuthority() {
        return name;
    }

}

一個 Role 就是某些 authority 的集合,然後在 User 中定義 roles 集合。

@lombok.Data
public class User implements UserDetails {
    private List<Role> roles = new ArrayList<>();

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<SimpleGrantedAuthority> authorities = new ArrayList<>();
        for (Role role : roles) {
            authorities.addAll(role.getAllowedOperations());
        }
        return authorities.stream().distinct().collect(Collectors.toList());
    }
}

在 getAuthorities 方法中,載入 roles 中的許可權去重後再返回即可。

通過這個例子大家應該就能搞明白 Role 和 Authority 了。

Spring Security 的 issue 上這個類似的問題:https://github.com/spring-projects/spring-security/issues/4912

從作者對這個問題的回覆中,也能看到一些端倪:

作者承認了目前加 ROLE_ 字首的方式一定程度上給開發者帶來了困惑,但這是一個歷史積累問題。
作者說如果不喜歡 ROLE_,那麼可以直接使用 hasAuthority 代替 hasRole,言下之意,就是這兩個功能是一樣的。
作者還說了一些關於許可權問題的看法,許可權是典型的對物件的控制,但是 Spring Security 開發者不能向 Spring Security 使用者新增所有許可權,因為在大多數系統中,許可權都過於複雜龐大而無法完全包含在記憶體中。當然,如果開發者有需要,可以自定義類繼承自 GrantedAuthority 以擴充套件其功能。
從作者的回覆中我們也可以看出來,hasAuthorityhasRole 功能上沒什麼區別,設計層面上確實是兩個不同的東西。

結論:程式碼上來說,hasRole 和 hasAuthority 寫程式碼時字首不同,但是最終執行是一樣的;設計上來說,role 和 authority 這是兩個層面的許可權設計思路,一個是角色,一個是許可權,角色是許可權的集合。

參考來源 https://cloud.tencent.com/developer/article/1703187

3.spring security擴充套件

最近看到個動態授權的demo,覺得很好,分析下,加深下對security的理解。demo地址 https://github.com/macrozheng/mall-tiny

3.1.認證擴充套件

認證就是判斷請求的使用者是否合法的,資料認證的來源通常是db、sso等,以db為例,實現UserDetailsService介面的loadUserByUsername方法獲取角色(許可權集合),實現UserDetails用於儲存使用者資訊和許可權資訊。 具體參考demo。這個簡單

3.2.鑑權擴充套件-動態鑑權

要動態的給予某個角色不同的訪問許可權應該怎麼做呢?

既然是動態鑑權了,那我們的許可權URI肯定是放在資料庫中了,我們要做的就是實時的在資料庫中去讀取不同角色對應的許可權然後與當前登入的使用者做個比較。

動態授權思路:

step1:我們仿照security自帶的鑑權filter FilterSecurityInterceptor來寫個DynamicSecurityFilter,public class DynamicSecurityFilter extends AbstractSecurityInterceptor implements Filter,doFilter方法內也呼叫super.beforeInvocation(fi),在Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object)這裡就要進行自定義獲取許可權集合了。

step2:定義個動態許可權資料來源public class DynamicSecurityMetadataSource implements FilterInvocationSecurityMetadataSource,覆寫getAttributes方法,獲取角色的許可權。

step3:寫個動態許可權決策管理器,用於判斷使用者是否有訪問許可權public class DynamicAccessDecisionManager implements AccessDecisionManager,覆寫decide方法,將訪問所需資源或使用者擁有資源進行比對,使用者認證資訊攜帶的許可權包含訪問所需資源,則授權通過。

具體參考demo即可,該demo非常好,可以直接用於實際工作中。

3.3.security filter重複執行問題

在學習這個demo時候發現security filter DynamicSecurityFilter重複執行了,由於實現了javax.servlet.Filter介面且是個bean,在springboot啟動過程中

具體堆疊是

org.springframework.boot.web.servlet.ServletContextInitializerBeans.addAdaptableBeans(ListableBeanFactory)

這樣DynamicSecurityFilter除了加入到FilterChainProxy內執行,還在web filterchain內執行,被重複執行了,通常防止重複執行就要繼承org.springframework.web.filter.OncePerRequestFilter`就可以避免重複執行,但是DynamicSecurityFilter要繼承org.springframework.security.access.intercept.AbstractSecurityInterceptor,因此防止DynamicSecurityFilter重複執行,可以參考OncePerRequestFilter的做法,在執行後設置attribute=true,下次再執行判斷是true就不執行了。

4.spring security設計的思考

spring security目的是解決web的安全問題,安全問題分為認證(你是誰?)和授權(你可以訪問哪些資源),如果沒有spring security,我們也是把使用者的認證和訪問控制寫在filter內(因為filter是進入應用之前執行),spring security是把認證從授權裡面剝離開了。spring security對認證和授權進行了抽象,並且預留了擴充套件,使用者可以很方便在該框架下快速開發web安全應用。如果讓我自己設計,只是把認證和授權寫到filter內,這樣每個應用可能都需要寫一遍,通用性不高。

參考

https://juejin.im/post/6844903954040520718

https://segmentfault.com/a/1190000020172284

http://www.spring4all.com/article/428
https://leer.moe/2019/03/26/spring-security-architecture/

5.spring-security的自動裝配

方法1:在WebSecurityConfigurerAdapter子類上加註解@EnableWebSecurity,即可開啟。

方法2:springboot的自動裝配,不加@EnableWebSecurity,即可。自動配置是SecurityAutoConfiguration,注意,如果使用者沒有實現了WebSecurityConfigurerAdapter,那麼springboot會生成一個預設的WebSecurityConfigurerAdapter物件,即DefaultConfigurerAdapter。

實際開發中不需要再手工加@EnableWebSecurity了,實現的WebSecurityConfigurerAdapter的子類上加個@Component即可。

參考 https://www.cnblogs.com/fanzhidongyzby/p/11610334.html

https://juejin.im/post/6847902222668431368

https://cloud.tencent.com/developer/article/1703187

https://cloud.tencent.com/developer/article/1703187

https://segmentfault.com/a/1190000012173419

https://segmentfault.com/a/1190000012173419