Spring 源码分析(五)Sercurity

Spring 源码分析(五)Sercurity

sschrodienger

2019/03/04


Filter 代理组件分析


DelegatingFilterProxy

DelegatingFilterProxy 是标准 servlet 过滤器的一个代理类,它可以代理 Spring 容器中实现了 Filter 接口的 Bean,以方便该过滤器获得 Spring 依赖注入以及生命周期的支持。

DelegatingFilterProxy 维护了一个代理类 private volatile Filter delegatedelegate是最重要的执行过滤器。

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
        throws ServletException, IOException {

    // Lazily initialize the delegate if necessary.
    Filter delegateToUse = this.delegate;
    if (delegateToUse == null) {
        synchronized (this.delegateMonitor) {
            delegateToUse = this.delegate;
            if (delegateToUse == null) {
                WebApplicationContext wac = findWebApplicationContext();
                if (wac == null) {
                    throw new IllegalStateException("No WebApplicationContext found: " +
                            "no ContextLoaderListener or DispatcherServlet registered?");
                }
                delegateToUse = initDelegate(wac);
            }
            this.delegate = delegateToUse;
        }
    }

    // Let the delegate perform the actual doFilter operation.
    invokeDelegate(delegateToUse, request, response, filterChain);
}

protected void invokeDelegate(
        Filter delegate, ServletRequest request, ServletResponse response, FilterChain filterChain)
        throws ServletException, IOException {

    delegate.doFilter(request, response, filterChain);
}

可以看到,当 DelegatingFilterProxy 注册到 tomcat 时,doFilter() 方法主要是调用 invokeDelegate() 方法 来执行代理的 doFilter() 方法。

FilterChainProxy

DelegatingFilterProxy 中,delegate 变量的实际类型是 FilterChainProxy。关键代码如下:

public class FilterChainProxy extends GenericFilterBean {
    // ~ Instance fields
    // ================================================================================================

    private final static String FILTER_APPLIED = FilterChainProxy.class.getName().concat(
            ".APPLIED");

    private List<SecurityFilterChain> filterChains;

    private HttpFirewall firewall = new StrictHttpFirewall();

    // ~ Methods
    // ========================================================================================================

    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException {
        boolean clearContext = request.getAttribute(FILTER_APPLIED) == null;
        if (clearContext) {
            try {
                request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
                doFilterInternal(request, response, chain);
            }
            finally {
                SecurityContextHolder.clearContext();
                request.removeAttribute(FILTER_APPLIED);
            }
        }
        else {
            doFilterInternal(request, response, chain);
        }
    }

    private void doFilterInternal(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException {

        FirewalledRequest fwRequest = firewall
                .getFirewalledRequest((HttpServletRequest) request);
        HttpServletResponse fwResponse = firewall
                .getFirewalledResponse((HttpServletResponse) response);

        List<Filter> filters = getFilters(fwRequest);

        //如果得到的过滤器的数量为零,则直接跳过
        if (filters == null || filters.size() == 0) {
            if (logger.isDebugEnabled()) {
                logger.debug(UrlUtils.buildRequestUrl(fwRequest)
                        + (filters == null ? " has no matching filters"
                                : " has an empty filter list"));
            }

            fwRequest.reset();

            chain.doFilter(fwRequest, fwResponse);

            return;
        }

        VirtualFilterChain vfc = new VirtualFilterChain(fwRequest, chain, filters);
        vfc.doFilter(fwRequest, fwResponse);
    }

    /**
     * Returns the first filter chain matching the supplied URL.
     *
     * @param request the request to match
     * @return an ordered array of Filters defining the filter chain
     */
    private List<Filter> getFilters(HttpServletRequest request) {
        for (SecurityFilterChain chain : filterChains) {
            if (chain.matches(request)) {
                return chain.getFilters();
            }
        }

        return null;
    }

    /**
     * Convenience method, mainly for testing.
     *
     * @param url the URL
     * @return matching filter list
     */
    public List<Filter> getFilters(String url) {
        return getFilters(firewall.getFirewalledRequest((new FilterInvocation(url, "GET")
                .getRequest())));
    }

    /**
     * @return the list of {@code SecurityFilterChain}s which will be matched against and
     * applied to incoming requests.
     */
    public List<SecurityFilterChain> getFilterChains() {
        return Collections.unmodifiableList(filterChains);
    }
    
    private static class VirtualFilterChain implements FilterChain {
        //链条中的节点全部执行完后,处理request请求的对象
        private final FilterChain originalChain;
        private final List<Filter> additionalFilters;
        private final FirewalledRequest firewalledRequest;
        private final int size;
        private int currentPosition = 0;

        private VirtualFilterChain(FirewalledRequest firewalledRequest,
                FilterChain chain, List<Filter> additionalFilters) {
            this.originalChain = chain;
            this.additionalFilters = additionalFilters;
            this.size = additionalFilters.size();
            this.firewalledRequest = firewalledRequest;
        }

        @Override
        public void doFilter(ServletRequest request, ServletResponse response)
                throws IOException, ServletException {
            if (currentPosition == size) {
                if (logger.isDebugEnabled()) {
                    logger.debug(UrlUtils.buildRequestUrl(firewalledRequest)
                            + " reached end of additional filter chain; proceeding with original chain");
                }

                // Deactivate path stripping as we exit the security filter chain
                this.firewalledRequest.reset();

                originalChain.doFilter(request, response);
            }
            else {
                currentPosition++;

                Filter nextFilter = additionalFilters.get(currentPosition - 1);

                nextFilter.doFilter(request, response, this);
            }
        }
    }

}

比较关键的变量是 FILTER_APPLIEDfilterChains,前者在 doFilter 中防止2次处理,后者存储了需要代理的所有 Filter 并在 doFilterInternal 中选择符合 url 条件的 Filter 运行。

内部类 VirtualFilterChain 继承自 FilterChain,使用的是责任链模式。如下图所示:

责任链执行模式

看源代码如何实现责任链模式。

private static class VirtualFilterChain implements FilterChain {
    //链条中的节点全部执行完后,处理request请求的对象
    private final FilterChain originalChain;
    private final List<Filter> additionalFilters;
    private final FirewalledRequest firewalledRequest;
    private final int size;
    private int currentPosition = 0;

