2019-07-24 Spring Security核心流程

Spring Security

學(xué)習(xí) Spring Security 用法,架構(gòu)、設(shè)計(jì)模式等。

核心組件

主要介紹 Spring Security 中常見(jiàn)核心 Java 類(lèi)以及他們之間的依賴(lài)關(guān)系,以及整個(gè)架構(gòu)的設(shè)計(jì)原理。

SecurityContextHolder

SecurityContextHolder 用于存儲(chǔ)安全上下文(security context)的信息。當(dāng)前操作的用戶(hù)是誰(shuí),該用戶(hù)是否已經(jīng)被認(rèn)證,他擁有哪些角色權(quán)限…這些都被保存在 SecurityContextHolder中。SecurityContextHolder 默認(rèn)使用 ThreadLocal 策略來(lái)存儲(chǔ)認(rèn)證信息??吹?ThreadLocal 也就意味著,這是一種與線(xiàn)程綁定的策略。Spring Security 在用戶(hù)登錄時(shí)自動(dòng)綁定認(rèn)證信息到當(dāng)前線(xiàn)程,在用戶(hù)退出時(shí),自動(dòng)清除當(dāng)前線(xiàn)程的認(rèn)證信息。但這一切的前提,是你在 web 場(chǎng)景下使用 Spring Security

獲取當(dāng)前用戶(hù)的信息

因?yàn)樯矸菪畔⑹桥c線(xiàn)程綁定的,所以可以在程序的任何地方使用靜態(tài)方法獲取用戶(hù)信息。一個(gè)例子如下:

Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();

if (principal instanceof UserDetails) {
String username = ((UserDetails)principal).getUsername();
} else {
String username = principal.toString();
}

getAuthentication() 返回了認(rèn)證信息,再次 getPrincipal() 返回了身份信息,UserDetails便是 Spring 對(duì)身份信息封裝的一個(gè)接口。AuthenticationUserDetails 的介紹在下面的小節(jié)具體講解,本節(jié)重要的內(nèi)容是介紹 SecurityContextHolder 這個(gè)容器。

Authentication

直接上源碼:

package org.springframework.security.core;// <1>

public interface Authentication extends Principal, Serializable { // <1>
    Collection<? extends GrantedAuthority> getAuthorities(); // <2>

    Object getCredentials();// <2>

    Object getDetails();// <2>

    Object getPrincipal();// <2>

    boolean isAuthenticated();// <2>

    void setAuthenticated(boolean var1) throws IllegalArgumentException;
}

<1> Authentication 是 spring security 包中的接口,直接繼承自 Principal 類(lèi),而 Principal 是位于 java.security 包中的??梢砸?jiàn)得,Authentication 在 spring security 中是最高級(jí)別的身份/認(rèn)證的抽象。

<2> 由這個(gè)頂級(jí)接口,我們可以得到用戶(hù)擁有的權(quán)限信息列表,密碼,用戶(hù)細(xì)節(jié)信息,用戶(hù)身份信息,認(rèn)證信息。

還記得1.1節(jié)中,authentication.getPrincipal()返回了一個(gè) Object,我們將 Principal 強(qiáng)轉(zhuǎn)成了 Spring Security 中最常用的UserDetails,這在 Spring Security 中非常常見(jiàn),接口返回 Object,使用 instanceof 判斷類(lèi)型,強(qiáng)轉(zhuǎn)成對(duì)應(yīng)的具體實(shí)現(xiàn)類(lèi)。接口詳細(xì)解讀如下:

  • getAuthorities(),權(quán)限信息列表,默認(rèn)是 GrantedAuthority 接口的一些實(shí)現(xiàn)類(lèi),通常是代表權(quán)限信息的一系列字符串。
  • getCredentials(),密碼信息,用戶(hù)輸入的密碼字符串,在認(rèn)證過(guò)后通常會(huì)被移除,用于保障安全。
  • getDetails(),細(xì)節(jié)信息,web 應(yīng)用中的實(shí)現(xiàn)接口通常為 WebAuthenticationDetails,它記錄了訪問(wèn)者的 ip 地址和 sessionId 的值。
  • getPrincipal(),敲黑板!??!最重要的身份信息,大部分情況下返回的是 UserDetails 接口的實(shí)現(xiàn)類(lèi),也是框架中的常用接口之一。UserDetails 接口將會(huì)在下面的小節(jié)重點(diǎn)介紹。

