Spring Security 源碼分析(一):過濾器鏈

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 配置,其在啟動時裝配名為 springSecurityFilterChainbean

  @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-autoconfigurespring.factories 中標識 SecurityAutoConfiguration 需要自動裝配,在裝配過程中又將 SpringBootWebSecurityConfiguration 引入,在此類中聲明了一個繼承自 WebSecurityConfigurerAdapterDefaultConfigurerAdapter,由此裝配了對 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 -> performBuildSpring Security 中的 AuthenticationManagerBuilder (認證管理器生成配置)、HttpSecurity (過濾器管理器生成配置)、WebSecurity (過濾器生成配置) 都是繼承 AbstractConfiguredSecurityBuilder 通過這個鏈條生成目標對象,這 3 個配置也是 Spring Security 的配置核心。

SecurityBuilder

WebSecurityConfigurerAdapter 配置

由上文可知,過濾器鏈生成過程中調(diào)用了 WebSecurityConfigurerAdapterinitconfigure 方法。

init 方法首先調(diào)用了 getHttp 方法,用于生成 AuthenticationManagerHttpSecurity 實例。

認證管理器 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 方法配置認證管理器,當 disableLocalConfigureAuthenticationBldrtrue 時會調(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)用注入進來的 UserDetailsServiceloadUserByUsername 方法。

  protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
    // ...

    UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);

    // ...
  }

取出賬戶信息后,AbstractUserDetailsAuthenticationProvider#authenticate 方法進行 check,如果密碼錯誤,或者未激活、過期等都會拋出 AuthenticationException 異常。

DaoAuthenticationProvider

在不自定義任何配置的情況下啟動引入 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ā)者需要干預認證管理器的生成,同樣可以提供這兩類認證邏輯。
先看如何干預:disableLocalConfigureAuthenticationBldrWebSecurityConfigurerAdapter 中默認為 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) AuthenticationProviderUserDetailsService 接口,填充自定義用戶邏輯。在注入自定義用戶認證邏輯時,實際還是包裝為
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:passwordBase64 編碼形式。在獲取用戶名、密碼后,同樣調(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);
  }

ExceptionTranslationFilterAuthenticationException 、 AccessDeniedException 和其它異常分別處理。FormLoginConfigurer 在初始化調(diào)用 init 方法時,對 ExceptionTranslationFilter 配置了 LoginUrlAuthenticationEntryPoint,因此對于認證和授權異常,會將請求重定向到登錄頁面。而對于其它異常,使用 AccessDeniedHandlerImpl,如果有錯誤頁面,跳轉到錯誤頁面;否則發(fā)送 403 錯誤給客戶端。

WebSecurityConfigurerAdapter#configure

WebSecurityConfigurerAdaptervoid 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)重要的定義

  • springSecurityFilterChainSpring Security 過濾器鏈。
  • AuthenticationManager:認證管理器,負責對用戶身份進行認證。
  • AuthenticationProvider:認證邏輯具體實現(xiàn),由認證管理器調(diào)用。
  • UserDetailsService:通過 username 認證,包裝成 AuthenticationProvider 使用。
  • SecurityContextPersistenceFilter:從會話中加載有效認證信息或創(chuàng)建默認認證信息上下文。
  • FilterSecurityInterceptor:最終決定是否放行請求,如果需要認證而未認證,或沒有相應的權限,都會判斷請求失敗。
  • FilterChainProxySpring Security 過濾器代理,關于安全的過濾邏輯均在此過濾器中執(zhí)行,執(zhí)行完成后才回到 Spring MVC 過濾器鏈中繼續(xù)執(zhí)行。
最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容