    private VirtualFilterChain(FirewalledRequest firewalledRequest,
            FilterChain chain, List<Filter> additionalFilters) {
        this.originalChain = chain;
        this.additionalFilters = additionalFilters;
        this.size = additionalFilters.size();
        this.firewalledRequest = firewalledRequest;
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response)
            throws IOException, ServletException {
        if (currentPosition == size) {
            if (logger.isDebugEnabled()) {
                logger.debug(UrlUtils.buildRequestUrl(firewalledRequest)
                        + " reached end of additional filter chain; proceeding with original chain");
            }

            // Deactivate path stripping as we exit the security filter chain
            this.firewalledRequest.reset();

            originalChain.doFilter(request, response);
        } else {
            currentPosition++;

            Filter nextFilter = additionalFilters.get(currentPosition - 1);

            nextFilter.doFilter(request, response, this);
        }
    }
}

在调用 nextFilter.doFilter(request, response, this) 时,会把自身当作 FilterChain 传入 nextFilter 中,这样,只要 nextFilter 调用 filterChain.doFilter,就会重新回到当前 VirtualFilterChain,并选择下一个 Filter 执行。执行图如下所示:

执行图

spring security core filter 组件分析


在 Spring web Security 中,spring security core filter 以责任链的模式注册在 FilterChainProxy 中,按照顺序依次是:

1. WebAsyncManagerIntegrationFilter
2. SecurityContextPersistenceFilter
3. HeaderWriterFilter
4. CsrfFilter
5. LogoutFilter
6. UsernamePasswordAuthenticationFilter
7. RequestCacheAwareFilter
8. SecurityContextHolderAwareRequestFilter
9. AnonymousAuthenticationFilter
10.SessionManagementFilter
11.ExceptionTranslationFilter
12.FilterSecurityInterceptor

和登陆息息相关的依次是 5,6,9,11,12,下面依次分析这些组件。

LogoutFilter

LogoutFilter 实现的功能比较简单,主要是当遇到 logoutUrl 的时候进行退出的操作,并且跳转到规定界面。

观察源码,如下:

public class LogoutFilter extends GenericFilterBean {

    // ~ Instance fields
    // ================================================================================================

    private RequestMatcher logoutRequestMatcher;

    private final LogoutHandler handler;
    private final LogoutSuccessHandler logoutSuccessHandler;

    // ~ Constructors
    // ===================================================================================================

    /**
     * Constructor which takes a <tt>LogoutSuccessHandler</tt> instance to determine the
     * target destination after logging out. The list of <tt>LogoutHandler</tt>s are
     * intended to perform the actual logout functionality (such as clearing the security
     * context, invalidating the session, etc.).
     */
    public LogoutFilter(LogoutSuccessHandler logoutSuccessHandler,
            LogoutHandler... handlers) {
        this.handler = new CompositeLogoutHandler(handlers);
        Assert.notNull(logoutSuccessHandler, "logoutSuccessHandler cannot be null");
        this.logoutSuccessHandler = logoutSuccessHandler;
        setFilterProcessesUrl("/logout");
    }

    public LogoutFilter(String logoutSuccessUrl, LogoutHandler... handlers) {
        this.handler = new CompositeLogoutHandler(handlers);
        Assert.isTrue(
                !StringUtils.hasLength(logoutSuccessUrl)
                        || UrlUtils.isValidRedirectUrl(logoutSuccessUrl),
                () -> logoutSuccessUrl + " isn't a valid redirect URL");
        SimpleUrlLogoutSuccessHandler urlLogoutSuccessHandler = new SimpleUrlLogoutSuccessHandler();
        if (StringUtils.hasText(logoutSuccessUrl)) {
            urlLogoutSuccessHandler.setDefaultTargetUrl(logoutSuccessUrl);
        }
        logoutSuccessHandler = urlLogoutSuccessHandler;
        setFilterProcessesUrl("/logout");
    }