Spring Security是如何完成身份認(rèn)證的?

  1. 用戶(hù)名和密碼被過(guò)濾器獲取到,封裝成 Authentication ,通常情況下是 UsernamePasswordAuthenticationToken 這個(gè)實(shí)現(xiàn)類(lèi)。
  2. AuthenticationManager 身份管理器負(fù)責(zé)驗(yàn)證這個(gè) Authentication
  3. 認(rèn)證成功后,AuthenticationManager 身份管理器返回一個(gè)被填充滿(mǎn)了信息的(包括上面提到的權(quán)限信息,身份信息,細(xì)節(jié)信息,但密碼通常會(huì)被移除)Authentication 實(shí)例。
  4. SecurityContextHolder 安全上下文容器將第3步填充了信息的 Authentication,通過(guò) SecurityContextHolder.getContext().setAuthentication(…) 方法,設(shè)置到其中。

這是一個(gè)抽象的認(rèn)證流程,而整個(gè)過(guò)程中,如果不糾結(jié)于細(xì)節(jié),其實(shí)只剩下一個(gè) AuthenticationManager 是我們沒(méi)有接觸過(guò)的了,這個(gè)身份管理器我們?cè)诤竺娴男」?jié)介紹。

AuthenticationManager

初次接觸 Spring Security 的朋友相信會(huì)被 AuthenticationManager,ProviderManagerAuthenticationProvider …這么多相似的 Spring 認(rèn)證類(lèi)搞得暈頭轉(zhuǎn)向,但只要稍微梳理一下就可以理解清楚它們的聯(lián)系和設(shè)計(jì)者的用意。AuthenticationManager(接口)是認(rèn)證相關(guān)的核心接口,也是發(fā)起認(rèn)證的出發(fā)點(diǎn),因?yàn)樵趯?shí)際需求中,我們可能會(huì)允許用戶(hù)使用用戶(hù)名+密碼登錄,同時(shí)允許用戶(hù)使用郵箱+密碼,手機(jī)號(hào)碼+密碼登錄,甚至,可能允許用戶(hù)使用指紋登錄(還有這樣的操作?沒(méi)想到吧),所以說(shuō) AuthenticationManager 一般不直接認(rèn)證,AuthenticationManager 接口的常用實(shí)現(xiàn)類(lèi) ProviderManager 內(nèi)部會(huì)維護(hù)一個(gè) List<AuthenticationProvider> 列表,存放多種認(rèn)證方式,實(shí)際上這是委托者模式的應(yīng)用(Delegate)。也就是說(shuō),核心的認(rèn)證入口始終只有一個(gè):AuthenticationManager,不同的認(rèn)證方式:用戶(hù)名+密碼(UsernamePasswordAuthenticationToken),郵箱+密碼,手機(jī)號(hào)碼+密碼登錄則對(duì)應(yīng)了三個(gè) AuthenticationProvider。這樣一來(lái)四不四就好理解多了?熟悉 shiro 的朋友可以把AuthenticationProvider 理解成 Realm。在默認(rèn)策略下,只需要通過(guò)一個(gè) AuthenticationProvider 的認(rèn)證,即可被認(rèn)為是登錄成功。

ProviderManager 關(guān)鍵部分源碼

public class ProviderManager implements AuthenticationManager, MessageSourceAware,
        InitializingBean {

    // 維護(hù)一個(gè)AuthenticationProvider列表
    private List<AuthenticationProvider> providers = Collections.emptyList();

    public Authentication authenticate(Authentication authentication)
          throws AuthenticationException {
       Class<? extends Authentication> toTest = authentication.getClass();
       AuthenticationException lastException = null;
       Authentication result = null;

       // 依次認(rèn)證
       for (AuthenticationProvider provider : getProviders()) {
          if (!provider.supports(toTest)) {
             continue;
          }
          try {
             result = provider.authenticate(authentication);

             if (result != null) {
                copyDetails(authentication, result);
                break;
             }
          }
          ...
          catch (AuthenticationException e) {
             lastException = e;
          }
       }
       // 如果有Authentication信息,則直接返回
       if (result != null) {
            if (eraseCredentialsAfterAuthentication
                    && (result instanceof CredentialsContainer)) {
                 //移除密碼
                ((CredentialsContainer) result).eraseCredentials();
            }
             //發(fā)布登錄成功事件
            eventPublisher.publishAuthenticationSuccess(result);
            return result;
       }
       ...
       //執(zhí)行到此,說(shuō)明沒(méi)有認(rèn)證成功,包裝異常信息
       if (lastException == null) {
          lastException = new ProviderNotFoundException(messages.getMessage(
                "ProviderManager.providerNotFound",
                new Object[] { toTest.getName() },
                "No AuthenticationProvider found for {0}"));
       }
       prepareException(lastException, authentication);
       throw lastException;
    }
}

