Spring Security

 對當前項目中使用到的Spring Security作一個簡單的理解總結,方便之後查閱。文章有疏漏之處,歡迎指正。web

Spring Security是一個可以爲基於Spring的企業應用系統提供聲明式的安全訪 問控制解決方案的安全框架。它提供了一組能夠在 Spring 應用上下文中配置的 Bean,充分利用了Spring IOC和AOP功能,爲應用系統提供聲明式的安全訪問控制功能,減小了爲企業系統安全控制編寫大量重複代碼的工做。應用程序層面的安全大概能夠歸爲兩類:身份認證和受權,Spring Security 在架構設計上就將二者分開了,在每一個架構上都留有擴展點。spring

1 身份認證架構

 

1.1 AuthenticationManager

身份認證的核心接口,只包含一個方法:
public interface AuthenticationManager {
    Authentication authenticate(Authentication authentication)
            throws AuthenticationException;
}

authenticate 方法可能產生三種結果:
a) 若是身份認證成功,返回完備的 Authentication 對象(通常來講, authenticated=true)
b) 若是身份認證失敗,拋出 AuthenticationException
c) 若是沒法認證,則返回 null 瀏覽器

1.2 ProviderManager

是最經常使用的 AuthenticationManager 接口的實現類,它將工做委託給 AuthenticationProvider 鏈。AuthenticationProvider 的接口定義相似於 AuthenticationManager,只不過多了個方法讓調用者判斷其是否支持傳入的 Authentication 類型。安全

ProviderManager 會遍歷 AuthenticationProvider 鏈,先判 斷其是否支持傳入的 Authentication 類型,若是支持,則調用 authenticate 方法, 如返回不爲 null 的 Authentication,則身份認證成功。服務器

for (AuthenticationProvider provider : getProviders()) {
            if (!provider.supports(toTest)) {
                continue;
            }

            if (debug) {
                logger.debug("Authentication attempt using "
                        + provider.getClass().getName());
            }

            try {
                result = provider.authenticate(authentication);

                if (result != null) {
                    copyDetails(authentication, result);
                    break;
                }
            }
            catch (AccountStatusException e) {
                prepareException(e, authentication);
                // SEC-546: Avoid polling additional providers if auth failure is due to
                // invalid account status
                throw e;
            }
            catch (InternalAuthenticationServiceException e) {
                prepareException(e, authentication);
                throw e;
            }
            catch (AuthenticationException e) {
                lastException = e;
            }
        }
View Code

2 受權架構

2.1 AccessDecisionManager

受權的核心接口,包含三個方法:
public interface AccessDecisionManager {

    void decide(Authentication authentication, Object object,
            Collection<ConfigAttribute> configAttributes) throws AccessDeniedException,
            InsufficientAuthenticationException;

    boolean supports(ConfigAttribute attribute);

    boolean supports(Class<?> clazz);
}

decide 方法決定是否容許訪問被保護的對象,傳入的參數中,authentication 是被身份認證經過後的完備對象,object是被保護的對象, configAttributes是被保護對象的屬性信息。cookie

2.2 AbstractAccessDecisionManager

實現了 AccessDecisionManager 接口的抽象類,它管理了一個 AccessDecisionVoter 列表,在執行受權操做時,調用每一個 AccessDecisionVoter 的 vote 方法獲得單獨投票結果,對每一個 voter 投票結果的仲裁則由其子類來完成。session

Spring Security 提供了 3 個子類,每一個子類的仲裁邏輯以下:架構

AffirmativeBased:只要有 voter投了同意票,則受權成功;在沒有同意票的狀況下,只要有反對票,則受權失敗;在所有棄權的狀況下,根據 isAllowIfAllAbstainDecisions 方法的返回值決定是否受權。併發

ConsensusBased: 根據少數服從多數的原則決定是否受權,若是同意票和反對票相等,根據 isAllowIfAllAbstainDecisions 方法的返回值決定是否受權。app

UnanimousBased:只有所有是同意票或者棄權才受權成功,只要有反對票則受權失敗。若是所有棄權,根據 isAllowIfAllAbstainDecisions 方法的返回值決定是否受權。

2.3 AccessDecisionVoter

對是否受權進行投票,核心方法是 vote, 該方法只能返回 3 種 int 值: ACCESS_GRANTED(1)ACCESS_ABSTAIN(0)ACCESS_DENIED(-1)

這兒就是一個擴展點,項目能夠實現本身的voter,例如: SimpleDecisionVoter implements AccessDecisionVoter<FilterInvocation>

 

3 Web 層安全

Spring Security 在 web 層的應用是基於 Servlet Filter 實現的,具體的實現類是 DelegatingFilterProxy,該代理類又會委託 Spring 容器管理的 FilterChainProxy 來處理,