    // ~ Methods
    // ========================================================================================================

    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);
    }

    /**
     * Allow subclasses to modify when a logout should take place.
     *
     * @param request the request
     * @param response the response
     *
     * @return <code>true</code> if logout should occur, <code>false</code> otherwise
     */
    protected boolean requiresLogout(HttpServletRequest request,
            HttpServletResponse response) {
        return logoutRequestMatcher.matches(request);
    }

    public void setLogoutRequestMatcher(RequestMatcher logoutRequestMatcher) {
        Assert.notNull(logoutRequestMatcher, "logoutRequestMatcher cannot be null");
        this.logoutRequestMatcher = logoutRequestMatcher;
    }

    public void setFilterProcessesUrl(String filterProcessesUrl) {
        this.logoutRequestMatcher = new AntPathRequestMatcher(filterProcessesUrl);
    }
}

首先看 doFilter() 函数,主要逻辑伪代码是:

//step 1.1
if (requiresLogout(request)) {
    //step 1.2
    Authentication auth = SecurityContextHolder.getContext().getAuthentication();
    this.handler.logout(request, response, auth);
    //step 1.3
    logoutSuccessHandler.onLogoutSuccess(request, response, auth);
    return;
} else {
    //step 2.1 
    chain.doFilter(request, response);
}

主要分为 4 步:

  • step 1.1:匹配 request 是否为 logout url
  • step 1.2:利用 handler 实现退出逻辑
  • step 1.3:执行 logoutSuccessHanleronLogoutSuccess 函数,直接返回
  • step 2.1:为匹配到 logout url,进入下一个责任链的 Filter

变量 logoutRequestMatcher 用于匹配 url。
变量 handler 用于处理登出逻辑。在标准 LogoutHandler 实现中,使用了 CompositeLogoutHandler,定义如下:

public final class CompositeLogoutHandler implements LogoutHandler {

    private final List<LogoutHandler> logoutHandlers;

    public CompositeLogoutHandler(LogoutHandler... logoutHandlers) {
        Assert.notEmpty(logoutHandlers, "LogoutHandlers are required");
        this.logoutHandlers = Arrays.asList(logoutHandlers);
    }

    public CompositeLogoutHandler(List<LogoutHandler> logoutHandlers) {
        Assert.notEmpty(logoutHandlers, "LogoutHandlers are required");
        this.logoutHandlers = logoutHandlers;
    }

    @Override
    public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
        for (LogoutHandler handler : this.logoutHandlers) {
            handler.logout(request, response, authentication);
        }
    }
}

CompositeLogoutHandler 实现了 LogoutHandler,并且用责任链的形式执行退出逻辑。在标准实现中,CsrfLogoutHandlerSecurityContextLogoutHandler 被用在了责任链中。主要看 SecurityContextLogoutHandler 的形式。

public class SecurityContextLogoutHandler implements LogoutHandler {
    private boolean invalidateHttpSession = true;
    private boolean clearAuthentication = true;

    // ~ Methods
    // ========================================================================================================

    /**
     * Requires the request to be passed in.
     *
     * @param request from which to obtain a HTTP session (cannot be null)
     * @param response not used (can be <code>null</code>)
     * @param authentication not used (can be <code>null</code>)
     */
    public void logout(HttpServletRequest request, HttpServletResponse response,
            Authentication authentication) {
        Assert.notNull(request, "HttpServletRequest required");
        if (invalidateHttpSession) {
            HttpSession session = request.getSession(false);
            if (session != null) {
                logger.debug("Invalidating session: " + session.getId());
                session.invalidate();
            }
        }

        if (clearAuthentication) {
            SecurityContext context = SecurityContextHolder.getContext();
            context.setAuthentication(null);
        }

        SecurityContextHolder.clearContext();
    }

    public boolean isInvalidateHttpSession() {
        return invalidateHttpSession;
    }

    /**
     * Causes the HttpSession to be invalidated when this  LogoutHandler is invoked. Defaults to true.
     */
    public void setInvalidateHttpSession(boolean invalidateHttpSession) {
        this.invalidateHttpSession = invalidateHttpSession;
    }

    /**
     * If true, removes the Authentication from the SecurityContext to prevent issues with concurrent requests.
     */
    public void setClearAuthentication(boolean clearAuthentication) {
        this.clearAuthentication = clearAuthentication;
    }
}

由此可见,主要是清空 session 和 SecurityContextHolder。

logoutSuccessHandler 变量实现了在登陆成功后实现的逻辑。默认的变量使用 SimpleUrlLogoutSuccessHandler,可以实现路径的跳转。定义如下:

public class SimpleUrlLogoutSuccessHandler extends
        AbstractAuthenticationTargetUrlRequestHandler implements LogoutSuccessHandler {

    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response,
            Authentication authentication) throws IOException, ServletException {
        super.handle(request, response, authentication);
    }

}

AbstractAuthenticationTargetUrlRequestHandler 实现了跳转的功能。

UsernamePasswordAuthenticationFilter