ProviderManager 中的 List ,會(huì)依照次序去認(rèn)證,認(rèn)證成功則立即返回,若認(rèn)證失敗則返回 null,下一個(gè) AuthenticationProvider 會(huì)繼續(xù)嘗試認(rèn)證,如果所有認(rèn)證器都無(wú)法認(rèn)證成功,則 ProviderManager 會(huì)拋出一個(gè)ProviderNotFoundException 異常。

以上已經(jīng)把 Spring Security 的整個(gè)認(rèn)證流程都講述了一遍,簡(jiǎn)單小結(jié)如下:身份信息的存放容器 SecurityContextHolder ,身份信息的抽象 Authentication ,身份認(rèn)證器 AuthenticationManager 及其認(rèn)證流程。姑且在這里做一個(gè)分隔線(xiàn)。下面來(lái)介紹下 AuthenticationProvider 接口的具體實(shí)現(xiàn)。

DaoAuthenticationProvider

AuthenticationProvider 最最最常用的一個(gè)實(shí)現(xiàn)便是 DaoAuthenticationProvider 。顧名思義,Dao 正是數(shù)據(jù)訪問(wèn)層的縮寫(xiě),也暗示了這個(gè)身份認(rèn)證器的實(shí)現(xiàn)思路。由于本文是一個(gè) Overview ,姑且只給出其 UML 類(lèi)圖:

按照我們最直觀的思路,怎么去認(rèn)證一個(gè)用戶(hù)呢?用戶(hù)前臺(tái)提交了用戶(hù)名和密碼,而數(shù)據(jù)庫(kù)中保存了用戶(hù)名和密碼,認(rèn)證便是負(fù)責(zé)比對(duì)同一個(gè)用戶(hù)名,提交的密碼和保存的密碼是否相同便是了。在Spring Security 中。提交的用戶(hù)名和密碼,被封裝成了UsernamePasswordAuthenticationToken ,而根據(jù)用戶(hù)名加載用戶(hù)的任務(wù)則是交給了 UserDetailsService ,在DaoAuthenticationProvider 中,對(duì)應(yīng)的方法便是 retrieveUser ,雖然有兩個(gè)參數(shù),但是 retrieveUser 只有第一個(gè)參數(shù)起主要作,返回一個(gè) UserDetails。還需要完成 UsernamePasswordAuthenticationTokenUserDetails 密碼的比對(duì),這便是交給 additionalAuthenticationChecks 方法完成的,如果這個(gè) void 方法沒(méi)有拋異常,則認(rèn)為比對(duì)成功。比對(duì)密碼的過(guò)程,用到了PasswordEncoderSaltSource ,密碼加密和鹽的概念相信不用我贅述了,它們?yōu)楸U习踩O(shè)計(jì),都是比較基礎(chǔ)的概念。

如果你已經(jīng)被這些概念搞得暈頭轉(zhuǎn)向了,不妨這么理解 DaoAuthenticationProvider :它獲取用戶(hù)提交的用戶(hù)名和密碼,比對(duì)其正確性,如果正確,返回一個(gè)數(shù)據(jù)庫(kù)中的用戶(hù)信息(假設(shè)用戶(hù)信息被保存在數(shù)據(jù)庫(kù)中)。

UserDetails 與 UserDetailsService

上面不斷提到了 UserDetails 這個(gè)接口,它代表了最詳細(xì)的用戶(hù)信息,這個(gè)接口涵蓋了一些必要的用戶(hù)信息字段,具體的實(shí)現(xiàn)類(lèi)對(duì)它進(jìn)行了擴(kuò)展。

public interface UserDetails extends Serializable {

   Collection<? extends GrantedAuthority> getAuthorities();

   String getPassword();

   String getUsername();

