2019-07-24 Spring Security核心流程

Spring Security

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

核心組件

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

SecurityContextHolder

SecurityContextHolder 用于存儲(chǔ)安全上下文(security context)的信息挑格。當(dāng)前操作的用戶是誰漂彤,該用戶是否已經(jīng)被認(rèn)證,他擁有哪些角色權(quán)限…這些都被保存在 SecurityContextHolder中媳板。SecurityContextHolder 默認(rèn)使用 ThreadLocal 策略來存儲(chǔ)認(rèn)證信息蛉幸【拊担看到 ThreadLocal 也就意味著,這是一種與線程綁定的策略又固。Spring Security 在用戶登錄時(shí)自動(dòng)綁定認(rèn)證信息到當(dāng)前線程仰冠,在用戶退出時(shí),自動(dòng)清除當(dāng)前線程的認(rèn)證信息昼捍。但這一切的前提担锤,是你在 web 場景下使用 Spring Security肛循。

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

因?yàn)樯矸菪畔⑹桥c線程綁定的累舷,所以可以在程序的任何地方使用靜態(tài)方法獲取用戶信息笋粟。一個(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 類,而 Principal 是位于 java.security 包中的问顷《耪可以見得羞芍,Authentication 在 spring security 中是最高級(jí)別的身份/認(rèn)證的抽象唯咬。

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

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

  • getAuthorities()剂公,權(quán)限信息列表纲辽,默認(rèn)是 GrantedAuthority 接口的一些實(shí)現(xiàn)類鳞上,通常是代表權(quán)限信息的一系列字符串因块。
  • getCredentials(),密碼信息拒名,用戶輸入的密碼字符串,在認(rèn)證過后通常會(huì)被移除同云,用于保障安全炸站。
  • getDetails()旱易,細(xì)節(jié)信息,web 應(yīng)用中的實(shí)現(xiàn)接口通常為 WebAuthenticationDetails忌堂,它記錄了訪問者的 ip 地址和 sessionId 的值士修。
  • getPrincipal()登淘,敲黑板G荨A髌蕖!最重要的身份信息在辆,大部分情況下返回的是 UserDetails 接口的實(shí)現(xiàn)類浑度,也是框架中的常用接口之一箩张。UserDetails 接口將會(huì)在下面的小節(jié)重點(diǎn)介紹。

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

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

這是一個(gè)抽象的認(rèn)證流程冕广,而整個(gè)過程中撒汉,如果不糾結(jié)于細(xì)節(jié)挠阁,其實(shí)只剩下一個(gè) AuthenticationManager 是我們沒有接觸過的了,這個(gè)身份管理器我們在后面的小節(jié)介紹丰刊。

AuthenticationManager

初次接觸 Spring Security 的朋友相信會(huì)被 AuthenticationManager寻歧,ProviderManager 猾封,AuthenticationProvider …這么多相似的 Spring 認(rèn)證類搞得暈頭轉(zhuǎn)向,但只要稍微梳理一下就可以理解清楚它們的聯(lián)系和設(shè)計(jì)者的用意枚钓。AuthenticationManager(接口)是認(rèn)證相關(guān)的核心接口搀捷,也是發(fā)起認(rèn)證的出發(fā)點(diǎn),因?yàn)樵趯?shí)際需求中家厌,我們可能會(huì)允許用戶使用用戶名+密碼登錄,同時(shí)允許用戶使用郵箱+密碼掰吕,手機(jī)號(hào)碼+密碼登錄殖熟,甚至,可能允許用戶使用指紋登錄(還有這樣的操作斑响?沒想到吧)菱属,所以說 AuthenticationManager 一般不直接認(rèn)證,AuthenticationManager 接口的常用實(shí)現(xiàn)類 ProviderManager 內(nèi)部會(huì)維護(hù)一個(gè) List<AuthenticationProvider> 列表舰罚,存放多種認(rèn)證方式纽门,實(shí)際上這是委托者模式的應(yīng)用(Delegate)。也就是說沸停,核心的認(rèn)證入口始終只有一個(gè):AuthenticationManager,不同的認(rèn)證方式:用戶名+密碼(UsernamePasswordAuthenticationToken)败玉,郵箱+密碼血淌,手機(jī)號(hào)碼+密碼登錄則對(duì)應(yīng)了三個(gè) AuthenticationProvider乳蓄。這樣一來四不四就好理解多了?熟悉 shiro 的朋友可以把AuthenticationProvider 理解成 Realm赠叼。在默認(rèn)策略下贯被,只需要通過一個(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í)行到此叉袍,說明沒有認(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)證器都無法認(rèn)證成功挤茄,則 ProviderManager 會(huì)拋出一個(gè)ProviderNotFoundException 異常。

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