复杂的一个类,主要实现了登陆逻辑。UsernamePasswordAuthenticationFilter 继承自 AbstractAuthenticationProcessingFilterAbstractAuthenticationProcessingFilter 实现了 doFilter() 方法,UsernamePasswordAuthenticationFilter 是使用的 AbstractAuthenticationProcessingFilterdoFilter() 方法。首先看 doFilter() 方法。

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
        throws IOException, ServletException {

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

    //step 1
    if (!requiresAuthentication(request, response)) {
        chain.doFilter(request, response);

        return;
    }
    
    Authentication authResult;

    try {
        //step 2
        authResult = attemptAuthentication(request, response);
        if (authResult == null) {
            // return immediately as subclass has indicated that it hasn't completed
            // authentication
            return;
        }
        sessionStrategy.onAuthentication(authResult, request, response);
    }
    //step 3
    catch (InternalAuthenticationServiceException failed) {
        unsuccessfulAuthentication(request, response, failed);

        return;
    }
    //step 4
    catch (AuthenticationException failed) {
        // Authentication failed
        unsuccessfulAuthentication(request, response, failed);
        return;
    }

    // Authentication success
    //step 5
    if (continueChainBeforeSuccessfulAuthentication) {
        chain.doFilter(request, response);
    }

    //step 6
    successfulAuthentication(request, response, chain, authResult);
}

AbstractAuthenticationProcessingFilter 主要实现了 6 个步骤,如下:

  • step 1:匹配是否为登陆界面并且为POST
  • step 2:尝试进行验证并执行与会话相关的操作
  • step 3:当抛出 InternalAuthenticationServiceException 错误时,执行 unsuccessfulAuthentication 函数并返回
  • step 4:当抛出 AuthenticationException 错误,即验证失败时,执行 unsuccessfulAuthentication 函数并返回
  • step 5:当 ontinueChainBeforeSuccessfulAuthenticationtrue 时,执行下一个 Filter 的函数
  • step 6:执行 successfulAuthentication() 函数。

step 2 执行 attemptAuthentication() 函数,是一个抽象函数,具体的实现在 UsernamePasswordAuthenicationFilter 中。

note

  • attemptAuthentication() 必须返回一个已验证用户填充的 Authentication,表明验证通过。
  • 或者返回一个 null 表明表示身份验证过程仍在进行中。在返回之前,实现应该执行完成流程所需的任何额外工作。
  • 如果身份验证过程失败,抛出 AuthenticationException 异常。

UsernamePasswordAuthenicationFilterattemptAuthentication 实现如下:

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);
}

obtainXXX() 函数通过使用 request.getParameter("XXX")获得参数,最重要的逻辑在 this.getAuthenticationManager().authenticate(authRequest) 中。

在介绍具体的验证逻辑之前,介绍几个基本的概念和类。

首先是 Autnenication 接口,这个接口就是我们所说的令牌,保存了该用户的 principal、credential、details、authorities。就用户名密码登陆来说,principal 就是用户名,credential 就是密码,details 就是 IP 之类的详细信息,authorities 代表的就是被授权的权利。部分如下:

public interface Authentication extends Principal, Serializable {
    // ~ Methods
    // ========================================================================================================

    /**
     * Set by an AuthenticationManager to indicate the authorities that the
     * principal has been granted. Implementations should ensure that modifications to the returned collection array do not affect the state of the Authentication object, or use an unmodifiable instance.
     */
    Collection<? extends GrantedAuthority> getAuthorities();

    /**
     * The credentials that prove the principal is correct. This is usually a password, but could be anything relevant to the AuthenticationManager. Callers are expected to populate the credentials.
     */
    Object getCredentials();

    Object getDetails();

    /**
     * The identity of the principal being authenticated. In the case of an authentication request with username and password, this would be the username. Callers are expected to populate the principal for an authentication request.
     * Many of the authentication providers will create a code UserDetails object as the principal.
     */
    Object getPrincipal();

    /**
     * Used to indicate to AbstractSecurityInterceptor whether it should present
     * the authentication token to the AuthenticationManager. Typically an
     * AuthenticationManage(or, more often, one of its
     * AuthenticationProviders) will return an immutable authentication token
     * after successful authentication, in which case that token can safely return
     * true to this method. Returning <code>true</code> will improve
     * performance, as calling the <code>AuthenticationManager</code> for every request
     * will no longer be necessary.
     */
    boolean isAuthenticated();

    void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}

Authentication 的抽象实现 AbstractAuthenticationToken 中,赋予了 principal 更多的意义,一般使用 userDetails 类作为 principal,已存储更多的信息,如被授予的权限、用户名、密码、是否账号过期、是否账号被锁、账号是否可用等信息。

UsernamePasswordAuthenticationToken 实现了 AbstractAuthenticationToken

AuthenticationManager,是主要的验证方法。
定义如下:

public interface AuthenticationManager {
    // ~ Methods
    // ========================================================================================================

    /**
     * Attempts to authenticate the passed Authentication object, returning a
     * fully populated Authentication object (including granted authorities)
     * if successful.
     *
     * @param authentication the authentication request object
     *
     * @return a fully authenticated object including credentials
     *
     * @throws AuthenticationException if authentication fails
     */
    Authentication authenticate(Authentication authentication)
            throws AuthenticationException;
}