   boolean isAccountNonExpired();

   boolean isAccountNonLocked();

   boolean isCredentialsNonExpired();

   boolean isEnabled();
}

它和 Authentication 接口很類(lèi)似,比如它們都擁有 username ,authorities ,區(qū)分他們也是本文的重點(diǎn)內(nèi)容之一。AuthenticationgetCredentials()UserDetails 中的 getPassword() 需要被區(qū)分對(duì)待,前者是用戶(hù)提交的密碼憑證,后者是用戶(hù)正確的密碼,認(rèn)證器其實(shí)就是對(duì)這兩者的比對(duì)。Authentication 中的 getAuthorities() 實(shí)際是由 UserDetailsgetAuthorities() 傳遞而形成的。還記得 Authentication 接口中的 getUserDetails() 方法嗎?其中的 UserDetails 用戶(hù)詳細(xì)信息便是經(jīng)過(guò)了 AuthenticationProvider 之后被填充的。

public interface UserDetailsService {
   UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

UserDetailsServiceAuthenticationProvider 兩者的職責(zé)常常被人們搞混,關(guān)于他們的問(wèn)題在文檔的 FAQissues 中屢見(jiàn)不鮮。記住一點(diǎn)即可,敲黑板?。。?code>UserDetailsService 只負(fù)責(zé)從特定的地方(通常是數(shù)據(jù)庫(kù))加載用戶(hù)信息,僅此而已,記住這一點(diǎn),可以避免走很多彎路。UserDetailsService 常見(jiàn)的實(shí)現(xiàn)類(lèi)有 JdbcDaoImpl,InMemoryUserDetailsManager,前者從數(shù)據(jù)庫(kù)加載用戶(hù),后者從內(nèi)存中加載用戶(hù),也可以自己實(shí)現(xiàn) UserDetailsService,通常這更加靈活。

架構(gòu)概覽圖

為了更加形象的理解上述我介紹的這些核心類(lèi),附上一張按照我的理解,所畫(huà)出 Spring Security 的一張非典型的 UML

整個(gè)Spring Security 都是圍繞以上這張架構(gòu)圖展開(kāi)的,最頂層為最核心最抽象的 AuthenticationManager 接口, ProviderManagerAuthenticationManager 的一個(gè)具體實(shí)現(xiàn),功能如其名字他的作用是管理 Provider 的, AuthenticationProvider 才是真正認(rèn)證的接口,因此我們?cè)趯?shí)踐中要實(shí)現(xiàn)我們自己的認(rèn)證方式,也就是 AuthenticationProvider 的一個(gè)具體實(shí)現(xiàn)。當(dāng)然可以實(shí)現(xiàn)多個(gè),如果有多種認(rèn)證方式,現(xiàn)實(shí)中往往也是有多種認(rèn)證方式。

注意 AuthenticationProvider 接口中的 supports 方法,ProviderManager 里面其實(shí)是維護(hù)了一個(gè) AuthenticationProviderList 因此到底使用哪個(gè) Provider 來(lái)做驗(yàn)證,可以使用 Filter 返回的 Authentication 實(shí)現(xiàn)類(lèi)來(lái)限定,部分代碼如下:

//Filter 代碼
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException, IOException, ServletException {
            ......
        return getAuthenticationManager().authenticate(new JwtAuthenticationToken(token));
    }
//Provider 代碼
    @Override
    public boolean supports(Class<?> authentication) {
        return (JwtAuthenticationToken.class.isAssignableFrom(authentication));
    } 
//在 ProviderManager 中驗(yàn)證時(shí)有如下代碼
for (AuthenticationProvider provider : getProviders()) {
   if (!provider.supports(toTest)) {
      continue;
   }

如果對(duì)Spring Security 的這些概念感到理解不能,不用擔(dān)心,因?yàn)檫@是 Architecture First 導(dǎo)致的必然結(jié)果,先過(guò)個(gè)眼熟。后續(xù)的文章會(huì)秉持 Code First 的理念,陸續(xù)詳細(xì)地講解這些實(shí)現(xiàn)類(lèi)的使用場(chǎng)景,源碼分析,以及最基本的:如何配置 Spring Security ,在后面可以時(shí)不時(shí)往回看一看,找到具體的類(lèi)在整個(gè)架構(gòu)中所處的位置。另外,一些 Spring Security的過(guò)濾器還未囊括在架構(gòu)概覽中,如將表單信息包裝成 UsernamePasswordAuthenticationToken 的過(guò)濾器,考慮到這些雖然也是架構(gòu)的一部分,但是真正重寫(xiě)他們的可能性較小,所以打算放到后面講解。

