天天看点

想了解 Spring Security 的原理,看这里

作者:南湘嘉荣
想了解 Spring Security 的原理,看这里

作者|知视

本文主要介绍Spring Security的实现基础,Servlet过滤器链的知识

如果觉得一件事做起来很难,那是你不了解它。

有人说Spring Security很难,其实那是因为你不了解它的原理。今天我们就来说说Spring Security的原理。

Spring Security提供的支持很多,但是作为Web应用开发人员来说,我们只需要了解其中关于Servlet的知识就够了。

进入正题。

我想,在讲Spring Security原理之前,很有必要先说一下什么是过滤器。生活中,我们会用过滤器将浑浊的水变成清澈干净的纯净水。

在Web应用中,Servlet提供的java.servlet.Filter接口同样有类似的作用。过滤器按照某些规则,将请求拦截下来并做某些处理之后,再将请求交给Servlet。

Spring Security对Servlet的支持就是基于一条关于Servlet的过滤器链。

想了解 Spring Security 的原理,看这里

如图,展示了在所有Servlet应用程序中一个HTTP请求处理程序的典型分层。当一个客户端向应用程序发送请求后,Servlet容器会创建一个FilterChain过滤器链。这个过滤器链里包含多个过滤器和一个基于请求URI路径来处理HttpServletRequest的Serv

-let。如果当前应用是一个Spring MVC应用,那么这个Servlet就DispatchServlet的一个实例。

由于过滤器只会影响下游的过滤器和Servlet,所以过滤器的顺序非常重要。

到这里,我们先来说下Servlet的FilterChain过滤器链原理。

请求在到达过滤器链之前,到底经历了什么?换个说法,请求是如何进入到过滤器链中的呢?这里我们以tomcat容器为例来讲解。

当tomcat启动之后,Connector就会在指定的端口监听客户端的请求。一旦客户端发来请求,Connector就会接收请求并将其解析成Request、Response对象。

接下来,Connector会将Request、Response对象交给CoyoteAdapter。

package org.apache.catalina.connector;
/**
 * Implementation of a request processor which delegates the processing to a
 * Coyote processor.
 *
 * @author Craig R. McClanahan
 * @author Remy Maucherat
 */
public class CoyoteAdapter implements Adapter {