DaoAuthenticationProvider

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

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

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

UserDetails 與 UserDetailsService

上面不斷提到了 UserDetails 這個(gè)接口巧婶,它代表了最詳細(xì)的用戶信息,這個(gè)接口涵蓋了一些必要的用戶信息字段涂乌,具體的實(shí)現(xiàn)類對(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 接口很類似,比如它們都擁有 username 湾盒,authorities 湿右,區(qū)分他們也是本文的重點(diǎn)內(nèi)容之一。AuthenticationgetCredentials()UserDetails 中的 getPassword() 需要被區(qū)分對(duì)待罚勾,前者是用戶提交的密碼憑證毅人,后者是用戶正確的密碼,認(rèn)證器其實(shí)就是對(duì)這兩者的比對(duì)尖殃。Authentication 中的 getAuthorities() 實(shí)際是由 UserDetailsgetAuthorities() 傳遞而形成的丈莺。還記得 Authentication 接口中的 getUserDetails() 方法嗎?其中的 UserDetails 用戶詳細(xì)信息便是經(jīng)過了 AuthenticationProvider 之后被填充的分衫。

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

UserDetailsServiceAuthenticationProvider 兩者的職責(zé)常常被人們搞混场刑,關(guān)于他們的問題在文檔的 FAQissues 中屢見不鮮。記住一點(diǎn)即可,敲黑板GO帧n戆谩!UserDetailsService 只負(fù)責(zé)從特定的地方(通常是數(shù)據(jù)庫)加載用戶信息瞎疼,僅此而已科乎,記住這一點(diǎn),可以避免走很多彎路贼急。UserDetailsService 常見的實(shí)現(xiàn)類有 JdbcDaoImpl茅茂,InMemoryUserDetailsManager,前者從數(shù)據(jù)庫加載用戶太抓,后者從內(nèi)存中加載用戶空闲,也可以自己實(shí)現(xiàn) UserDetailsService,通常這更加靈活走敌。

架構(gòu)概覽圖

為了更加形象的理解上述我介紹的這些核心類碴倾,附上一張按照我的理解,所畫出 Spring Security 的一張非典型的 UML

整個(gè)Spring Security 都是圍繞以上這張架構(gòu)圖展開的掉丽,最頂層為最核心最抽象的 AuthenticationManager 接口跌榔, ProviderManagerAuthenticationManager 的一個(gè)具體實(shí)現(xiàn),功能如其名字他的作用是管理 Provider 的捶障, AuthenticationProvider 才是真正認(rèn)證的接口僧须,因此我們在實(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 來做驗(yàn)證,可以使用 Filter 返回的 Authentication 實(shí)現(xiàn)類來限定空免,部分代碼如下:

//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é)果蹋砚,先過個(gè)眼熟扼菠。后續(xù)的文章會(huì)秉持 Code First 的理念,陸續(xù)詳細(xì)地講解這些實(shí)現(xiàn)類的使用場景坝咐,源碼分析循榆,以及最基本的:如何配置 Spring Security ,在后面可以時(shí)不時(shí)往回看一看墨坚,找到具體的類在整個(gè)架構(gòu)中所處的位置秧饮。另外,一些 Spring Security的過濾器還未囊括在架構(gòu)概覽中,如將表單信息包裝成 UsernamePasswordAuthenticationToken 的過濾器盗尸,考慮到這些雖然也是架構(gòu)的一部分柑船,但是真正重寫他們的可能性較小,所以打算放到后面講解泼各。

配置介紹

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”(首頁),”/login”(登錄),”/logout”(注銷),之外,其他路徑都需要認(rèn)證扣蜻。
  • 指定“/login”該路徑為登錄頁面逆巍,當(dāng)未認(rèn)證的用戶嘗試訪問任何受保護(hù)的資源時(shí),都會(huì)跳轉(zhuǎn)到“/login”莽使。
  • 默認(rèn)指定“/logout”為注銷頁面
  • 配置一個(gè)內(nèi)存中的用戶認(rèn)證器锐极,使用admin/admin作為用戶名和密碼,具有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ù)庫加載權(quán)限芳肌,這邊因?yàn)?Spring Security 的機(jī)制是在模塊啟動(dòng)的時(shí)候進(jìn)行加載的溪烤,如果想要?jiǎng)討B(tài) reload 權(quán)限,調(diào)查來看Spring 并沒有提供相關(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和方法名組合來區(qū)分,注意一種特殊情況掂铐,
                //因?yàn)槭鞘褂肁nt語法來匹配URL罕拂,如果出現(xiàn)請求方法相同且URL是匹配子集關(guān)系時(shí),要把最具體的URL放在
                //前面全陨,比如同是 GET 方法爆班,Api1:/api/user/paged 和 Api2:/api/user/{id},在Ant語法里面
                //Api2是能匹配Api1的辱姨,如果用戶有Api2的權(quán)限而沒有Api1的權(quán)限柿菩,在如下初始化權(quán)限時(shí)Api2初始化的
                //順序在Api1的前面,就會(huì)導(dǎo)致用戶即使沒有Api1的權(quán)限也能訪問雨涛,因此要確保數(shù)據(jù)庫加載的時(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)
                //自定義沒有權(quán)限時(shí)的處理枢舶,一般根據(jù)業(yè)務(wù)封裝自定義的返回結(jié)果
                .accessDeniedHandler(ajaxAccessDeniedHandler)

            .and()
                //Restful API 完全無狀態(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()
                //處理跨域過濾器
                .addFilterBefore(new CustomCorsFilter(), UsernamePasswordAuthenticationFilter.class)
                //處理登錄獲取token的過濾器
                .addFilterBefore(buildAjaxLoginProcessingFilter(AUTHENTICATION_URL), UsernamePasswordAuthenticationFilter.class)
                //驗(yàn)證用戶token凉泄,以及用戶是否有接口訪問權(quán)限的過濾器
                .addFilterBefore(buildJwtTokenAuthenticationProcessingFilter(permitAllEndpointList, Arrays.asList(API_ROOT_URL,API_ATTACHMENT_URL)), UsernamePasswordAuthenticationFilter.class);
    }