配置介紹

Security 安全核心配置例子:

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/", "/home").permitAll()
                .anyRequest().authenticated()
                .and()
            .formLogin()
                .loginPage("/login").permitAll()
                .and()
            .logout()
                .permitAll();
    }

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth
            .inMemoryAuthentication()
                .withUser("admin").password("admin").roles("USER");
    }
}

當(dāng)配置了上述的javaconfig之后,我們的應(yīng)用便具備了如下的功能:

  • 除了“/”,”/home”(首頁(yè)),”/login”(登錄),”/logout”(注銷(xiāo)),之外,其他路徑都需要認(rèn)證。
  • 指定“/login”該路徑為登錄頁(yè)面,當(dāng)未認(rèn)證的用戶(hù)嘗試訪問(wèn)任何受保護(hù)的資源時(shí),都會(huì)跳轉(zhuǎn)到“/login”。
  • 默認(rèn)指定“/logout”為注銷(xiāo)頁(yè)面
  • 配置一個(gè)內(nèi)存中的用戶(hù)認(rèn)證器,使用admin/admin作為用戶(hù)名和密碼,具有USER角色
  • 防止CSRF攻擊
  • Session Fixation protection
  • Security Header(添加一系列和Header相關(guān)的控制)
    • HTTP Strict Transport Security for secure requests
    • 集成X-Content-Type-Options
    • 緩存控制
    • 集成X-XSS-Protection.aspx)
    • X-Frame-Options integration to help prevent Clickjacking(iframe被默認(rèn)禁止使用)
  • 為Servlet API集成了如下的幾個(gè)方法
    • HttpServletRequest#getRemoteUser())
    • HttpServletRequest.html#getUserPrincipal())
    • HttpServletRequest.html#isUserInRole(java.lang.String))
    • HttpServletRequest.html#login(java.lang.String, java.lang.String))
    • HttpServletRequest.html#logout())

一個(gè) Restful 配置