FilterChainProxy 維護了一系列內部的 filter, 正是這些內部的 filter 實現 了所有的安全邏輯。關係圖以下:

在web.xml在配置便可:

 

<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>

 

3.1 核心過濾器

以我所在的項目組對Spring Security的使用爲例,一次web請求的filter chain 以下:

3.1.1 SecurityContextPersistenceFilter

此filter先嚐試從servlet session中獲取 SecurityContext ,若是沒有獲取到,則建立一個空的 SecurityContext 對象,SecurityContextHolder 是 SecurityContext 的存放容器,使用 ThreadLocal 存儲並將此對象跟當前線程關聯。

下圖顯示了整個交互過程:

3.1.2 LogoutFilter

先判斷請求的 url 是否匹配 logout-url,若是匹配則重定向到指定的 url,不然直接調用下一個 filter。下圖顯示了整個交互過程:

屬性名 做用
logout-url 表示此請求作爲退出登陸的默認地址
invalidate-session 表示是否要在退出登陸後讓當前session失效,默認爲true。
delete-cookies 指定退出登陸後須要刪除的cookie名稱,多個cookie之間以逗號分隔。
logout-success-url 指定成功退出登陸後要重定向的URL。須要注意的是對應的URL應當是不須要登陸就能夠訪問的。
success-handler-ref 指定用來處理成功退出登陸的LogoutSuccessHandler的引用。

 

 

 

 

 

 

 

 

 

簡單看一下LogoutFilter的源碼

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;

        if (requiresLogout(request, response)) {
            Authentication auth = SecurityContextHolder.getContext().getAuthentication();

            if (logger.isDebugEnabled()) {
                logger.debug("Logging out user '" + auth
                        + "' and transferring to logout destination");
            }

            this.handler.logout(request, response, auth);

            logoutSuccessHandler.onLogoutSuccess(request, response, auth);

            return;
        }

        chain.doFilter(request, response);
    }

若是 requiresLogout(request, response)爲true,則分別調用 CompositeLogoutHandler 的 logout(request, response, auth) 方法和 LogoutSuccessHandler 的 onLogoutSuccess(request, response, auth);

在這兩個方法中,logout 和 onLogoutSuccess 除了執行Spring Security本身的一些內部方法,好比 SecurityContextLogoutHandler 的 logout,咱們也能夠本身定義本身的方法,退出登陸須要刪除自定義的cookie等。

3.1.3 UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter

先看看 AbstractAuthenticationProcessingFilter 的 doFilter 方法

 1 public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
 2             throws IOException, ServletException {
 3 
 4         HttpServletRequest request = (HttpServletRequest) req;
 5         HttpServletResponse response = (HttpServletResponse) res;
 6 
 7         if (!requiresAuthentication(request, response)) {
 8             chain.doFilter(request, response);
 9 
10             return;
11         }
12 
13         if (logger.isDebugEnabled()) {
14             logger.debug("Request is to process authentication");
15         }
16 
17         Authentication authResult;
18 
19         try {
20             authResult = attemptAuthentication(request, response);
21             if (authResult == null) {
22                 // return immediately as subclass has indicated that it hasn't completed
23                 // authentication
24                 return;
25             }
26             sessionStrategy.onAuthentication(authResult, request, response);
27         }
28         catch (InternalAuthenticationServiceException failed) {
29             logger.error(
30                     "An internal error occurred while trying to authenticate the user.",
31                     failed);
32             unsuccessfulAuthentication(request, response, failed);
33 
34             return;
35         }
36         catch (AuthenticationException failed) {
37             // Authentication failed
38             unsuccessfulAuthentication(request, response, failed);
39 
40             return;
41         }
42 
43         // Authentication success
44         if (continueChainBeforeSuccessfulAuthentication) {
45             chain.doFilter(request, response);
46         }
47 
48         successfulAuthentication(request, response, chain, authResult);
49     }

20行的 attemptAuthentication(request, response) 就是 UsernamePasswordAuthenticationFilter 是嘗試認證過程。

--------------------------------------UsernamePasswordAuthenticationFilter 分析開始---------------------------------------------------------------

身份認證過程:根據 form 表單中的用戶名獲取用戶信息(包含密碼),將數 據庫中的密碼和 form 表單中的密碼作比對,若匹配則身份認證成功,並調用 AuthenticationSuccessHandler. onAuthenticationSuccess()。

下圖顯示的是認證成 功的交互過程: 

由於真實項目登陸除了校驗用戶名和密碼外,可能還有有些額外的校驗,好比驗證碼之類的,全部咱們會自定義一個類去繼承 UsernamePasswordAuthenticationFilter,而後重寫 attemptAuthentication() 方法。

