# 前言设计模式
最近在作 Spring OAuth2 登陆,并在登陆以后保存 Cookies。具体而言就是 Spring OAuth2 和 Spring Security 集成。Google一下居然没有发现一种能知足个人要求。最终只有研究源码了。session
有时间会画个 UML 图。ide
# 一些基础知识ui
# 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 中调用、须要作什么配置。