    @Override
    public void service(org.apache.coyote.Request req, org.apache.coyote.Response res)
            throws Exception {


        Request request = (Request) req.getNote(ADAPTER_NOTES);
        Response response = (Response) res.getNote(ADAPTER_NOTES);


        if (request == null) {
            // Create objects
            request = connector.createRequest();
            request.setCoyoteRequest(req);
            response = connector.createResponse();
            response.setCoyoteResponse(res);


            // Link objects
            request.setResponse(response);
            response.setRequest(request);


            // Set as notes
            req.setNote(ADAPTER_NOTES, request);
            res.setNote(ADAPTER_NOTES, response);


            // Set query string encoding
            req.getParameters().setQueryStringCharset(connector.getURICharset());
        }


        if (connector.getXpoweredBy()) {
            response.addHeader("X-Powered-By", POWERED_BY);
        }


        boolean async = false;
        boolean postParseSuccess = false;


        req.getRequestProcessor().setWorkerThreadName(THREAD_NAME.get());


        try {
            // Parse and set Catalina and configuration specific
            // request parameters
            postParseSuccess = postParseRequest(req, request, res, response);
            if (postParseSuccess) {
                //check valves if we support async
                request.setAsyncSupported(
                        connector.getService().getContainer().getPipeline().isAsyncSupported());
                // 调用容器
                connector.getService().getContainer().getPipeline().getFirst()
                .invoke(request, response);
            }
            if (request.isAsync()) {
                async = true;
                ReadListener readListener = req.getReadListener();
                if (readListener != null && request.isFinished()) {
                    // Possible the all data may have been read during service()
                    // method so this needs to be checked here
                    ClassLoader oldCL = null;
                    try {
                        oldCL = request.getContext().bind(false, null);
                        if (req.sendAllDataReadEvent()) {
                            req.getReadListener().onAllDataRead();
                        }
                    } finally {
                        request.getContext().unbind(false, oldCL);
                    }
                }


                Throwable throwable =
                        (Throwable) request.getAttribute(RequestDispatcher.ERROR_EXCEPTION);


                // If an async request was started, is not going to end once
                // this container thread finishes and an error occurred, trigger
                // the async error process
                if (!request.isAsyncCompleting() && throwable != null) {
                    request.getAsyncContextInternal().setErrorState(throwable, true);
                }
            } else {
                request.finishRequest();
                response.finishResponse();
            }


        } catch (IOException e) {
            // Ignore
        } finally {
            AtomicBoolean error = new AtomicBoolean(false);
            res.action(ActionCode.IS_ERROR, error);


            if (request.isAsyncCompleting() && error.get()) {
                // Connection will be forcibly closed which will prevent
                // completion happening at the usual point. Need to trigger
                // call to onComplete() here.
                res.action(ActionCode.ASYNC_POST_PROCESS,  null);
                async = false;
            }


            // Access log
            if (!async && postParseSuccess) {
                // Log only if processing was invoked.
                // If postParseRequest() failed, it has already logged it.
                Context context = request.getContext();
                Host host = request.getHost();
                // If the context is null, it is likely that the endpoint was
                // shutdown, this connection closed and the request recycled in
                // a different thread. That thread will have updated the access
                // log so it is OK not to update the access log here in that
                // case.
                // The other possibility is that an error occurred early in
                // processing and the request could not be mapped to a Context.
                // Log via the host or engine in that case.
                long time = System.currentTimeMillis() - req.getStartTime();
                if (context != null) {
                    context.logAccess(request, response, time, false);
                } else if (response.isError()) {
                    if (host != null) {
                        host.logAccess(request, response, time, false);
                    } else {
                        connector.getService().getContainer().logAccess(
                                request, response, time, false);
                    }
                }
            }


            req.getRequestProcessor().setWorkerThreadName(null);


            // Recycle the wrapper request and response
            if (!async) {
                updateWrapperErrorCount(request, response);
                request.recycle();
                response.recycle();
            }
        }
    }
}           

在service()方法中,CoyoteAdapter会绑定相应的容器(上述代码第54行),然后从engine逐层调用至valve。到执行到最后一个StandardWrapperValve时,该valve会创建一个FilterChain。

ApplicationFilterChain filterChain =
                ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);           

如果servlet对象和filterChain对象都不为null,那么StandardWrapperValve就会调用FilterChain的doFilter方法,从而请求request就会进入Servlet的过滤器链中。

filterChain.doFilter(request.getRequest(),
                                  response.getResponse());           

FileterChain是Servlet的一个接口。该接口只有一个doFilter()方法,用以触发调用下一个过滤器。或者,当当前调用的过滤器是最后一个过滤器时,用以触发调用过滤器链的尾部资源。所谓尾部资源,一般来说就是Servlet实例。

package javax.servlet;


import java.io.IOException;


/**
 * A FilterChain is an object provided by the servlet container to the developer
 * giving a view into the invocation chain of a filtered request for a resource.
 * Filters use the FilterChain to invoke the next filter in the chain, or if the
 * calling filter is the last filter in the chain, to invoke the resource at the
 * end of the chain.
 *
 * @see Filter
 * @since Servlet 2.3
 **/


public interface FilterChain {


    /**
     * Causes the next filter in the chain to be invoked, or if the calling
     * filter is the last filter in the chain, causes the resource at the end of
     * the chain to be invoked.
     *
     * @param request
     *            the request to pass along the chain.
     * @param response
     *            the response to pass along the chain.
     *
     * @throws IOException if an I/O error occurs during the processing of the
     *                     request
     * @throws ServletException if the processing fails for any other reason


     * @since 2.3
     */
    public void doFilter(ServletRequest request, ServletResponse response)
            throws IOException, ServletException;


}           

