天天看點

想了解 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,再傳回給用戶端。