類 UsernamePasswordAuthenticationFilter 的 attemptAuthentication(HttpServletRequest request, HttpServletResponse response)方法:

public Authentication attemptAuthentication(HttpServletRequest request,
            HttpServletResponse response) throws AuthenticationException {
        if (postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException(
                    "Authentication method not supported: " + request.getMethod());
        }

        String username = obtainUsername(request);
        String password = obtainPassword(request);

        if (username == null) {
            username = "";
        }

        if (password == null) {
            password = "";
        }

        username = username.trim();

        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
                username, password);

        // Allow subclasses to set the "details" property
        setDetails(request, authRequest);

        return this.getAuthenticationManager().authenticate(authRequest);
    }
View Code

而後執行類 ProviderManager 的 authenticate(Authentication authentication) 方法,已經到了身份認證架構部分,

繼續調用抽象類 AbstractUserDetailsAuthenticationProvider 的 authenticate(Authentication authentication) 方法:

 1 public Authentication authenticate(Authentication authentication)
 2             throws AuthenticationException {
 3         Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
 4                 messages.getMessage(
 5                         "AbstractUserDetailsAuthenticationProvider.onlySupports",
 6                         "Only UsernamePasswordAuthenticationToken is supported"));
 7 
 8         // Determine username
 9         String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
10                 : authentication.getName();
11 
12         boolean cacheWasUsed = true;
13         UserDetails user = this.userCache.getUserFromCache(username);
14 
15         if (user == null) {
16             cacheWasUsed = false;
17 
18             try {
19                 user = retrieveUser(username,
20                         (UsernamePasswordAuthenticationToken) authentication);
21             }
22             catch (UsernameNotFoundException notFound) {
23                 logger.debug("User '" + username + "' not found");
24 
25                 if (hideUserNotFoundExceptions) {
26                     throw new BadCredentialsException(messages.getMessage(
27                             "AbstractUserDetailsAuthenticationProvider.badCredentials",
28                             "Bad credentials"));
29                 }
30                 else {
31                     throw notFound;
32                 }
33             }
34 
35             Assert.notNull(user,
36                     "retrieveUser returned null - a violation of the interface contract");
37         }
38 
39         try {
40             preAuthenticationChecks.check(user);
41             additionalAuthenticationChecks(user,
42                     (UsernamePasswordAuthenticationToken) authentication);
43         }
44         catch (AuthenticationException exception) {
45             if (cacheWasUsed) {
46                 // There was a problem, so try again after checking
47                 // we're using latest data (i.e. not from the cache)
48                 cacheWasUsed = false;
49                 user = retrieveUser(username,
50                         (UsernamePasswordAuthenticationToken) authentication);
51                 preAuthenticationChecks.check(user);
52                 additionalAuthenticationChecks(user,
53                         (UsernamePasswordAuthenticationToken) authentication);
54             }
55             else {
56                 throw exception;
57             }
58         }
59 
60         postAuthenticationChecks.check(user);
61 
62         if (!cacheWasUsed) {
63             this.userCache.putUserInCache(user);
64         }
65 
66         Object principalToReturn = user;
67 
68         if (forcePrincipalAsString) {
69             principalToReturn = user.getUsername();
70         }
71 
72         return createSuccessAuthentication(principalToReturn, authentication, user);
73     }
View Code

19行 retrieveUser 方法的具體實如今類 DaoAuthenticationProvider 中,這兒就是從咱們系統中根據 username,查詢到一個 UserDetails 數據。

40行 preAuthenticationChecks.check(user); 則是爲已經查詢到的用戶,附上在當前系統中的權限數據。

41行 additionalAuthenticationChecks 裏面就是密碼的校驗,具體實現類仍是 DaoAuthenticationProvider,自定義的 passwordEncoderHandler 實現了 PasswordEncoder 接口 isPasswordValid 方法,去比較密碼是否匹配。

--------------------------------------UsernamePasswordAuthenticationFilter 分析結束---------------------------------------------------------------

繼續 AbstractAuthenticationProcessingFilter 的 doFilter 方法分析,

32和38行 unsuccessfulAuthentication(request, response, failed) 是認證失敗的處理邏輯,

  認證失敗時,首先 SecurityContextHolder.clearContext(),清除認證信息,而後在 SimpleUrlAuthenticationFailureHandler 能夠處理自定義的認證失敗邏輯

48行的 successfulAuthentication(request, response, chain, authResult) 則是認證成功的邏輯。

  認證成功時,首先 SecurityContextHolder.getContext().setAuthentication(authentication),其次執行 AbstractRememberMeServices 的 loginSuccess 方法,而後執行 SavedRequestAwareAuthenticationSuccessHandler的 onAuthenticationSuccess方法。

