美文网首页
Spring 源码分析(四)Sercurity

Spring 源码分析(四)Sercurity

作者: sschrodinger | 来源:发表于2019-05-28 14:47 被阅读0次

Spring 源码分析(四)Sercurity

sschrodinger

2019/03/04


登陆验证流程


假设有如下的 spring security 配置。

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
        .inMemoryAuthentication()
        .passwordEncoder(NoOpPasswordEncoder.getInstance())
        .withUser("user_1").password("user_1").roles("USER")
        .and()
        .withUser("user_2").password("user_2").roles("USER");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // TODO Auto-generated method stub
        http
        .authorizeRequests()
            .antMatchers("/resources/**").permitAll()
            .antMatchers("/register").permitAll()
            .anyRequest().authenticated()
            .and()
        .formLogin()
            .loginPage("/login")
            .defaultSuccessUrl("/home", true)
            .permitAll();
    }
    
     @Bean
     @Override
     public AuthenticationManager authenticationManagerBean() throws Exception {
         return super.authenticationManagerBean();
     }

}

输入任意 url (非/login),流程如下:

step 1

在 Spring 中,登陆验证的逻辑始于 DelegatingFilterProxy,这是一个继承了 Filter 的类,用于实现 Java Servlet 规范的过滤器。

DelegatingFilterProxydoFilter() 方法如下:

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

最重要的是 invokeDelegate() 方法,Spring 所有的 Filter 逻辑都是在 invokeDelegate() 方法中完成的,所以没有 filterChain.doFilter(request, response, filterChain) 将处理过程抛出到下一 Filter。

参数 delefateToUse 包括了所有的过滤器,调试信息如下:

delegate    FilterChainProxy  (id=124)
    |-...//其他
    |-beanName  "springSecurityFilterChain" (id=126)
    |-filterChains  ArrayList<E>  (id=133)  
        |-elementData   Object[1]  (id=158) 
            |-[0]   DefaultSecurityFilterChain  (id=3344)   
                |-org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@630e5010,
                |-org.springframework.security.web.context.SecurityContextPersistenceFilter@4eab9aec,
                |-org.springframework.security.web.header.HeaderWriterFilter@533e8807,
                |-org.springframework.security.web.csrf.CsrfFilter@156eeff1,
                |-org.springframework.security.web.authentication.logout.LogoutFilter@656c0eae,
                |-org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@267b678f,
                |-org.springframework.security.web.savedrequest.RequestCacheAwareFilter@2c6efee3,
                |-org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@56adbb07,
                |-org.springframework.security.web.authentication.AnonymousAuthenticationFilter@10b5ff4d,
                |-org.springframework.security.web.session.SessionManagementFilter@2b4ba2d9,
                |-org.springframework.security.web.access.ExceptionTranslationFilter@2bc0603f,
                |-org.springframework.security.web.access.intercept.FilterSecurityInterceptor@4e826fd4

Spring security 的验证工作都是在 DelegatingFilterProxy 中的一个名为 springSecurityFilterChain 中完成的。

我们看 invokeDelegate() 的源码,如下:

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

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

step 2

第二步进入 delegatedoFilter() 方法执行。

delegate 的本质是继承了 Filter 的类。维护了一个 SecurityFilterChain 列表。SecurityFilterChain 定义如下:

public interface SecurityFilterChain {

    boolean matches(HttpServletRequest request);

    List<Filter> getFilters();
}

在当前环境中,List<SecurityFilterChain> 只维护了一个元素。即

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

我们看 doFilter() 方法,首先,判断 request 是否有 FILTER_APPLIED 属性,如果没有,则需要添加,然后执行 doFilterInternal() 方法。

getFilters(fwRequest) 通过 url 得到符合条件的所有 filterChain 的第一个 Filter。即会得到如下 12 个 Filter:

org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter,
org.springframework.security.web.context.SecurityContextPersistenceFilter,
org.springframework.security.web.header.HeaderWriterFilter,
org.springframework.security.web.csrf.CsrfFilter,
org.springframework.security.web.authentication.logout.LogoutFilter,
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter,
org.springframework.security.web.savedrequest.RequestCacheAwareFilter,
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter,
org.springframework.security.web.authentication.AnonymousAuthenticationFilter,
org.springframework.security.web.session.SessionManagementFilter,
org.springframework.security.web.access.ExceptionTranslationFilter,
org.springframework.security.web.access.intercept.FilterSecurityInterceptor

变量chain代表原始 FilterChain,当 getFilters(fwRequest) 返回不为 null 时,返回值和 chain 会组合成 VirtualFilterChain,构造函数如下:

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

VirtualFilterChaindoFilter 函数如下:

@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 {
        //currrent addition filter pos, init is zero
        currentPosition++;

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

        if (logger.isDebugEnabled()) {
            logger.debug(UrlUtils.buildRequestUrl(firewalledRequest)
                    + " at position " + currentPosition + " of " + size
                    + " in additional filter chain; firing Filter: '"
                    + nextFilter.getClass().getSimpleName() + "'");
        }

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

注意,当执行 VirtualFilterChaindoFilter 函数时,会先执行 else 语句,currentPosition 先自加,然后将自身作为参数执行 nextFilterdofilter() 方法,这样就会重复执行 else 语句,将所有的 additionalFilter 全部执行完。

step 3

执行所有 VirtualFilterChain 中的 Filter

nextFilter() 依次执行如下 12 个 Filter

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

其中比较重要的是2,5,6,9,12三个 filter

SecurityContextPersistenceFilter 主要是对保存的密码信息等做持久化处理。

LogoutFilter 主要是匹配退出 url 并作退出处理逻辑。

UsernamePasswordAuthenticationFilter 主要作用是当匹配登录界面时,做一些登陆的操作,否则直接到下一过滤器。

AnonymousAuthenticationFilter 主要作用是当没有进行登陆时,自动创建匿名用户。

FilterSecurityInterceptor 继承了 AbstractSecurityInterceptor,主要用于登陆的核心逻辑。

note(AbstractSecurityInterceptor api note)

  • Abstract class that implements security interception for secure objects.
  • The AbstractSecurityInterceptor will ensure the proper startupconfiguration of the security interceptor. It will also implement the proper handlingof secure object invocations, namely:
    • Obtain the Authentication object from the SecurityContextHolder.
    • Determine if the request relates to a secured or public invocation by looking upthe secure object request against the SecurityMetadataSource.
    • For an invocation that is secured (there is a list of ConfigAttributesfor the secure object invocation):
      • If either the org.springframework.security.core.Authentication.isAuthenticated() returns false, or the alwaysReauthenticate is true,authenticate the request against the configured AuthenticationManager. Whenauthenticated, replace the Authentication object on the SecurityContextHolder with the returned value.
      • Authorize the request against the configured AccessDecisionManager.
      • Perform any run-as replacement via the configured RunAsManager.
      • Pass control back to the concrete subclass, which will actually proceed withexecuting the object. A InterceptorStatusToken is returned so that after thesubclass has finished proceeding with execution of the object, its finally clause canensure the AbstractSecurityInterceptor is re-called and tidies upcorrectly using finallyInvocation(InterceptorStatusToken).
      • The concrete subclass will re-call the AbstractSecurityInterceptor viathe afterInvocation(InterceptorStatusToken, Object) method.
      • If the RunAsManager replaced the Authentication object,return the SecurityContextHolder to the object that existed after the callto AuthenticationManager.
      • If an AfterInvocationManager is defined, invoke the invocation managerand allow it to replace the object due to be returned to the caller.
    • For an invocation that is public (there are no ConfigAttributes forthe secure object invocation):
      • As described above, the concrete subclass will be returned an InterceptorStatusToken which is subsequently re-presented to the AbstractSecurityInterceptor after the secure object has been executed. The AbstractSecurityInterceptor will take no further action when its afterInvocation(InterceptorStatusToken, Object) is called.
    • Control again returns to the concrete subclass, along with the Objectthat should be returned to the caller. The subclass will then return that result orexception to the original caller.

FilterSecurityInterceptorinvoke() 方法如下:

public void invoke(FilterInvocation fi) throws IOException, ServletException {
    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 {
        // first time this request being called, so perform security checking
        if (fi.getRequest() != null && observeOncePerRequest) {
            fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
        }

        InterceptorStatusToken token = super.beforeInvocation(fi);

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

        super.afterInvocation(token, null);
    }
}

beforeInvocation() 函数的处理逻辑是先验证,如果不满足条件,则抛出 accessDeniedException 错误,否则生成 InterceptorStatusToken 给上一级。

protected InterceptorStatusToken beforeInvocation(Object object) {
    Assert.notNull(object, "Object was null");
    final boolean debug = logger.isDebugEnabled();

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

ExceptionTranslationFilter 会捕捉 FilterSecurityInterceptor 抛出的错误,并进行错误处理。错误处理语句如下:

catch (Exception ex) {
    // Try to extract a SpringSecurityException from the stacktrace
    Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);
    RuntimeException ase = (AuthenticationException) throwableAnalyzer
            .getFirstThrowableOfType(AuthenticationException.class, causeChain);

    if (ase == null) {
        ase = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType(
                AccessDeniedException.class, causeChain);
    }

    if (ase != null) {
        if (response.isCommitted()) {
            throw new ServletException("Unable to handle the Spring Security Exception because the response is already committed.", ex);
        }
        handleSpringSecurityException(request, response, chain, ase);
    }
    else {
        // Rethrow ServletExceptions and RuntimeExceptions as-is
        if (ex instanceof ServletException) {
            throw (ServletException) ex;
        }
        else if (ex instanceof RuntimeException) {
            throw (RuntimeException) ex;
    }

    // Wrap other Exceptions. This shouldn't actually happen
        // as we've already covered all the possibilities for doFilter
        throw new RuntimeException(ex);
    }
}

错误的主要处理过程在 handleSpringSecurityException(request, response, chain, ase) 中,如下:

private void handleSpringSecurityException(HttpServletRequest request,
    HttpServletResponse response, FilterChain chain, RuntimeException exception)
    throws IOException, ServletException {
    if (exception instanceof AuthenticationException) {
        logger.debug(
                "Authentication exception occurred; redirecting to authentication entry point",
                exception);

        sendStartAuthentication(request, response, chain,
                    (AuthenticationException) exception);
    } else if (exception instanceof AccessDeniedException) {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authenticationTrustResolver.isAnonymous(authentication) || authenticationTrustResolver.isRememberMe(authentication)) {
            logger.debug(
                    "Access is denied (user is " + (authenticationTrustResolver.isAnonymous(authentication) ? "anonymous" : "not fully authenticated") + "); "redirecting to authentication entry point",
                    exception);

            sendStartAuthentication(
                    request,
                    response,
                    chain,
                    new InsufficientAuthenticationException(
                        messages.getMessage(
                            "ExceptionTranslationFilter.insufficientAuthentication",
                            "Full authentication is required to access this resource")));
        } else {
            logger.debug(
                    "Access is denied (user is not anonymous); delegating to AccessDeniedHandler",
                    exception);

            accessDeniedHandler.handle(request, response,
                    (AccessDeniedException) exception);
        }
    }
}

