Spring Security 是一個能夠為企業(yè)應用系統(tǒng)提供聲明式的安全訪問控制解決方案的安全框架,減少了為企業(yè)系統(tǒng)安全控制編寫大量重復代碼的工作,能夠與 Spring 無縫集成。本文旨在從實際應用角度出發(fā),閱讀 Spring Security 源碼,分析其實現(xiàn)原理。
注冊過濾器鏈
引入 spring-security 包:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.1.3.RELEASE</version>
</dependency>
對 Web 資源的控制通過 WebSecurityConfiguration 配置,其在啟動時裝配名為 springSecurityFilterChain 的 bean:
@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() {
});
// 應用 WebSecurityConfigurerAdapter 中的各項配置
webSecurity.apply(adapter);
}
// 生成 Spring Security 過濾器鏈
return webSecurity.build();
}
WebSecurityConfigurerAdapter 實現(xiàn)
spring-boot-autoconfigure 在 spring.factories 中標識 SecurityAutoConfiguration 需要自動裝配,在裝配過程中又將 SpringBootWebSecurityConfiguration 引入,在此類中聲明了一個繼承自 WebSecurityConfigurerAdapter 的 DefaultConfigurerAdapter,由此裝配了對 WebSecurityConfigurerAdapter 的默認實現(xiàn),此類對 spring security 做了許多默認配置。
如果需要干預 spring security 配置,則需要繼承 WebSecurityConfigurerAdapter 并裝配到 Spring 容器中。
@Configuration
@ConditionalOnClass(WebSecurityConfigurerAdapter.class)
@ConditionalOnMissingBean(WebSecurityConfigurerAdapter.class)
@ConditionalOnWebApplication(type = Type.SERVLET)
public class SpringBootWebSecurityConfiguration {
@Configuration
@Order(SecurityProperties.BASIC_AUTH_ORDER)
static class DefaultConfigurerAdapter extends WebSecurityConfigurerAdapter {
}
}
AbstractSecurityBuilder#build
WebSecurity 實例 調(diào)用 apply 方法獲取 WebSecurityConfigurerAdapter 中的配置,并調(diào)用 build 方法構造過濾器鏈,其實現(xiàn)為:
public final O build() throws Exception {
if (this.building.compareAndSet(false, true)) {
this.object = doBuild();
return this.object;
}
throw new AlreadyBuiltException("This object has already been built");
}
protected final O doBuild() throws Exception {
synchronized (configurers) {
buildState = BuildState.INITIALIZING;
beforeInit();
init();
buildState = BuildState.CONFIGURING;
beforeConfigure();
configure();
buildState = BuildState.BUILDING;
O result = performBuild();
buildState = BuildState.BUILT;
return result;
}
}
/**
* 內(nèi)部配置初始化
*/
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);
}
}
/**
* 配置邏輯
*/
private void configure() throws Exception {
Collection<SecurityConfigurer<O, B>> configurers = getConfigurers();
for (SecurityConfigurer<O, B> configurer : configurers) {
configurer.configure((B) this);
}
}
可以看到構建操作為將通過 apply 方法應用進來的配置分別初始化和構建,鏈條為 beforeInit -> init -> beforeConfigure -> configure -> performBuild。Spring Security 中的 AuthenticationManagerBuilder (認證管理器生成配置)、HttpSecurity (過濾器管理器生成配置)、WebSecurity (過濾器生成配置) 都是繼承 AbstractConfiguredSecurityBuilder 通過這個鏈條生成目標對象,這 3 個配置也是 Spring Security 的配置核心。