AuthenticationManager 的实现必须遵循以下的原则:

  • 当账户不能使用并且 AuthenticationManager 可以检测到这个状态时必须抛出 DisabledException(A DisabledException must be thrown if an account is disabled and the AuthenticationManager can test for this state)
  • 当账户被锁定并且 AuthenticationManager 可以检测到这个状态时必须抛出 LockedException 错误。(A LockedException must be thrown if an account is locked and the AuthenticationManager can test for account locking)
  • 如果出现不正确的凭证,则必须抛出 BadCredentialsException(A BadCredentialsException must be thrown if incorrect credentials arepresented. Whilst the above exceptions are optional, an AuthenticationManager must always test credentials)
  • 异常应该按照上述顺序进行测试(例如,如果帐户被禁用或锁定,则立即拒绝身份验证请求,且不执行凭据测试过程),并在适用的情况下抛出异常。此证书将针对禁用或锁定帐户进行测试(Exceptions should be tested for and if applicable thrown in the order expressedabove (i.e. if an account is disabled or locked, the authentication request isimmediately rejected and the credentials testing process is not performed). Thisprevents credentials being tested against disabled or locked accounts)

在 spring security 中,AuthenticationManager 只有一个实现,即 ProviderManager。在 ProviderManager 中,最重要的是维持了一个 AuthenticationProvider 列表。
AuthenticationProvider 接口定义如下:

public interface AuthenticationProvider {
    
    //和AuthenticationManager相同
    Authentication authenticate(Authentication authentication)
            throws AuthenticationException;

    //如果此AuthenticationProvider支持指定的身份验证对象,则返回true。
    //返回true并不保证AuthenticationProvider能够对身份验证类的呈现实例进行身份验证。它只是表明它可以支持对其进行更密切的评估。AuthenticationProvider仍然可以从authenticate(Authentication)方法返回null,以指示应该尝试另一个AuthenticationProvider。
    //选择能够执行身份验证的AuthenticationProvider是在ProviderManager运行时进行的。
    boolean supports(Class<?> authentication);
}

ProviderManagerauthenticate 方法,如下:

public Authentication authenticate(Authentication authentication)
        throws AuthenticationException {
    Class<? extends Authentication> toTest = authentication.getClass();
    AuthenticationException lastException = null;
    AuthenticationException parentException = null;
    Authentication result = null;
    Authentication parentResult = null;

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

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

    if (result == null && parent != null) {
        // Allow the parent to try.
        try {
            result = parentResult = parent.authenticate(authentication);
        }
        catch (ProviderNotFoundException e) {
            // ignore as we will throw below if no other exception occurred prior to
            // calling parent and the parent
            // may throw ProviderNotFound even though a provider in the child already
            // handled the request
        }
        catch (AuthenticationException e) {
            lastException = parentException = e;
        }
    }

    if (result != null) {
        if (eraseCredentialsAfterAuthentication
                && (result instanceof CredentialsContainer)) {
            // Authentication is complete. Remove credentials and other secret data
            // from authentication
            ((CredentialsContainer) result).eraseCredentials();
        }

        // If the parent AuthenticationManager was attempted and successful than it will publish an AuthenticationSuccessEvent
        // This check prevents a duplicate AuthenticationSuccessEvent if the parent AuthenticationManager already published it
        if (parentResult == null) {
            eventPublisher.publishAuthenticationSuccess(result);
        }
        return result;
    }

    // Parent was null, or didn't authenticate (or throw an exception).

    if (lastException == null) {
        lastException = new ProviderNotFoundException(messages.getMessage(
                "ProviderManager.providerNotFound",
                new Object[] { toTest.getName() },
                "No AuthenticationProvider found for {0}"));
    }

    // If the parent AuthenticationManager was attempted and failed than it will publish an AbstractAuthenticationFailureEvent
    // This check prevents a duplicate AbstractAuthenticationFailureEvent if the parent AuthenticationManager already published it
    if (parentException == null) {
        prepareException(lastException, authentication);
    }
    throw lastException;
}

具体的实现逻辑是调用支持该 AuthenticationAuthenticationProvider进行验证,如果遇到账号被锁定或者被禁用(即抛出 AccountStatusException 异常,这是 DisabledExceptionLockedException 的父类),如果验证不正确,即遇到 AuthenticationException 异常,则记录最新异常到 lastException,并执行下一个 provider。所有 provider 执行完成之后,如果 result 为空,则说明没有验证通过,如果存在 AuthenticationManager parent,则尝试执行 parent 的验证函数。如果这一步执行完之后 result 不为空,则返回 result,否则抛出 lastException

Spring 默认使用 DaoThenticationProvider 来实现 AuthenticationProviderDaoAuthenticationProvider 继承自 AbstractUserDetailsAuthenticationProviderAbstractUserDetailsAuthenticationProviderAuthenticate() 方法如下(只支持 UsernamePasswordAuthenticationToken):

