Spring Security 源碼解析(一)AbstractAuthenticationProcessingFilter

# 前言設計模式

 

最近在作 Spring OAuth2 登陸,並在登陸以後保存 Cookies。具體而言就是 Spring OAuth2 和 Spring Security 集成。Google一下居然沒有發現一種能知足個人要求。最終只有研究源碼了。session

有時間會畫個 UML 圖。ide

 

# 一些基礎知識ui

 

  • Spring Security 驗證身份的方式是利用 Filter,再加上 HttpServletRequest 的一些信息進行過濾。
  • 類 Authentication 保存的是身份認證信息。
  • 類 AuthenticationProvider 提供身份認證途徑。
  • 類 AuthenticationManager 保存的 AuthenticationProvider 集合,並調用 AuthenticationProvider 進行身份認證。

 

# AbstractAuthenticationProcessingFilter this

 

## 設計模式spa

 

### 抽象工廠模式debug

 

AbstractAuthenticationProcessingFilter 是一個抽象類,主要的功能是身份認證。OAuth2ClientAuthenticationProcessingFilter(Spriing OAuth2)、RememberMeAuthenticationFilter(RememberMe)都繼承了 AbstractAuthenticationProcessingFilter ,並重寫了方法 attemptAuthentication 進行身份認證。設計

    /**
     * Performs actual authentication. 進行真正的認證。
     * <p>
     * The implementation should do one of the following: 具體實現須要作以下事情:
     * <ol>
     * <li>Return a populated authentication token for the authenticated user, indicating
     * successful authentication</li> 返回一個具體的 Authentication認證對象。
     * <li>Return null, indicating that the authentication process is still in progress.
     * Before returning, the implementation should perform any additional work required to
     * complete the process.</li> 返回 null,表示實現的子類不能處理該身份認證,還須要別的類進行身份認證(往 FilterChain 傳遞)。
     * <li>Throw an <tt>AuthenticationException</tt> if the authentication process fails</li> 拋出異常 AuthenticationException 表示認證失敗。
     * </ol>
     *
     * @param request from which to extract parameters and perform the authentication
     * @param response the response, which may be needed if the implementation has to do a
     * redirect as part of a multi-stage authentication process (such as OpenID).
     *
     * @return the authenticated user token, or null if authentication is incomplete.
     *
     * @throws AuthenticationException if authentication fails.
     */
    public abstract Authentication attemptAuthentication(HttpServletRequest request,
            HttpServletResponse response) throws AuthenticationException, IOException,
            ServletException;

這個方法的目的很明確,就是須要子類提供身份認證的具體實現。子類根據 HttpServletRequest 等信息進行身份認證,並返回 Authentication 對象、 null、異常,分別表示認證成功返回的身份認證信息、須要其餘 Filter 繼續進行身份認證、認證失敗。下面是一個 OAuth2ClientAuthenticationProcessingFilter 對於方法 attemptAuthentication 的實現,具體代碼的行爲就不解釋了。rest

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException, IOException, ServletException {

        OAuth2AccessToken accessToken;
        try {
            accessToken = restTemplate.getAccessToken();
        } catch (OAuth2Exception e) {
            BadCredentialsException bad = new BadCredentialsException("Could not obtain access token", e);
            publish(new OAuth2AuthenticationFailureEvent(bad));
            throw bad;            
        }
        try {
            OAuth2Authentication result = tokenServices.loadAuthentication(accessToken.getValue());
            if (authenticationDetailsSource!=null) {
                request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, accessToken.getValue());
                request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE, accessToken.getTokenType());
                result.setDetails(authenticationDetailsSource.buildDetails(request));
            }
            publish(new AuthenticationSuccessEvent(result));
            return result;
        }
        catch (InvalidTokenException e) {
            BadCredentialsException bad = new BadCredentialsException("Could not obtain user details from token", e);
            publish(new OAuth2AuthenticationFailureEvent(bad));
            throw bad;            
        }

    }

 