tomcat容器对FilterChain提供了默认实现ApplicationFilterChain。由此可见,FilterChain是由tomcat进行创建和管理的(前面讲了Serlvlet应用程序收到请求后,Servlet容器会创建一个FilterChain,tomcat就是Servlet容器)。

package org.apache.catalina.core;


import java.io.IOException;
import java.security.Principal;
import java.security.PrivilegedActionException;
import java.util.Set;


import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.Servlet;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;


import org.apache.catalina.Globals;
import org.apache.catalina.security.SecurityUtil;
import org.apache.tomcat.util.ExceptionUtils;
import org.apache.tomcat.util.res.StringManager;


/**
 * Implementation of <code>javax.servlet.FilterChain</code> used to manage
 * the execution of a set of filters for a particular request.  When the
 * set of defined filters has all been executed, the next call to
 * <code>doFilter()</code> will execute the servlet's <code>service()</code>
 * method itself.
 *
 * @author Craig R. McClanahan
 */
public final class ApplicationFilterChain implements FilterChain {


    // Used to enforce requirements of SRV.8.2 / SRV.14.2.5.1
    private static final ThreadLocal<ServletRequest> lastServicedRequest;
    private static final ThreadLocal<ServletResponse> lastServicedResponse;


    static {
        if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
            lastServicedRequest = new ThreadLocal<>();
            lastServicedResponse = new ThreadLocal<>();
        } else {
            lastServicedRequest = null;
            lastServicedResponse = null;
        }
    }


    // -------------------------------------------------------------- Constants


    public static final int INCREMENT = 10;
    // ----------------------------------------------------- Instance Variables
    /**
     * 过滤器链中所有的过滤器
     */
    private ApplicationFilterConfig[] filters = new ApplicationFilterConfig[0];
    
    /**
     * 用以指示过滤器链的当前的位置,也就是当前被调用的过滤器的位置
     */
    private int pos = 0;
    /**
     * 当前过滤器链中过滤器的数量
     */
    private int n = 0;


    private Servlet servlet = null;


    private boolean servletSupportsAsync = false;


    private static final StringManager sm =
      StringManager.getManager(Constants.Package);


    /**
     * Static class array used when the SecurityManager is turned on and
     * <code>doFilter</code> is invoked.
     */
    private static final Class<?>[] classType = new Class[]{
        ServletRequest.class, ServletResponse.class, FilterChain.class};


    /**
     * Static class array used when the SecurityManager is turned on and
     * <code>service</code> is invoked.
     */
    private static final Class<?>[] classTypeUsedInService = new Class[]{
        ServletRequest.class, ServletResponse.class};
}

           

ApplicationFilterChain类有两个重要的属性:pos和ApplicationFilterConfig数组。

pos用以指示过滤器链的当前的位置,也就是当前被调用的过滤器的位置。

ApplicationFilterConfig数组存储过滤器链中所有的过滤器。

再看doFilter方法。

@Override
public void doFilter(ServletRequest request, ServletResponse response)
        throws IOException, ServletException {
        //检查是否开启了安全功能
        if( Globals.IS_SECURITY_ENABLED ) {
            final ServletRequest req = request;
            final ServletResponse res = response;
            try {
                java.security.AccessController.doPrivileged(
                    new java.security.PrivilegedExceptionAction<Void>() {
                        @Override
                        public Void run()
                            throws ServletException, IOException {
                            //方法调用
                            internalDoFilter(req,res);
                            return null;
                        }
                    }
                );
            } catch( PrivilegedActionException pe) {
                Exception e = pe.getException();
                if (e instanceof ServletException)
                    throw (ServletException) e;
                else if (e instanceof IOException)
                    throw (IOException) e;
                else if (e instanceof RuntimeException)
                    throw (RuntimeException) e;
                else
                    throw new ServletException(e.getMessage(), e);
            }
        } else {
            //方法调用
            internalDoFilter(request,response);
        }
}           

调用doFilter方法时,ServletRequest和ServletResponse对象会作为传入。首先doFilter方法会校验当前应用程序是否开启了安全功能,如果开启了安全功能,就会进行权限验证,验证完之后会调用internalDoFilter方法。如果没开启安全功能,则直接调用internalDoFilter方法。

