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容器內執行了,執行流程如下圖:

image-20201004172800818

啟動流程圖如圖

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進行認證。認證類圖關系如下

image-20200927215953294

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)設置)。

image-20201004110506969

因此代碼@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方法:

image-20201004111358212

繼而最終執行的是hasAnyAuthorityName

image-20201004111515555

本質就是判斷請求經過授權后所攜帶的權限信息是否包含設置允許訪問的路徑,包含,則認為鑒權通過。

getAuthoritySet()方法就是獲取該請求經過認證后的權限信息。

image-20201004121258403

authentication.getAuthorities()獲取的就是用戶所攜帶的權限信息,就是UsernamePasswordAuthenticationToken.authorities字段,該字段是在認證filter UsernamePasswordAuthenticationFilter賦值的,調用處是

AbstractUserDetailsAuthenticationProvider.createSuccessAuthentication(Object, Authentication, UserDetails) 創建認證對象Authentication,即UsernamePasswordAuthenticationToken,代碼如下

image-20201004122333195

紅框處就是設置權限地方,來源就是UserDetails,調用處是DaoAuthenticationProvider.retrieveUser(String, UsernamePasswordAuthenticationToken),loadUserByUsername就是我們實現的認證來源了,通常是數據庫,從數據庫查詢,代碼例子如下

image-20201004122619174

其中紅框內就是設置用戶的權限了。

從源碼可以看到,框架認為系統所具有的權限為從登陸信息中的authorities字段,是一個GrantedAuthority類型的List。
而框架中默認使用的GrantedAuthority則是SimpleGrantedAuthority,其實就是一個字符串的包裝類,現在我們已經知道如何去給用戶賦予權限了了,只需要將權限字符串設置到其登陸信息的authorities字段即可。(在本例中,把權限賦給了UserDetails對象的具體實現者org.springframework.security.core.userdetails.User.authorities,當然我們可以自行實現UserDetails)

鑒權很簡單,只要用戶所具有的角色和url中授予的角色任一匹配,則表示鑒權通過。不過有一點迷惑,為什么這里是調用的hasAuthority而不是hasRole函數呢?回過頭來看下我們的配置內容:

image-20201004123306090

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

image-20201004123406700

就是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啟動過程中

具體堆棧是

image-20201004152709743

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

image-20201004152813721

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

image-20201004153356179

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM