作者|知视
本文主要介绍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,再返回给客户端。