private void internalDoFilter(ServletRequest request,
                                  ServletResponse response)
        throws IOException, ServletException {


        // 如果不是调用的不是最后一个过滤器,则继续调用下一个过滤器
        if (pos < n) {
            ApplicationFilterConfig filterConfig = filters[pos++];
            try {
                Filter filter = filterConfig.getFilter();


                if (request.isAsyncSupported() && "false".equalsIgnoreCase(
                        filterConfig.getFilterDef().getAsyncSupported())) {
                    request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR, Boolean.FALSE);
                }
                if( Globals.IS_SECURITY_ENABLED ) {
                    final ServletRequest req = request;
                    final ServletResponse res = response;
                    Principal principal =
                        ((HttpServletRequest) req).getUserPrincipal();


                    Object[] args = new Object[]{req, res, this};
                    SecurityUtil.doAsPrivilege ("doFilter", filter, classType, args, principal);
                } else {
                    filter.doFilter(request, response, this);
                }
            } catch (IOException | ServletException | RuntimeException e) {
                throw e;
            } catch (Throwable e) {
                e = ExceptionUtils.unwrapInvocationTargetException(e);
                ExceptionUtils.handleThrowable(e);
                throw new ServletException(sm.getString("filterChain.filter"), e);
            }
            return;
        }


        // 调用servlet实例
        try {
            if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
                lastServicedRequest.set(request);
                lastServicedResponse.set(response);
            }


            if (request.isAsyncSupported() && !servletSupportsAsync) {
                request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR,
                        Boolean.FALSE);
            }
            // Use potentially wrapped request from this point
            if ((request instanceof HttpServletRequest) &&
                    (response instanceof HttpServletResponse) &&
                    Globals.IS_SECURITY_ENABLED ) {
                final ServletRequest req = request;
                final ServletResponse res = response;
                Principal principal =
                    ((HttpServletRequest) req).getUserPrincipal();
                Object[] args = new Object[]{req, res};
                SecurityUtil.doAsPrivilege("service",
                                           servlet,
                                           classTypeUsedInService,
                                           args,
                                           principal);
            } else {
                servlet.service(request, response);
            }
        } catch (IOException | ServletException | RuntimeException e) {
            throw e;
        } catch (Throwable e) {
            e = ExceptionUtils.unwrapInvocationTargetException(e);
            ExceptionUtils.handleThrowable(e);
            throw new ServletException(sm.getString("filterChain.servlet"), e);
        } finally {
            if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
                lastServicedRequest.set(null);
                lastServicedResponse.set(null);
            }
        }
}           

在internalDoFilter方法中,如果当前被调用的过滤器的位置pos值小于过滤器链中过滤器的数量,那么程序就会调用过滤器的doFilter方法。

filter.doFilter(request, response, this);           

与此同时,当前请求的过滤器链对象(也就是this)也会作为参数传入到当前被调用的过滤器中。

以HttpFiler中的doFilter方法为例(其他过滤器的doFilter方法也差不多)。

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        if (!(request instanceof HttpServletRequest)) {
            throw new ServletException(request + " not HttpServletRequest");
        }
        if (!(response instanceof HttpServletResponse)) {
            throw new ServletException(request + " not HttpServletResponse");
        }
        doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
}


protected void doFilter(HttpServletRequest request, HttpServletResponse response,
            FilterChain chain) throws IOException, ServletException {
    //调用过滤器链的doFilter方法,调用下一个过滤器
    chain.doFilter(request, response);
}           

进入过滤器中的doFilter方法,在做完每个过滤器的个性化业务逻辑处理之后,就会调用过滤器链的doFilter方法来调用下一个过滤器。同时,当前过滤器的调用结束。

如此循环,直到过滤器链中的最后一个过滤器。

当最后一个过滤器被调用结束之后,过滤器链就会调用servlet的实例。

servlet.service(request, response);           

至此,Servlet处理完业务最后将结果返回给Connector,再返回给客户端。