至於方法 attemptAuthentication 是怎麼被調用的?身份認證流程很簡單,可是身份認證完成以前、完成以後,也須要作不少的操做。大部分操做都是一塵不變的,身份認證以前確認是否要進行身份驗證、保存身份認證信息、成功處理、失敗處理等。具體流程,在下面的方法中體現。能夠看出這就是個工廠,已經肯定好身份認證的流程,因此咱們須要作的事情就是重寫身份認證機制(方法 attemptAuthentication)就能夠了。code

    /**
     * Invokes the
     * {@link #requiresAuthentication(HttpServletRequest, HttpServletResponse)
     * requiresAuthentication} method to determine whether the request is for
     * authentication and should be handled by this filter. If it is an authentication
     * request, the
     * {@link #attemptAuthentication(HttpServletRequest, HttpServletResponse)
     * attemptAuthentication} will be invoked to perform the authentication. There are
     * then three possible outcomes:
     * <ol>
     * <li>An <tt>Authentication</tt> object is returned. The configured
     * {@link SessionAuthenticationStrategy} will be invoked (to handle any
     * session-related behaviour such as creating a new session to protect against
     * session-fixation attacks) followed by the invocation of
     * {@link #successfulAuthentication(HttpServletRequest, HttpServletResponse, FilterChain, Authentication)}
     * method</li>
     * <li>An <tt>AuthenticationException</tt> occurs during authentication. The
     * {@link #unsuccessfulAuthentication(HttpServletRequest, HttpServletResponse, AuthenticationException)
     * unsuccessfulAuthentication} method will be invoked</li>
     * <li>Null is returned, indicating that the authentication process is incomplete. The
     * method will then return immediately, assuming that the subclass has done any
     * necessary work (such as redirects) to continue the authentication process. The
     * assumption is that a later request will be received by this method where the
     * returned <tt>Authentication</tt> object is not null.
     * </ol>
     */
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {

        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;

        // 是否須要身份認證
        if (!requiresAuthentication(request, response)) {
            // 不須要身份認證,傳遞到 FilterChain 繼續過濾
            chain.doFilter(request, response);

            return;
        }

        if (logger.isDebugEnabled()) {
            logger.debug("Request is to process authentication");
        }

        Authentication authResult;

        try {
            // 進行身份認證,該方法須要子類重寫
            authResult = attemptAuthentication(request, response);
            if (authResult == null) {
                // return immediately as subclass has indicated that it hasn't completed
                // authentication
                return;
            }
            // 身份認證成功,保存 session
            sessionStrategy.onAuthentication(authResult, request, response);
        }
        // 身份認證代碼出錯
        catch (InternalAuthenticationServiceException failed) {
            logger.error(
                    "An internal error occurred while trying to authenticate the user.",
                    failed);
            // 身份認證失敗一系列事物處理,包括調用 RememberMeServices 等
            unsuccessfulAuthentication(request, response, failed);

            return;
        }
        // 身份認證失敗異常
        catch (AuthenticationException failed) {
            // Authentication failed
            // 身份認證失敗一系列事物處理,包括調用 RememberMeServices 等
            unsuccessfulAuthentication(request, response, failed);

            return;
        }

        // Authentication success
        // 身份認證成功以後是否須要傳遞到 FilterChain
        if (continueChainBeforeSuccessfulAuthentication) {
            chain.doFilter(request, response);
        }

        // 身份認證成功一系列事物處理,包括調用 RememberMeServices 等
        successfulAuthentication(request, response, chain, authResult);
    }
}

 

### 策略模式

 

這裏還能夠看一下方法 doFilter 的內部調用,好比下面這個方法。

    /**
     * Default behaviour for successful authentication.
     * <ol>
     * <li>Sets the successful <tt>Authentication</tt> object on the
     * {@link SecurityContextHolder}</li>
     * <li>Informs the configured <tt>RememberMeServices</tt> of the successful login</li>
     * <li>Fires an {@link InteractiveAuthenticationSuccessEvent} via the configured
     * <tt>ApplicationEventPublisher</tt></li>
     * <li>Delegates additional behaviour to the {@link AuthenticationSuccessHandler}.</li>
     * </ol>
     *
     * Subclasses can override this method to continue the {@link FilterChain} after
     * successful authentication.
     * @param request
     * @param response
     * @param chain
     * @param authResult the object returned from the <tt>attemptAuthentication</tt>
     * method.
     * @throws IOException
     * @throws ServletException
     */
    protected void successfulAuthentication(HttpServletRequest request,
                                            HttpServletResponse response, FilterChain chain, Authentication authResult)
            throws IOException, ServletException {

        if (logger.isDebugEnabled()) {
            logger.debug("Authentication success. Updating SecurityContextHolder to contain: "
                    + authResult);
        }

        // 認證成功設置身份認證信息
        SecurityContextHolder.getContext().setAuthentication(authResult);

        // RememberMeServices 設置成功登陸信息,如 Cookie 等
        rememberMeServices.loginSuccess(request, response, authResult);

        // 認證成功發送事件
        // Fire event
        if (this.eventPublisher != null) {
            eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
                    authResult, this.getClass()));
        }

        // 認證成功處理器
        successHandler.onAuthenticationSuccess(request, response, authResult);
    }

Spring Security 仍是很貼心的把這個方法的修飾符設定成了 protected,以知足咱們重寫身份認證成功以後的機制,雖然大多數狀況下並不須要。不須要的緣由是認證成功以後的流程基本最多也就是這樣,若是想改變一些行爲,能夠直接傳遞給 AbstractAuthenticationProcessingFilter 一些具體實現便可,如 AuthenticationSuccessHandler(認證成功處理器)。根據在這個處理器內能夠進行身份修改、返回結果修改等行爲。下面是該對象的定義。

    private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();

各類各樣的 AuthenticationSuccessHandler 能夠提供多種多樣的認證成功行爲,這是一種策略模式。

 

# 後記

 

Spring Security 採起了多種設計模式,這是 Spring 家族代碼的一向特性。讓人比較着急的是,Spring Security 雖然能夠作到開箱即用,可是想要自定義代碼的話,必需要熟悉 Spring Security 代碼。好比如何使用 RememberMeServices。RememberMeService 有三個方法,登陸成功操做、登陸失敗操做、自動登陸操做。你能夠重寫這些方法,但你若是不看源碼,你沒法得知這些方法會在何時調用、在哪一個 Filter 中調用、須要作什麼配置。

相關文章
相關標籤/搜索