public Authentication authenticate(Authentication authentication)
        throws AuthenticationException {
    Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
            () -> messages.getMessage(
                    "AbstractUserDetailsAuthenticationProvider.onlySupports",
                    "Only UsernamePasswordAuthenticationToken is supported"));

    // Determine username
    String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
            : authentication.getName();

    boolean cacheWasUsed = true;
    UserDetails user = this.userCache.getUserFromCache(username);

    if (user == null) {
        cacheWasUsed = false;

        try {
            user = retrieveUser(username,
                    (UsernamePasswordAuthenticationToken) authentication);
        }
        catch (UsernameNotFoundException notFound) {
            logger.debug("User '" + username + "' not found");

            if (hideUserNotFoundExceptions) {
                throw new BadCredentialsException(messages.getMessage(
                        "AbstractUserDetailsAuthenticationProvider.badCredentials",
                        "Bad credentials"));
            }
            else {
                throw notFound;
            }
        }

        Assert.notNull(user,
                "retrieveUser returned null - a violation of the interface contract");
    }

    try {
        preAuthenticationChecks.check(user);
        additionalAuthenticationChecks(user,
                (UsernamePasswordAuthenticationToken) authentication);
    }
    catch (AuthenticationException exception) {
        if (cacheWasUsed) {
            // There was a problem, so try again after checking
            // we're using latest data (i.e. not from the cache)
            cacheWasUsed = false;
            user = retrieveUser(username,
                    (UsernamePasswordAuthenticationToken) authentication);
            preAuthenticationChecks.check(user);
            additionalAuthenticationChecks(user,
                    (UsernamePasswordAuthenticationToken) authentication);
        }
        else {
            throw exception;
        }
    }

    postAuthenticationChecks.check(user);

    if (!cacheWasUsed) {
        this.userCache.putUserInCache(user);
    }
    Object principalToReturn = user;
    if (forcePrincipalAsString) {
        principalToReturn = user.getUsername();
    }
    return createSuccessAuthentication(principalToReturn, authentication, user);
}

处理逻辑是首先根据 Token authentication 获得用户名,然后判断是否有 UserDetail 缓存,如果没有,通过 retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication) 检索得到 userDetail,如果没有找到,抛出 UsernameNotFoundException 异常或者 BadCredentialsException 异常。当有 userDetail 之后,调用 DefaultPreAuthenticationCheckscheck() 函数,即 preAuthenticationChecks.check(user) 用来测试 userDetail 账号是否被锁,账号是否不可用,账号是否过期,并抛出相应错误。完成之后,执行 additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication),这个函数是一个抽象函数,要求子类实现,增加更多的检测。如果抛出 AuthenticationException 异常,并且是从缓存中获得 userDetail 的话,会重新调用 retrieveUser 重新检测,如果都不通过,才彻底抛出异常。如果没有抛出,会执行 postAuthenticationChecks.checks(user),默认实现是 DefaultPostAuthenticationChecks 主要是检查 密钥是否过期,如过期,抛出 CredentialsExpiredException 异常。最后调用 createSuccessAuthentication 创建 UsernamePasswordAuthenticationToken 并返回。

createSuccessAuthentication() 函数如下:

    protected Authentication createSuccessAuthentication(Object principal,
            Authentication authentication, UserDetails user) {
        UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(
                principal, authentication.getCredentials(),
                authoritiesMapper.mapAuthorities(user.getAuthorities()));
        result.setDetails(authentication.getDetails());

        return result;
    }

注意 CredentialsExpiredExceptionLockedExceptionDisabledExceptionAccountExpiredException都是 AccountStatusException 的子类,都会被 ProviderManager 捕获并且直接抛出错误

DaoAuthenticationProvider 实现了 additionalAuthenticationChecks()retrieveUser() 函数,并且改写了 createSuccessAuthentication() 函数。

additionalAuthenticationChecks() 如下:

protected void additionalAuthenticationChecks(UserDetails userDetails,
        UsernamePasswordAuthenticationToken authentication)
        throws AuthenticationException {
    if (authentication.getCredentials() == null) {
        logger.debug("Authentication failed: no credentials provided");

        throw new BadCredentialsException(messages.getMessage(
                "AbstractUserDetailsAuthenticationProvider.badCredentials",
                "Bad credentials"));
    }

    String presentedPassword = authentication.getCredentials().toString();

    if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
        logger.debug("Authentication failed: password does not match stored value");

        throw new BadCredentialsException(messages.getMessage(
                "AbstractUserDetailsAuthenticationProvider.badCredentials",
                "Bad credentials"));
    }
}

如上,additionalAuthenticationChecks() 主要是增加了密码验证的逻辑,如果验证不通过,抛出 BadCredentialsException 错误。

retrieveUser() 函数主要实现 userDetail 的获取。

protected final UserDetails retrieveUser(String username,
        UsernamePasswordAuthenticationToken authentication)
        throws AuthenticationException {
    prepareTimingAttackProtection();
    try {
        UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
        if (loadedUser == null) {
            throw new InternalAuthenticationServiceException(
                    "UserDetailsService returned null, which is an interface contract violation");
        }
        return loadedUser;
    }
    catch (UsernameNotFoundException ex) {
        mitigateAgainstTimingAttack(authentication);
        throw ex;
    }
    catch (InternalAuthenticationServiceException ex) {
        throw ex;
    }
    catch (Exception ex) {
        throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
    }
}