動(dòng)態(tài)從數(shù)據(jù)庫(kù)加載權(quán)限,這邊因?yàn)?Spring Security 的機(jī)制是在模塊啟動(dòng)的時(shí)候進(jìn)行加載的,如果想要?jiǎng)討B(tài) reload 權(quán)限,調(diào)查來(lái)看Spring 并沒(méi)有提供相關(guān)的接口,需要?jiǎng)討B(tài) reload Spring 相關(guān)的 bean 是一種比較危險(xiǎn)暴力的做法,需要多加注意。

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        List<String> permitAllEndpointList = Arrays.asList(AUTHENTICATION_URL,"/api/*/webjars/**","/api/*/swagger**","/api/*/swagger-resources/**","/api/*/v2/api-docs");
        //load Authorities from DB
        ResponseData<List<Authority>> authorityRules = authorityService.getAuthority();
        
        try {
            for (AuthorityModel rule : authorityRules.getData()) {
                //這里因?yàn)?Restful API url 有相同的情況,因此需要URL和方法名組合來(lái)區(qū)分,注意一種特殊情況,
                //因?yàn)槭鞘褂肁nt語(yǔ)法來(lái)匹配URL,如果出現(xiàn)請(qǐng)求方法相同且URL是匹配子集關(guān)系時(shí),要把最具體的URL放在
                //前面,比如同是 GET 方法,Api1:/api/user/paged 和 Api2:/api/user/{id},在Ant語(yǔ)法里面
                //Api2是能匹配Api1的,如果用戶(hù)有Api2的權(quán)限而沒(méi)有Api1的權(quán)限,在如下初始化權(quán)限時(shí)Api2初始化的
                //順序在Api1的前面,就會(huì)導(dǎo)致用戶(hù)即使沒(méi)有Api1的權(quán)限也能訪問(wèn),因此要確保數(shù)據(jù)庫(kù)加載的時(shí)候Api1在Api2
                //前面
                if (HttpMethod.POST.name().equals(rule.getMethod().name())) {
                    http
                            .authorizeRequests()
                            .antMatchers(HttpMethod.POST, rule.getPattern()).hasAuthority(rule.getSystemName());
                } else if (HttpMethod.GET.name().equals(rule.getMethod().name())) {
                    http
                            .authorizeRequests()
                            .antMatchers(HttpMethod.GET, rule.getPattern()).hasAuthority(rule.getSystemName());
                } else if (HttpMethod.PUT.name().equals(rule.getMethod().name())) {
                    http
                            .authorizeRequests()
                            .antMatchers(HttpMethod.PUT, rule.getPattern()).hasAuthority(rule.getSystemName());
                } else if (HttpMethod.DELETE.name().equals(rule.getMethod().name())) {
                    http
                            .authorizeRequests()
                            .antMatchers(HttpMethod.DELETE, rule.getPattern()).hasAuthority(rule.getSystemName());
                }
            }
        } catch (Exception e) {
            log.error("", e);
        }

        http
            .csrf().disable() // We don't need CSRF for JWT based authentication
            .exceptionHandling().authenticationEntryPoint(this.authenticationEntryPoint)
                //自定義沒(méi)有權(quán)限時(shí)的處理,一般根據(jù)業(yè)務(wù)封裝自定義的返回結(jié)果
                .accessDeniedHandler(ajaxAccessDeniedHandler)

            .and()
                //Restful API 完全無(wú)狀態(tài),使用JWT token
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
                //定義哪些API不需要認(rèn)證,比如獲取 Token 的接口
                .authorizeRequests()
                .antMatchers(permitAllEndpointList.toArray(new String[permitAllEndpointList.size()])).permitAll()
            .and()
                // Protected API End-points
                .authorizeRequests()
                .antMatchers(API_ROOT_URL, API_ATTACHMENT_URL).authenticated()
            .and()
                //處理跨域過(guò)濾器
                .addFilterBefore(new CustomCorsFilter(), UsernamePasswordAuthenticationFilter.class)
                //處理登錄獲取token的過(guò)濾器
                .addFilterBefore(buildAjaxLoginProcessingFilter(AUTHENTICATION_URL), UsernamePasswordAuthenticationFilter.class)
                //驗(yàn)證用戶(hù)token,以及用戶(hù)是否有接口訪問(wèn)權(quán)限的過(guò)濾器
                .addFilterBefore(buildJwtTokenAuthenticationProcessingFilter(permitAllEndpointList, Arrays.asList(API_ROOT_URL,API_ATTACHMENT_URL)), UsernamePasswordAuthenticationFilter.class);
    }

具體解釋見(jiàn)代碼上的注釋

@EnableWebSecurity

我們自己定義的配置類(lèi) WebSecurityConfig 加上了 @EnableWebSecurity 注解,同時(shí)繼承了 WebSecurityConfigurerAdapter。你可能會(huì)在想誰(shuí)的作用大一點(diǎn),毫無(wú)疑問(wèn) @EnableWebSecurity起到?jīng)Q定性的配置作用,它其實(shí)是個(gè)組合注解。

@Retention(value = java.lang.annotation.RetentionPolicy.RUNTIME)
@Target(value = { java.lang.annotation.ElementType.TYPE })
@Documented
@Import({ WebSecurityConfiguration.class,// <2>
        SpringWebMvcImportSelector.class })// <1>
@EnableGlobalAuthentication // <3>
@Configuration
public @interface EnableWebSecurity {
    boolean debug() default false;
}
  • <1> @Importspringboot 提供的用于引入外部的配置的注解,可以理解為:@EnableWebSecurity 注解激活了 @Import 注解中包含的配置類(lèi)。
  • <2> WebSecurityConfiguration 顧名思義,是用來(lái)配置 web 安全的,下面的小節(jié)會(huì)詳細(xì)介紹。
  • <3> @EnableGlobalAuthentication 注解的源碼如下:
@Import(AuthenticationConfiguration.class)
@Configuration
public @interface EnableGlobalAuthentication {
}

注意點(diǎn)同樣在 @Import 之中,它實(shí)際上激活了 AuthenticationConfiguration 這樣的一個(gè)配置類(lèi),用來(lái)配置認(rèn)證相關(guān)的核心類(lèi)。

也就是說(shuō):@EnableWebSecurity完成的工作便是加載了WebSecurityConfigurationAuthenticationConfiguration 這兩個(gè)核心配置類(lèi),也就此將 spring security 的職責(zé)劃分為了配置安全信息,配置認(rèn)證信息兩部分。