RememberMeAuthenticationFilter

這個filter貌似是查詢 當前SecurityContextHolder的Authentication是否爲null,若是爲空則繼續走相似上一個filter的身份認證過程,若是不爲空,繼續下一個filter。

3.1.4 RequestCacheAwareFilter

此 filter 一般用做臨時重定向,場景是這樣的:在未登錄的狀況下訪問某個安全 url,會重定向到登錄頁,在重定向以前先在 session 中保存訪問此安全 url 的請求,在登錄成功後再繼續處理它。下圖顯示的是交互過程:

圖中 SAVED_REQUEST = "SPRING_SECURITY_SAVED_REQUEST"

3.1.5 SecurityContextHolderAwareRequestFilter

將 servlet request 封裝到 SecurityContextHolderAwareRequestWrapper 類中,此 包裝類繼承自 HttpServletRequestWrapper,並提供了額外的跟安全相關的方法。

3.1.6 AnonymousAuthenticationFilter

若是到了這步 SecurityContext 中的 Authentication 對象仍然爲null, 則建立一個 AnonymousAuthenticationToken,這個對象能夠類比網站的匿名用戶。下圖展現 的是交互過程:

3.1.7 SessionManagementFilter

這個過濾器看名字就知道是管理 session 的了,提供兩大類功能: session 固化 保護(經過 session-fixation-protection 配置),session 併發控制(經過 concurrency-control 配置)。

3.1.8 ExceptionTranslationFilter

當請求到達這裏時,會將對後續過濾器的調用封裝在 try..catch 塊中,若是捕獲 到 AuthenticationException 異常,則調用 authenticationEntryPoint.commence 方法開啓新的身份認證流程(通常來講是跳轉到登錄頁),

若是捕獲到 AccessDeniedException 異常,調用 accessDeniedHandler.handle 方法處理(好比: 展現未受權的錯誤頁面)。下圖顯示的是交互過程:

3.1.9 FilterSecurityInterceptor

此過濾器是整個過濾器鏈的最後一環,用於保護 Http 資源的,它須要一個 AccessDecisionManager 和一個 AuthenticationManager 的引用。它會從 SecurityContextHolder 獲取 Authentication,

而後經過 SecurityMetadataSource 能夠得知當前請求是否在請求受保護的資源。對於請求那些受保護的資源,若是 Authentication.isAuthenticated()返回 false 或者 FilterSecurityInterceptor 的 alwaysReauthenticate 屬性爲 true ,

那麼將會使用其引用的 AuthenticationManager 再認證一次,認證以後再使用認證後的 Authentication替換 SecurityContextHolder 中擁有的那個。而後就是利用 AccessDecisionManager 進行權限的檢查,也就進行到了受權架構部分。

下圖顯示的是交互過程:

3.2其餘過濾器 

3.2.1 CsrfFilter 

該過濾器經過 token 防範 CSRF 攻擊,將從 tokenRepository 獲取的 token 與從 請求參數或者 header 參數中獲取的 token 作比較,若是不匹配,調用 accessDeniedHandler.handle 方法來處理。下圖展現的是交互過程: 

3.2.2 HeaderWriterFilter 

該過濾器經過寫響應安全頭的方式來保護瀏覽器端的安全,下面簡單介紹下各類 http 安全頭。

content-security-policy:經過定義內容來源來預防 XSS 攻擊或者代碼植入攻擊,下面 的例子只容許當前域或者 google-analytics.com 的腳本執行。

content-security-policy: script-src 'self' https://www.google-analytics.com 
 

X-XSS-Protection: 又稱 XSS 過濾器,經過指示瀏覽器阻止含有惡意腳本的響應來預防 XSS 攻擊。 

x-xss-protection: 1; mode=block 
 

strict-transport-security(HSTS):該頭指示瀏覽器只能使用 https 訪問 web server。 

strict-transport-security: max-age=31536000; includeSubDomains; preload 

 

X-Frame-Options:來確保本身網站的內容沒有被嵌到別人的網站中去,也從而避免了點擊 劫持 (clickjacking) 的攻擊。X-Frame-Options 有三個值: DENY(表示該頁面不容許 在 frame 中展現,即使是在相同域名的頁面中嵌套也不容許),

SAMEORIGIN(表示該頁面可 以在相同域名頁面的 frame 中展現),ALLOW-FROM uri(表示該頁面能夠在指定來源的 frame 中展現)。 

X-Content-Type-Options:若是服務器發送響應頭 "X-Content-Type-Options: nosniff",則 script 和 styleSheet 元素會拒絕包含錯誤的 MIME 類型的響應。這是一 種安全功能,有助於防止基於 MIME 類型混淆的攻擊。 

 

4.參考文獻

相關文章
相關標籤/搜索