# 前言
最近在做 Spring OAuth2 登錄,並在登錄之后保存 Cookies。具體而言就是 Spring OAuth2 和 Spring Security 集成。Google一下竟然沒有發現一種能滿足我的要求。最終只有研究源碼了。
有時間會畫個 UML 圖。
# 一些基礎知識
- Spring Security 驗證身份的方式是利用 Filter,再加上 HttpServletRequest 的一些信息進行過濾。
- 類 Authentication 保存的是身份認證信息。
- 類 AuthenticationProvider 提供身份認證途徑。
- 類 AuthenticationManager 保存的 AuthenticationProvider 集合,並調用 AuthenticationProvider 進行身份認證。
# AbstractAuthenticationProcessingFilter
## 設計模式
### 抽象工廠模式
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 的實現,具體代碼的行為就不解釋了。
@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)就可以了。
/** * 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 中調用、需要做什么配置。