• Spring Security 源码解析(一)AbstractAuthenticationProcessingFilter


    # 前言

    最近在做 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 中调用、需要做什么配置。

  • 相关阅读:
    从今天开始,记录学习的点滴。
    git命令整理
    vue ie报错:[Vue warn]: Error in v-on handler: "ReferenceError: “Promise”未定义"
    HTML5知识整理
    解决You are using the runtime-only build of Vue where the template compiler is not available. Either pre
    HTML5本地存储
    网站建设流程图说明
    vue支持的修饰符(常用整理)
    vue绑定内联样式
    vue绑定class的几种方式
  • 原文地址:https://www.cnblogs.com/Piers/p/8620523.html
Copyright © 2020-2023  润新知