WebSecurityConfiguration

在這個(gè)配置類(lèi)中,有一個(gè)非常重要的Bean被注冊(cè)了。

@Configuration
public class WebSecurityConfiguration {
    //DEFAULT_FILTER_NAME = "springSecurityFilterChain"
    @Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)
    public Filter springSecurityFilterChain() throws Exception {
        ...
    }
 }

在未使用springboot之前,大多數(shù)人都應(yīng)該對(duì)springSecurityFilterChain這個(gè)名詞不會(huì)陌生,他是spring security的核心過(guò)濾器,是整個(gè)認(rèn)證的入口。在曾經(jīng)的XML配置中,想要啟用spring security,需要在web.xml中進(jìn)行如下配置:

<!-- Spring Security -->
   <filter>
       <filter-name>springSecurityFilterChain</filter-name>
       <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
   </filter>

   <filter-mapping>
       <filter-name>springSecurityFilterChain</filter-name>
       <url-pattern>/*</url-pattern>
   </filter-mapping>

而在springboot集成之后,這樣的XMLjava配置取代。WebSecurityConfiguration中完成了聲明springSecurityFilterChain的作用,并且最終交給DelegatingFilterProxy這個(gè)代理類(lèi),負(fù)責(zé)攔截請(qǐng)求(注意DelegatingFilterProxy這個(gè)類(lèi)不是spring security包中的,而是存在于web包中,spring使用了代理模式來(lái)實(shí)現(xiàn)安全過(guò)濾的解耦)。

AuthenticationConfiguration

@Configuration
@Import(ObjectPostProcessorConfiguration.class)
public class AuthenticationConfiguration {
    @Bean
    public AuthenticationManagerBuilder authenticationManagerBuilder(
            ObjectPostProcessor<Object> objectPostProcessor) {
        return new AuthenticationManagerBuilder(objectPostProcessor);
    }
    public AuthenticationManager getAuthenticationManager() throws Exception {
        ...
    }
}

AuthenticationConfiguration 的主要任務(wù),便是負(fù)責(zé)生成全局的身份認(rèn)證管理者 AuthenticationManager。還記得在《初識(shí) Spring Security 篇一》中,介紹了 Spring Security 的認(rèn)證體系,AuthenticationManager 便是最核心的身份認(rèn)證管理器。

WebSecurityConfigurerAdapter

適配器模式在 spring 中被廣泛的使用,在配置中使用 Adapter 的好處便是,我們可以選擇性的配置想要修改的那一部分配置,而不用覆蓋其他不相關(guān)的配置。WebSecurityConfigurerAdapter 中我們可以選擇自己想要修改的內(nèi)容,來(lái)進(jìn)行重寫(xiě),而其提供了三個(gè) configure 重載方法,是我們主要關(guān)心的:

    /**WebSecurityConfigurerAdapter**/

    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        this.disableLocalConfigureAuthenticationBldr = true;
    }
    ...
        /**
     * Override this method to configure {@link WebSecurity}. For example, if you wish to
     * ignore certain requests.
     */
    public void configure(WebSecurity web) throws Exception {
    }
    /**
     * Override this method to configure the {@link HttpSecurity}. Typically subclasses
     * should not invoke this method by calling super as it may override their
     * configuration. The default configuration is:
     *
     * <pre>
     * http.authorizeRequests().anyRequest().authenticated().and().formLogin().and().httpBasic();
     * </pre>
     *
     * @param http the {@link HttpSecurity} to modify
     * @throws Exception if an error occurs
     */
    // @formatter:off
    protected void configure(HttpSecurity http) throws Exception {
        logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity).");

        http
            .authorizeRequests()
                .anyRequest().authenticated()
                .and()
            .formLogin().and()
            .httpBasic();
    }

由參數(shù)就可以知道,分別是對(duì) AuthenticationManagerBuilder,WebSecurity,HttpSecurity進(jìn)行個(gè)性化的配置。

HttpSecurity常用配置