WebSecurityConfigurerAdapter 配置
由上文可知,過濾器鏈生成過程中調(diào)用了 WebSecurityConfigurerAdapter 的 init 和 configure 方法。
init 方法首先調(diào)用了 getHttp 方法,用于生成 AuthenticationManager 和 HttpSecurity 實例。
認證管理器 AuthenticationManager 的配置
來看看 WebSecurityConfigurerAdapter 關于認證管理器的組成:
/**
* 認證管理器,管理多種認證方式(AuthenticationProvider),進行實際的認證調(diào)用
*/
private AuthenticationManager authenticationManager;
/**
* 認證配置,裝配認證方式,通過 @Autowired 自動注入
*/
private AuthenticationConfiguration authenticationConfiguration;
/**
* 同于生成系統(tǒng)配置的認證管理器
*/
private AuthenticationManagerBuilder authenticationBuilder;
/**
* 用于生成開發(fā)者可干預的認證管理器
*/
private AuthenticationManagerBuilder localConfigureAuthenticationBldr;
/**
* true - 不使用可干預的認證管理器生成方式
*/
private boolean disableLocalConfigureAuthenticationBldr;
WebSecurityConfigurerAdapter 在初始化過程中會調(diào)用 authenticationManager 方法配置認證管理器,當 disableLocalConfigureAuthenticationBldr 為 true 時會調(diào)用 AuthenticationConfiguration#getAuthenticationManager 生成認證管理器,當為 false 時會使用開發(fā)者干預過的 localConfigureAuthenticationBldr 生成認證管理器。
protected AuthenticationManager authenticationManager() throws Exception {
if (!authenticationManagerInitialized) {
// void configure(AuthenticationManagerBuilder auth); 開發(fā)者可以重載此方法干預認證管理器的成邏輯
configure(localConfigureAuthenticationBldr);
if (disableLocalConfigureAuthenticationBldr) {
// 不需要干預,使用系統(tǒng)配置邏輯
authenticationManager = authenticationConfiguration.getAuthenticationManager();
} else {
authenticationManager = localConfigureAuthenticationBldr.build();
}
authenticationManagerInitialized = true;
}
return authenticationManager;
}
認證管理器的系統(tǒng)配置邏輯
WebSecurityConfigurerAdapter 中通過 @Autowired 注入了 AuthenticationConfiguration,此類的主要功能是為 AuthenticationManagerBuilder 裝配 AuthenticationProvider,可以裝配的認證配置邏輯分為兩類:
- (1)在
spring context中查找UserDetailsService等類的相關實現(xiàn),包裝成DaoAuthenticationProvider配置到AuthenticationManagerBuilder中。
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
UserDetailsService userDetailsService = getBeanOrNull(UserDetailsService.class);
// ...
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);
// ...
auth.authenticationProvider(provider);
}
UserDetailsService 的作用是通過用戶名查找用戶信息:
public interface UserDetailsService {
// 根據(jù)用戶名查找用戶信息
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
Spring Security 關于 UserDetails 的默認實現(xiàn)為 org.springframework.security.core.userdetails.User,它有一個構造方法,可以構造用戶名、密碼、是否激活、是否過期、是否憑證過期、是否鎖定:
public User(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities);
DaoAuthenticationProvider 繼承自 AbstractUserDetailsAuthenticationProvider,這個抽象類實現(xiàn)了 AuthenticationProvider 接口的 authenticate 方法,此方法會調(diào)用子類實現(xiàn)的 retrieveUser 方法。DaoAuthenticationProvider 的實現(xiàn)是調(diào)用注入進來的 UserDetailsService 的 loadUserByUsername 方法。
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
// ...
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
// ...
}
取出賬戶信息后,AbstractUserDetailsAuthenticationProvider#authenticate 方法進行 check,如果密碼錯誤,或者未激活、過期等都會拋出 AuthenticationException 異常。

