作者|知視
本文主要介紹Spring Security的實作基礎,Servlet過濾器鍊的知識
如果覺得一件事做起來很難,那是你不了解它。
有人說Spring Security很難,其實那是因為你不了解它的原理。今天我們就來說說Spring Security的原理。
Spring Security提供的支援很多,但是作為Web應用開發人員來說,我們隻需要了解其中關于Servlet的知識就夠了。
進入正題。
我想,在講Spring Security原理之前,很有必要先說一下什麼是過濾器。生活中,我們會用過濾器将渾濁的水變成清澈幹淨的純淨水。
在Web應用中,Servlet提供的java.servlet.Filter接口同樣有類似的作用。過濾器按照某些規則,将請求攔截下來并做某些處理之後,再将請求交給Servlet。
Spring Security對Servlet的支援就是基于一條關于Servlet的過濾器鍊。
如圖,展示了在所有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,再傳回給用戶端。