如上所示,主要是通过 this.getUserDetailsService().loadUserByUsername(username) 函数获取,this.getUserDetailsService() 返回一个 UserDetailsService 接口实现 UserDetail 的查询。

createSuccessAuthentication() 主要增加了可否使用增强密钥的判断,增加了安全性。

protected Authentication createSuccessAuthentication(Object principal,
        Authentication authentication, UserDetails user) {
    boolean upgradeEncoding = this.userDetailsPasswordService != null
            && this.passwordEncoder.upgradeEncoding(user.getPassword());
    if (upgradeEncoding) {
        String presentedPassword = authentication.getCredentials().toString();
        String newPassword = this.passwordEncoder.encode(presentedPassword);
        user = this.userDetailsPasswordService.updatePassword(user, newPassword);
    }
    return super.createSuccessAuthentication(principal, authentication, user);
}

UserDetailsService 接口最常见的实现为 InMemoryUserDetailsManagerJdbcUserDetailsManager,前者在内存中维护一个 <<String -> UserDetail>> 映射,后者直接从数据库中读取数据,前者的 loadUserByUsername 实现如下:

public UserDetails loadUserByUsername(String username)
        throws UsernameNotFoundException {
    UserDetails user = users.get(username.toLowerCase());

    if (user == null) {
        throw new UsernameNotFoundException(username);
    }

    return new User(user.getUsername(), user.getPassword(), user.isEnabled(),
            user.isAccountNonExpired(), user.isCredentialsNonExpired(),
            user.isAccountNonLocked(), user.getAuthorities());
}

用于验证的接口及类图如下所示:

用于验证的接口与类url图

验证流程如下:

验证流程

回到 UsernamePasswordAuthenticationFilter,我们知道,当验证失败时,会抛出三种错误,第一种为 AccountStatusException,第二种为 InternalAuthenticationServiceException,第三种为 AuthenticationException。在 UsernamePasswordAuthenticationFilter 的第三步,第四步,分别用两个 catch 语句块进行捕捉,进行错误处理然后直接返回,如下:

catch (InternalAuthenticationServiceException failed) {
        unsuccessfulAuthentication(request, response, failed);

    return;
}
//step 4
// AccountStatusException 为 AuthenticationException 子类,这个捕捉函数可以捕捉到 AccountStatusException 和 AuthenticationException 两种异常
catch (AuthenticationException failed) {
    // Authentication failed
    unsuccessfulAuthentication(request, response, failed);
    return;
}

unsucessfulAuthentication() 函数的功能较简单,即调用 rememberMeServices.loginFailfailureHandler.onAuthenticationFailure 设置失败的操作。

protected void unsuccessfulAuthentication(HttpServletRequest request,
        HttpServletResponse response, AuthenticationException failed)
        throws IOException, ServletException {
        SecurityContextHolder.clearContext();

    rememberMeServices.loginFail(request, response);

    failureHandler.onAuthenticationFailure(request, response, failed);
}

重点看 failureHandler.onAuthenticationFailure
failureHander 是实现了 AuthenticationFailureHandler 的类,默认实现是 SimpleUrlAuthenticationFailureHandleronAuthenticationFailure 函数如下,可以看出其主要目的是实现跳转或者重定向。

public void onAuthenticationFailure(HttpServletRequest request,
        HttpServletResponse response, AuthenticationException exception)
        throws IOException, ServletException {

    if (defaultFailureUrl == null) {
        logger.debug("No failure URL set, sending 401 Unauthorized error");

        response.sendError(HttpStatus.UNAUTHORIZED.value(),
            HttpStatus.UNAUTHORIZED.getReasonPhrase());
    } else {
        saveException(request, exception);

        if (forwardToDestination) {
            logger.debug("Forwarding to " + defaultFailureUrl);

            request.getRequestDispatcher(defaultFailureUrl)
                    .forward(request, response);
        } else {
            logger.debug("Redirecting to " + defaultFailureUrl);
            redirectStrategy.sendRedirect(request, response, defaultFailureUrl);
        }
    }
}

UsernamePasswordAuthenticationFilter 的第六步即认证成功后的护理,主要是调用 successfulAuthentication() 函数进行处理。函数如下:

protected void successfulAuthentication(HttpServletRequest request,
        HttpServletResponse response, FilterChain chain, Authentication authResult)
        throws IOException, ServletException {

    SecurityContextHolder.getContext().setAuthentication(authResult);

    rememberMeServices.loginSuccess(request, response, authResult);

    // Fire event
    if (this.eventPublisher != null) {
        eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
                authResult, this.getClass()));
    }

    successHandler.onAuthenticationSuccess(request, response, authResult);
}

主要工作就是把密钥存储在 SecurityContextHolder 上,并调用 successHandler.onAuthenticationSuccess() 实现相关的操作。与错误情况类似,登陆成功也主要是进行一些跳转。

AnonymousAuthenticationFilter