具體解釋見代碼上的注釋

@EnableWebSecurity

我們自己定義的配置類 WebSecurityConfig 加上了 @EnableWebSecurity 注解,同時(shí)繼承了 WebSecurityConfigurerAdapter蚯根。你可能會(huì)在想誰的作用大一點(diǎ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 注解中包含的配置類教藻。
  • <2> WebSecurityConfiguration 顧名思義,是用來配置 web 安全的拗盒,下面的小節(jié)會(huì)詳細(xì)介紹怖竭。
  • <3> @EnableGlobalAuthentication 注解的源碼如下:
@Import(AuthenticationConfiguration.class)
@Configuration
public @interface EnableGlobalAuthentication {
}

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

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

WebSecurityConfiguration

在這個(gè)配置類中恼策,有一個(gè)非常重要的Bean被注冊了鸦致。

@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的核心過濾器分唾,是整個(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è)代理類,負(fù)責(zé)攔截請求(注意DelegatingFilterProxy這個(gè)類不是spring security包中的沙峻,而是存在于web包中睦授,spring使用了代理模式來實(shí)現(xiàn)安全過濾的解耦)。

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)容蒋纬,來進(jìn)行重寫猎荠,而其提供了三個(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 作為根開始配置些楣,每一個(gè) and() 對(duì)應(yīng)了一個(gè)模塊的配置(等同于xml配置中的結(jié)束標(biāo)簽),并且 and() 返回了 HttpSecurity 本身宪睹,于是可以連續(xù)進(jìn)行配置愁茁。他們配置的含義也非常容易通過變量本身來推測

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

他們分別代表了 http 請求相關(guān)的安全配置罪帖,這些配置項(xiàng)無一例外的返回了 Configurer 類促煮,而所有的 http 相關(guān)配置可以通過查看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é)議有一定的了解才能完全掌握所有的配置,不過整袁,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)存中用戶的配置盗飒。