在不自定義任何配置的情況下啟動引入 spring-boot-starter-security 包的項目,會發(fā)現(xiàn)控制臺輸出了這樣的日志:
2049-04-09 00:00:00.000 INFO 8504 --- [ restartedMain] .s.s.UserDetailsServiceAutoConfiguration : Using generated security password: 1a1890f2-97d5-421d-a775-953f7641b579
打開 UserDetailsServiceAutoConfiguration 類,此類在沒有 UserDetailsService 的自定義實現(xiàn)時,會裝配一個實現(xiàn)了 UserDetailsService 接口的 InMemoryUserDetailsManager,默認生成一個登錄名為 user 的用戶,密碼通過 UUID 隨機生成,并在控制臺輸出。默認的用戶配置取自 SecurityProperties,開發(fā)者可以在配置文件中加以覆蓋。
到此為止,用戶在不做任何額外配置的情況下,擁有了一個可以認證通過的賬號。
- (2)在
spring context中查找AuthenticationProvider的實現(xiàn),直接配置到AuthenticationManagerBuilder中,但是spring沒有裝配AuthenticationProvider的默認實現(xiàn)。
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
// ...
AuthenticationProvider authenticationProvider = getBeanOrNull(AuthenticationProvider.class);
// ...
auth.authenticationProvider(authenticationProvider);
}
開發(fā)者可干預的認證管理器
上文說到,系統(tǒng)會默認裝配兩類 AuthenticationProvider,如果開發(fā)者需要干預認證管理器的生成,同樣可以提供這兩類認證邏輯。
先看如何干預:disableLocalConfigureAuthenticationBldr 在 WebSecurityConfigurerAdapter 中默認為 false,void configure(AuthenticationManagerBuilder auth) 方法將此屬性修改為 true,如果開發(fā)者需要干預,則需要覆蓋此方法:
@Resource
private UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
// 自定義 AuthenticationProvider 實現(xiàn)
// .authenticationProvider(...)
// 自定義 UserDetailsService 實現(xiàn)
.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
}
開發(fā)者可以實現(xiàn) AuthenticationProvider 或 UserDetailsService 接口,填充自定義用戶邏輯。在注入自定義用戶認證邏輯時,實際還是包裝為
AuthenticationProvider,因此往后的認證邏輯就與系統(tǒng)的默認配置相符了。
public <T extends UserDetailsService> DaoAuthenticationConfigurer<AuthenticationManagerBuilder, T> userDetailsService(T userDetailsService) throws Exception {
this.defaultUserDetailsService = userDetailsService;
return apply(new DaoAuthenticationConfigurer<>(userDetailsService));
}
在 AuthenticationManagerBuilder 配置完成后,build 方法會將所有的 AuthenticationProvider 交給 ProviderManager 管理。在進行認證時,ProviderManager 會逐個調(diào)用認證邏輯進行認證,有一個通過即認證成功。
密碼加密管理
Spring Security 默認要求用戶密碼加密。以上文中提到的 UserDetailsServiceAutoConfiguration 為例,生成的密碼需要配置 {noop} 前綴:
/**
* 隨機密碼配置
*/
private String getOrDeducePassword(SecurityProperties.User user, PasswordEncoder encoder) {
String password = user.getPassword();
// ...
return "{noop}" + password;
}
這是因為如果不配置系統(tǒng)默認加密方式 PasswordEncoder,認證流程的校驗密碼環(huán)節(jié)會讀取密碼的前綴來判斷密碼使用的是哪種加密方式:
public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) {
// ...
// 獲取密碼前綴(加密方式)
String id = extractId(prefixEncodedPassword);
PasswordEncoder delegate = this.idToPasswordEncoder.get(id);
if (delegate == null) {
// ...
// 查不到前綴或者找不到前綴對應的加密方式,會拋出異常,因此是強制加密的
}
String encodedPassword = extractEncodedPassword(prefixEncodedPassword);
return delegate.matches(rawPassword, encodedPassword);
}
在項目開發(fā)中,為每個密碼密文加一個前綴是不明智的,因此開發(fā)者如果需要自定義用戶加載方式,可以在配置的同時手動指定加密方式:
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 自定義認證服務
auth.userDetailsService(userDetailsService)
// 配置 bCrypt 作為密碼加密方式
.passwordEncoder(new BCryptPasswordEncoder());
}
BCryptPasswordEncoder 在加密時會隨機生成鹽值,在與密碼明文進行匹配時,可以從密文中讀取到使用的鹽值,對明文加密后匹配。隨機生成鹽值的方式大大增強了密碼的安全性。
過濾器管理器 HttpSecurity 配置
當認證管理器初始化完成,WebSecurityConfigurerAdapter 會繼續(xù)配置 HttpSecurity,它用于配置 web 請求的安全配置,默認會應用到所有請求,開發(fā)者也可通過 RequestMatcher 配置例外。
來看看 HttpSecurity 的默認配置:
/**
* 創(chuàng)建 HttpSecurity 實例
*/
protected final HttpSecurity getHttp() throws Exception {
// ...
http = new HttpSecurity(objectPostProcessor, authenticationBuilder, sharedObjects);
if (!disableDefaults) {
http
// csrf 跨站請求偽造保護
.csrf().and()
// 配置異步支持
.addFilter(new WebAsyncManagerIntegrationFilter())
// security 異常處理
.exceptionHandling().and()
// 將請求的 header 寫入響應的 header
.headers().and()
// session 管理器,可以配置一個用戶僅有一個會話有效
.sessionManagement().and()
// 保存認證信息(session維度)
.securityContext().and()
// 保存 request cache
.requestCache().and()
// 匿名認證配置
.anonymous().and()
// 配置重載 servlet 相關安全方法
.servletApi().and()
// 表單登錄頁配置
.apply(new DefaultLoginPageConfigurer<>()).and()
// 匹配 /logout 做登出邏輯,成功后跳轉登錄頁
.logout();
// ...
}
// HttpSecurity 擴展配置
configure(http);
return http;
}
/**
* HttpSecurity 擴展配置
*/
protected void configure(HttpSecurity http) throws Exception {
http
// 約束基于 HttpServletRequest 的請求
.authorizeRequests()
// 任何請求 需要認證
.anyRequest().authenticated()
.and()
// 表單登錄
.formLogin().and()
// http basic 認證
.httpBasic();
}
與配置認證管理器相同的是,在配置 HttpSecurity 的過程中,留有一個名為 configure 的方法供開發(fā)者配置。默認的配置方法攔截了所有請求,要求必須經(jīng)過身份認證才能正確訪問 web 資源,默認有表單登錄和 http basic 兩種認證方式可以選擇。HttpSecurity 提供的大多數(shù)配置方法,都是通過過濾器實現(xiàn)的。
form login 表單登錄
formLogin 方法引入了 FormLoginConfigurer,此類中配置了兩個過濾器:
-
UsernamePasswordAuthenticationFilter:在創(chuàng)建過濾器時默認使用/login POST作為表單登錄請求,這個過濾器的過濾邏輯就是調(diào)用上文中配置的AuthenticationManager進行認證。
public UsernamePasswordAuthenticationFilter() {
super(new AntPathRequestMatcher("/login", "POST"));
}
如果認證成功,默認會保存認證信息,并重定向到相應的請求地址;如果認證失敗,默認會重定向到登錄頁面。
-
DefaultLoginPageGeneratingFilter:用于配置登錄頁面,登錄頁面默認的登錄、登出、登錄錯誤地址分別為/login /login?logout /login?error,其初始化配置在HttpSecurity的默認配置中。過濾邏輯為當請求為這 3 個地址時,會生成一個表單登錄的HTML并立即返回。
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
boolean loginError = isErrorPage(request);
boolean logoutSuccess = isLogoutSuccess(request);
// 登錄、登出或登錄失敗時跳轉到登錄頁。
if (isLoginUrlRequest(request) || loginError || logoutSuccess) {
// 生成 HTML 表單
String loginPageHtml = generateLoginPageHtml(request, loginError, logoutSuccess);
response.setContentType("text/html;charset=UTF-8");
response.setContentLength(loginPageHtml.getBytes(StandardCharsets.UTF_8).length);
response.getWriter().write(loginPageHtml);
return;
}
chain.doFilter(request, response);
}
http basic 認證
httpBasic 方法配置了 BasicAuthenticationFilter 過濾器,其過濾邏輯是從取出 Authorization 頭,請求頭內(nèi)容為 username:password 的 Base64 編碼形式。在獲取用戶名、密碼后,同樣調(diào)用 AuthenticationManager 進行認證。
csrf 跨站請求偽造保護
csrf 方法配置了 CsrfFilter,其過濾邏輯為默認放行 GET 等請求,其它請求需要進行 CsrfToken 校驗。訪問請求走到這個過濾器時,如果沒有攜帶 CsrfToken,會新生成并放入請求中。過濾器鏈繼續(xù)走到 DefaultLoginPageGeneratingFilter,由于在 DefaultLoginPageConfigurer 配置時,從請求中會取出 CsrfToken 交給 DefaultLoginPageGeneratingFilter,所以 CsrfToken 會一并生成 HTML 表單,我們使用默認的登錄頁面就能正確提交表單。
securityContext 認證上下文
securityContext 方法配置了 SecurityContextPersistenceFilter,其過濾邏輯為為每個會話創(chuàng)建一個 SecurityContext。
HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
// 從 session 中取出或在 session中設置 SecurityContext
SecurityContext contextBeforeChainExecution = repo.loadContext(holder);
SecurityContextHolder.setContext(contextBeforeChainExecution);
在 UsernamePasswordAuthenticationFilter 中,如果認證成功,則會調(diào)用 successfulAuthentication 方法,將認證成功的 Authentication 信息放入 SecurityContextHolder 中。開發(fā)者由此可以使用 SecurityContextHolder.getContext().getAuthentication(); 獲取當前會話的認證用戶信息。由于認證信息存放在 session 中,一旦用戶認證成功,當訪問其它請求時,經(jīng)由此過濾器時,就可以直接取得認證信息,安全通過過濾器鏈。
authorizeRequests 最后的守門員
在經(jīng)歷一系列過濾邏輯之后,請求來到 spring security 最后的過濾器 FilterSecurityInterceptor,此過濾器通過調(diào)用 authorizeRequests 方法加載 ExpressionUrlAuthorizationConfigurer 配置:
public void configure(H http) throws Exception {
// ...
FilterSecurityInterceptor securityInterceptor = createFilterSecurityInterceptor(
http, metadataSource, http.getSharedObject(AuthenticationManager.class));
// ...
http.addFilter(securityInterceptor);
http.setSharedObject(FilterSecurityInterceptor.class, securityInterceptor);
}
private FilterSecurityInterceptor createFilterSecurityInterceptor(H http,
FilterInvocationSecurityMetadataSource metadataSource,
AuthenticationManager authenticationManager) throws Exception {
FilterSecurityInterceptor securityInterceptor = new FilterSecurityInterceptor();
securityInterceptor.setSecurityMetadataSource(metadataSource);
// 創(chuàng)建權限驗證配置,默認為 AffirmativeBased,即滿足一項則鑒權成功
securityInterceptor.setAccessDecisionManager(getAccessDecisionManager(http));
// 配置認證管理器
securityInterceptor.setAuthenticationManager(authenticationManager);
securityInterceptor.afterPropertiesSet();
return securityInterceptor;
}
在此過濾器的邏輯中,視圖對此次訪問進行權限驗證,如果無權限,則會拋出 AccessDeniedException:
// Attempt authorization
try {
this.accessDecisionManager.decide(authenticated, object, attributes);
}
catch (AccessDeniedException accessDeniedException) {
publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated, accessDeniedException));
throw accessDeniedException;
}
一旦拋出了異常,順序排在 FilterSecurityInterceptor 前一位的過濾器 ExceptionTranslationFilter 正好就能捕捉到此異常。此過濾器在 HttpSecurity 通過調(diào)用exceptionHandling 方法配置 ExceptionHandlingConfigurer 聲明。過濾邏輯如下:
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);
logger.debug("Chain processed normally");
} catch (IOException ex) {
throw ex;
} catch (Exception ex) {
// 捕捉 FilterSecurityInterceptor 過濾邏輯拋出的異常
// ...
if (ase == null) {
ase = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType(
AccessDeniedException.class, causeChain);
}
if (ase != null) {
// ...
// 捕捉到 AccessDeniedException
handleSpringSecurityException(request, response, chain, ase);
}
else {
// ...
}
}
}
private void handleSpringSecurityException(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, RuntimeException exception)
throws IOException, ServletException {
if (exception instanceof AuthenticationException) {
// ...
// 認證異常
sendStartAuthentication(request, response, chain,
(AuthenticationException) exception);
} 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")));
}
else {
// ...
// 其它異常
accessDeniedHandler.handle(request, response,
(AccessDeniedException) exception);
}
}
}
protected void sendStartAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain,
AuthenticationException reason) throws ServletException, IOException {
// ...
SecurityContextHolder.getContext().setAuthentication(null);
// 緩存被中斷的請求
requestCache.saveRequest(request, response);
// ...
// 錯誤處理
authenticationEntryPoint.commence(request, response, reason);
}
ExceptionTranslationFilter 對 AuthenticationException 、 AccessDeniedException 和其它異常分別處理。FormLoginConfigurer 在初始化調(diào)用 init 方法時,對 ExceptionTranslationFilter 配置了 LoginUrlAuthenticationEntryPoint,因此對于認證和授權異常,會將請求重定向到登錄頁面。而對于其它異常,使用 AccessDeniedHandlerImpl,如果有錯誤頁面,跳轉到錯誤頁面;否則發(fā)送 403 錯誤給客戶端。
WebSecurityConfigurerAdapter#configure
在 WebSecurityConfigurerAdapter 中 void configure(WebSecurity web) 方法默認為空實現(xiàn),通過覆蓋此方法,開發(fā)者可以配置不需要經(jīng)過 Spring Security 認證的請求。
public void configure(WebSecurity web) throws Exception {
// 忽略指定url的請求(不走過濾器鏈)
web.ignoring().mvcMatchers("/**");
}
WebSecurity#performBuild
完成加載 WebSecurityConfigurerAdapter 中的配置后,進入 WebSecurity#performBuild,生成真正的安全過濾器鏈:
protected Filter performBuild() throws Exception {
// ...
int chainSize = ignoredRequests.size() + securityFilterChainBuilders.size();
List<SecurityFilterChain> securityFilterChains = new ArrayList<>(chainSize);
// 上文配置的需要忽略的請求
for (RequestMatcher ignoredRequest : ignoredRequests) {
securityFilterChains.add(new DefaultSecurityFilterChain(ignoredRequest));
}
// 上文配置的 HttpSecurity 實例
for (SecurityBuilder<? extends SecurityFilterChain> securityFilterChainBuilder : securityFilterChainBuilders) {
// 調(diào)用 build 方法生成 DefaultSecurityFilterChain
securityFilterChains.add(securityFilterChainBuilder.build());
}
// 過濾器鏈代理
FilterChainProxy filterChainProxy = new FilterChainProxy(securityFilterChains);
// ...
}
FilterChainProxy 執(zhí)行各過濾器
生成 FilterChainProxy 的過程中,先是加載開發(fā)者配置的需要忽略的請求,包裝到 DefaultSecurityFilterChain 中,生成一個沒有過濾器的過濾器鏈。隨之又調(diào)用 HttpSecurity#build 方法生成真正有過濾邏輯的過濾器鏈:
private FilterComparator comparator = new FilterComparator();
protected DefaultSecurityFilterChain performBuild() throws Exception {
// 先對配置的各過濾器進行排序,再加到過濾器鏈中
Collections.sort(filters, comparator);
return new DefaultSecurityFilterChain(requestMatcher, filters);
}
過濾器過濾順序排序依賴 FilterComparator。自此,springSecurityFilterChain 配置完成并被加入全局請求過濾器列表。
當請求經(jīng)由 FilterChainProxy 過濾時,先根據(jù) request 匹配過濾器:
private List<Filter> getFilters(HttpServletRequest request) {
for (SecurityFilterChain chain : filterChains) {
if (chain.matches(request)) {
return chain.getFilters();
}
}
return null;
}
當請求配置為忽略,會匹配到無過濾器的 DefaultSecurityFilterChain,因此無需經(jīng)歷 Spring Security 各過濾器;否則走正常過濾邏輯。
拿到請求匹配的過濾器列表后,FilterChainProxy 生成一個 VirtualFilterChain,Spring Security 相關過濾器的過濾邏輯均在此執(zhí)行,當相關過濾器執(zhí)行完畢,再回到 Spring MVC 的過濾器鏈上來繼續(xù)執(zhí)行。
private VirtualFilterChain(FirewalledRequest firewalledRequest, FilterChain chain, List<Filter> additionalFilters) {
// Spring MVC 過濾器鏈
this.originalChain = chain;
// 請求匹配到的過濾器列表
this.additionalFilters = additionalFilters;
this.size = additionalFilters.size();
this.firewalledRequest = firewalledRequest;
}
@Override
public void doFilter(ServletRequest request, ServletResponse response)
throws IOException, ServletException {
if (currentPosition == size) {
// ...
// Spring Security 相關過濾器執(zhí)行完畢,回到 Spring MVC 的過濾器鏈上來繼續(xù)執(zhí)行
originalChain.doFilter(request, response);
}
else {
currentPosition++;
Filter nextFilter = additionalFilters.get(currentPosition - 1);
// 按順序來執(zhí)行 Spring Security 相關過濾器
nextFilter.doFilter(request, response, this);
}
}
小結
(1) Spring Security 開箱即用,擁有完善的默認配置機制,基于過濾器對 web 應用進行保護。
(2) 如果開發(fā)者需要對 Spring Security 自動配置進行干預,可以繼承 WebSecurityConfigurerAdapter 并實現(xiàn)它的 3 個 configure 方法:
-
void configure(AuthenticationManagerBuilder auth):配置認證管理器,開發(fā)者需要實現(xiàn)UserDetailsService接口,編寫自定義認證邏輯,并將接口實現(xiàn)注冊到Spring容器,在此方法中指定認證邏輯實現(xiàn)。
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
}
-
void configure(HttpSecurity http):配置過濾器管理器,開發(fā)者在此方法中對默認的HttpSecurity進行修改:
protected void configure(HttpSecurity http) throws Exception {
http
// 表單登錄
.formLogin().and()
// 關閉 csrf 保護
.csrf().disable()
// 任何請求都需要認證
.authorizeRequests().anyRequest().authenticated();
}
-
void configure(WebSecurity web):請求忽略配置,開發(fā)者在此可以配置不需要進行安全認證的請求:
public void configure(WebSecurity web) throws Exception {
// 忽略指定url的請求(不走過濾器鏈)
web.ignoring().mvcMatchers("/**");
}
(3)重要的定義
-
springSecurityFilterChain:Spring Security過濾器鏈。 -
AuthenticationManager:認證管理器,負責對用戶身份進行認證。 -
AuthenticationProvider:認證邏輯具體實現(xiàn),由認證管理器調(diào)用。 -
UserDetailsService:通過username認證,包裝成AuthenticationProvider使用。 -
SecurityContextPersistenceFilter:從會話中加載有效認證信息或創(chuàng)建默認認證信息上下文。 -
FilterSecurityInterceptor:最終決定是否放行請求,如果需要認證而未認證,或沒有相應的權限,都會判斷請求失敗。 -
FilterChainProxy:Spring Security過濾器代理,關于安全的過濾邏輯均在此過濾器中執(zhí)行,執(zhí)行完成后才回到Spring MVC過濾器鏈中繼續(xù)執(zhí)行。