AnonymousAuthenticationFilter 的逻辑很简单,当在 SecurityContextHolder 中没有值时,就创建一个匿名的 Token,传递到下一个 Filter,代码如下:

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
        throws IOException, ServletException {

    if (SecurityContextHolder.getContext().getAuthentication() == null) {
        SecurityContextHolder.getContext().setAuthentication(
                createAuthentication((HttpServletRequest) req));
    } else {}
    chain.doFilter(req, res);
}

protected Authentication createAuthentication(HttpServletRequest request) {
    AnonymousAuthenticationToken auth = new AnonymousAuthenticationToken(key,
            principal, authorities);
    auth.setDetails(authenticationDetailsSource.buildDetails(request));

    return auth;
}

ExceptionTranslationFilter

FilterSecurityInterceptor

FilterSecurityInterceptor 继承自 AbstractSecurityInterceptor。主要作用是通过 Filter 接口实现对 http 资源的控制。
FilterSecurityInterceptordoFilter 函数如下:

public void doFilter(ServletRequest request, ServletResponse response,
        FilterChain chain) throws IOException, ServletException {
    FilterInvocation fi = new FilterInvocation(request, response, chain);
    invoke(fi);
}

其中,FilterInvocation 的主要作用是对 request, response, chain 的封装。重点函数还是在 invoke() 上。 invoke()函数如下所示:

public void invoke(FilterInvocation fi) throws IOException, ServletException {
    // step 1
    if ((fi.getRequest() != null)
            && (fi.getRequest().getAttribute(FILTER_APPLIED) != null)
            && observeOncePerRequest) {
        // filter already applied to this request and user wants us to observe
        // once-per-request handling, so don't re-do security checking
        fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
    } else {
        // step 2
        // first time this request being called, so perform security checking
        if (fi.getRequest() != null && observeOncePerRequest) {
            fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
        }

        // step 3
        InterceptorStatusToken token = super.beforeInvocation(fi);

        // step 4
        try {
            fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
        } finally {
            super.finallyInvocation(token);
        }

        // step 5
        super.afterInvocation(token, null);
    }
}

其中,step 1,2 的作用主要是判断是否是一次应用并且已经应用的 request,如果是则直接进入下一个 Filter,如果不是并且还没有应用,则设应用标志为 true 进行处理。step 3 对 fi 做验证,step 4,5 对 fi做一些其他的处理。

beforeInvocation() 的实现在父类 AbstractSecurityIntercepter 中,如下:

protected InterceptorStatusToken beforeInvocation(Object object) {

    if (!getSecureObjectClass().isAssignableFrom(object.getClass())) {
        throw new IllegalArgumentException(
                "Security invocation attempted for object "
                        + object.getClass().getName()
                        + " but AbstractSecurityInterceptor only configured to support secure objects of type: "
                        + getSecureObjectClass());
    }

    Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
            .getAttributes(object);

    if (attributes == null || attributes.isEmpty()) {
        if (rejectPublicInvocations) {
            throw new IllegalArgumentException(
                    "Secure object invocation "
                            + object
                            + " was denied as public invocations are not allowed via this interceptor. "
                            + "This indicates a configuration error because the "
                            + "rejectPublicInvocations property is set to 'true'");
        }

        if (debug) {
            logger.debug("Public object - authentication not attempted");
        }

        publishEvent(new PublicInvocationEvent(object));

        return null; // no further work post-invocation
    }

    if (debug) {
        logger.debug("Secure object: " + object + "; Attributes: " + attributes);
    }

    if (SecurityContextHolder.getContext().getAuthentication() == null) {
        credentialsNotFound(messages.getMessage(
                "AbstractSecurityInterceptor.authenticationNotFound",
                "An Authentication object was not found in the SecurityContext"),
                object, attributes);
    }

    Authentication authenticated = authenticateIfRequired();

    // Attempt authorization
    try {
        this.accessDecisionManager.decide(authenticated, object, attributes);
    }
    catch (AccessDeniedException accessDeniedException) {
        publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,
                accessDeniedException));

        throw accessDeniedException;
    }

    if (debug) {
        logger.debug("Authorization successful");
    }

    if (publishAuthorizationSuccess) {
        publishEvent(new AuthorizedEvent(object, attributes, authenticated));
    }

    // Attempt to run as a different user
        Authentication runAs = this.runAsManager.buildRunAs(authenticated, object,
            attributes);

    if (runAs == null) {
        if (debug) {
            logger.debug("RunAsManager did not change Authentication object");
        }

        // no further work post-invocation
        return new InterceptorStatusToken(SecurityContextHolder.getContext(), false,
                attributes, object);
    } else {
        if (debug) {
            logger.debug("Switching to RunAs Authentication: " + runAs);
        }

        SecurityContext origCtx = SecurityContextHolder.getContext();
        SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext());
        SecurityContextHolder.getContext().setAuthentication(runAs);

        // need to revert to token.Authenticated post-invocation
        return new InterceptorStatusToken(origCtx, true, attributes, object);
    }
}

主要是调用 this.accessDecisionManager.decide(authenticated, object, attributes) 进行授权。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,552评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,666评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,519评论 0 334
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,180评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,205评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,344评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,781评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,449评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,635评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,467评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,515评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,217评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,775评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,851评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,084评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,637评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,204评论 2 341

推荐阅读更多精彩内容