細(xì)心的朋友會(huì)發(fā)現(xiàn)嚷量,在前面的文章中我們配置內(nèi)存中的用戶時(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()的方式則類似于一個(gè)匿名內(nèi)部類痕囱,它的作用域局限于一個(gè)WebSecurityConfigurerAdapter內(nèi)部田轧。關(guān)于這一點(diǎn)的區(qū)別,可以參考我曾經(jīng)提出的issuespring-security#issues4571鞍恢。官方文檔中傻粘,也給出了配置多個(gè)WebSecurityConfigurerAdapter的場景以及demo,將在該系列的后續(xù)文章中解讀

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市向胡,隨后出現(xiàn)的幾起案子湿故,更是在濱河造成了極大的恐慌,老刑警劉巖稽莉,帶你破解...
    沈念sama閱讀 211,817評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件瀑志,死亡現(xiàn)場離奇詭異,居然都是意外死亡污秆,警方通過查閱死者的電腦和手機(jī)劈猪,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,329評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來混狠,“玉大人岸霹,你說我怎么就攤上這事〗龋” “怎么了贡避?”我有些...
    開封第一講書人閱讀 157,354評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長予弧。 經(jīng)常有香客問我刮吧,道長,這世上最難降的妖魔是什么掖蛤? 我笑而不...
    開封第一講書人閱讀 56,498評(píng)論 1 284
  • 正文 為了忘掉前任杀捻,我火速辦了婚禮,結(jié)果婚禮上蚓庭,老公的妹妹穿的比我還像新娘致讥。我一直安慰自己,他們只是感情好器赞,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,600評(píng)論 6 386
  • 文/花漫 我一把揭開白布垢袱。 她就那樣靜靜地躺著,像睡著了一般港柜。 火紅的嫁衣襯著肌膚如雪请契。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,829評(píng)論 1 290
  • 那天夏醉,我揣著相機(jī)與錄音爽锥,去河邊找鬼。 笑死畔柔,一個(gè)胖子當(dāng)著我的面吹牛氯夷,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播靶擦,決...
    沈念sama閱讀 38,979評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼肠槽,長吁一口氣:“原來是場噩夢啊……” “哼擎淤!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起秸仙,我...
    開封第一講書人閱讀 37,722評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤嘴拢,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后寂纪,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體席吴,經(jīng)...
    沈念sama閱讀 44,189評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,519評(píng)論 2 327
  • 正文 我和宋清朗相戀三年捞蛋,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了孝冒。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,654評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡拟杉,死狀恐怖庄涡,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情搬设,我是刑警寧澤穴店,帶...
    沈念sama閱讀 34,329評(píng)論 4 330
  • 正文 年R本政府宣布,位于F島的核電站拿穴,受9級(jí)特大地震影響泣洞,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜默色,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,940評(píng)論 3 313
  • 文/蒙蒙 一球凰、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧腿宰,春花似錦呕诉、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,762評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至规肴,卻和暖如春捶闸,著一層夾襖步出監(jiān)牢的瞬間夜畴,已是汗流浹背拖刃。 一陣腳步聲響...
    開封第一講書人閱讀 31,993評(píng)論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留贪绘,地道東北人兑牡。 一個(gè)月前我還...
    沈念sama閱讀 46,382評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像税灌,于是被迫代替她去往敵國和親均函。 傳聞我的和親對(duì)象是個(gè)殘疾皇子亿虽,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,543評(píng)論 2 349

推薦閱讀更多精彩內(nèi)容