@Configuration
@EnableWebSecurity
public class CustomWebSecurityConfig extends WebSecurityConfigurerAdapter {
  
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/resources/**", "/signup", "/about").permitAll()
                .antMatchers("/admin/**").hasRole("ADMIN")
                .antMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')")
                .anyRequest().authenticated()
                .and()
            .formLogin()
                .usernameParameter("username")
                .passwordParameter("password")
                .failureForwardUrl("/login?error")
                .loginPage("/login")
                .permitAll()
                .and()
            .logout()
                .logoutUrl("/logout")
                .logoutSuccessUrl("/index")
                .permitAll()
                .and()
            .httpBasic()
                .disable();
    }
}

上述是一個(gè)使用 Java Configuration 配置 HttpSecurity 的典型配置,其中 http 作為根開(kāi)始配置,每一個(gè) and() 對(duì)應(yīng)了一個(gè)模塊的配置(等同于xml配置中的結(jié)束標(biāo)簽),并且 and() 返回了 HttpSecurity 本身,于是可以連續(xù)進(jìn)行配置。他們配置的含義也非常容易通過(guò)變量本身來(lái)推測(cè)

  • authorizeRequests()配置路徑攔截,表明路徑訪問(wèn)所對(duì)應(yīng)的權(quán)限,角色,認(rèn)證信息。
  • formLogin()對(duì)應(yīng)表單認(rèn)證相關(guān)的配置
  • logout()對(duì)應(yīng)了注銷(xiāo)相關(guān)的配置
  • httpBasic()可以配置basic登錄

他們分別代表了 http 請(qǐng)求相關(guān)的安全配置,這些配置項(xiàng)無(wú)一例外的返回了 Configurer 類(lèi),而所有的 http 相關(guān)配置可以通過(guò)查看HttpSecurity的主要方法得知:

    public LogoutConfigurer<HttpSecurity> logout() throws Exception {
        return getOrApply(new LogoutConfigurer<HttpSecurity>());
    }
    ......
        public CsrfConfigurer<HttpSecurity> csrf() throws Exception {
        ApplicationContext context = getContext();
        return getOrApply(new CsrfConfigurer<HttpSecurity>(context));
    }

需要對(duì) http 協(xié)議有一定的了解才能完全掌握所有的配置,不過(guò),springbootspring security的自動(dòng)配置已經(jīng)足夠使用了。其中每一項(xiàng) Configurer(e.g.FormLoginConfigurer,CsrfConfigurer)都是 HttpConfigurer 的細(xì)化配置項(xiàng)。

WebSecurityBuilder

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    public void configure(WebSecurity web) throws Exception {
        web
            .ignoring()
            .antMatchers("/resources/**");
    }
}

以筆者的經(jīng)驗(yàn),這個(gè)配置中并不會(huì)出現(xiàn)太多的配置信息。

AuthenticationManagerBuilder

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
            .inMemoryAuthentication()
            .withUser("admin").password("admin").roles("USER");
    }
}

想要在 WebSecurityConfigurerAdapter 中進(jìn)行認(rèn)證相關(guān)的配置,可以使用 configure(AuthenticationManagerBuilder auth) 暴露一個(gè) AuthenticationManager 的建造器:AuthenticationManagerBuilder 。如上所示,我們便完成了內(nèi)存中用戶(hù)的配置。

細(xì)心的朋友會(huì)發(fā)現(xiàn),在前面的文章中我們配置內(nèi)存中的用戶(hù)時(shí),似乎不是這么配置的,而是:

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth
            .inMemoryAuthentication()
                .withUser("admin").password("admin").roles("USER");
    }
}

如果你的應(yīng)用只有唯一一個(gè) WebSecurityConfigurerAdapter ,那么他們之間的差距可以被忽略,從方法名可以看出兩者的區(qū)別:使用@Autowired注入的 AuthenticationManagerBuilder 是全局的身份認(rèn)證器,作用域可以跨越多個(gè)WebSecurityConfigurerAdapter,以及影響到基于Method的安全控制;而 protected configure()的方式則類(lèi)似于一個(gè)匿名內(nèi)部類(lèi),它的作用域局限于一個(gè)WebSecurityConfigurerAdapter內(nèi)部。關(guān)于這一點(diǎn)的區(qū)別,可以參考我曾經(jīng)提出的issuespring-security#issues4571。官方文檔中,也給出了配置多個(gè)WebSecurityConfigurerAdapter的場(chǎng)景以及demo,將在該系列的后續(xù)文章中解讀

原文鏈接:http://blog.didispace.com/xjf-spring-security-4/

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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