sendStartAuthentication 函数中,会对连接进行重定向到规定的登陆界面。

如在 /login 界面利用 POST 提交表单信息登陆。流程的 step 1 和 step 2 和上相同,step 3 略有区别。在执行到 UsernamePasswordAuthenticationFilter 时,就会进行登陆逻辑,代码如下:

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

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

    if (!requiresAuthentication(request, response)) {
        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;
        }
        sessionStrategy.onAuthentication(authResult, request, response);
    }
    catch (InternalAuthenticationServiceException failed) {
        logger.error(
                "An internal error occurred while trying to authenticate the user.",
                failed);
        unsuccessfulAuthentication(request, response, failed);

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

        return;
    }

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

    successfulAuthentication(request, response, chain, authResult);
}

具体逻辑是 attemptAuthentication() 函数进行验证,当验证成功时,执行 successfulAuthentication(request, response, chain, authResult) 进行跳转,失败时,执行 unsuccessfulAuthentication(request, response, failed) 进行跳转,假如设置了跳转地址,则回继续调用 doFilter() 方法,在 FilterSecurityInterceptor 中利用UserNamePasswordToken 进行授权。

综上,时序图(关键类)如下:


spring security

相关文章

网友评论

      本文标题:Spring 源码分析(四)Sercurity

      本文链接:https://www.haomeiwen.com/subject/ahkdtctx.html