Struts2 运行流程分析之doFilter


注意: 本文基于Struts2.2.1版本进行的代码分析

首先先大致看一下StrutsPrepareAndExecuteFilter.java的doFilter的实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
 public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {

        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;

        try {

        	// 设置request/response编码
            prepare.setEncodingAndLocale(request, response);

            // 设置Action上下文
            prepare.createActionContext(request, response);

            // 将当前Dispatcher加入线程管理,用于解决线程安全问题
            prepare.assignDispatcherToThread();


            // 排除拦截路径,具体在<constant name="struts.action.excludePattern" value=".*validcode.*,.*tohtml.*"/>中设置
			if ( excludedPatterns != null && prepare.isUrlExcluded(request, excludedPatterns)) {
				chain.doFilter(request, response);
			} else {

				// 对request进行封装
				request = prepare.wrapRequest(request);

				// 根据Request的url获取映射的Action
				ActionMapping mapping = prepare.findActionMapping(request, response, true);

				// 找不到映射就认为是静态文件,继续下一个filter
				if (mapping == null) {

					boolean handled = execute.executeStaticResourceRequest(request, response);
					if (!handled) {
						chain.doFilter(request, response);
					}
				} else {

					// 执行action
					execute.executeAction(request, response, mapping);
				}
			}
        } finally {
            prepare.cleanupRequest(request);
        }
    }

上面对dofilter进行了简单分析,下面我们进行详细分析,先整理出几个点:

  1. 设置request/response编码
  2. 设置Action上下文
  3. Dispatcher加入线程管理
  4. 排除除拦截路径
  5. 对request进行封装
  6. 根据request获取action映射
  7. 执行Action

0x01 设置request/response编码

首先看一下 prepare.setEncodingAndLocale(request, response); 的代码实现:

1
2
3
    public void setEncodingAndLocale(HttpServletRequest request, HttpServletResponse response) {
        dispatcher.prepare(request, response);
    }

其实还是调用dispatcher对象, 我们继续看prepare的实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public void prepare(HttpServletRequest request, HttpServletResponse response) {


        String encoding = null;

        // 搜索代码defaultEncoding并没有被初始化设置
        if (defaultEncoding != null) {
            encoding = defaultEncoding;
        }


        Locale locale = null;

      
        if (defaultLocale != null) {
            locale = LocalizedTextUtil.localeFromString(defaultLocale, request.getLocale());
        }

        if (encoding != null) {
            try {

            	// 设置编码
                request.setCharacterEncoding(encoding);
            } catch (Exception e) {
                LOG.error("Error setting character encoding to '" + encoding + "' - ignoring.", e);
            }
        }

        
        // 设置response编码
        if (locale != null) {
            response.setLocale(locale);
        }


        // 该变量是根据struts.dispatcher.parametersWorkaround进行设置
        // 对于某些Java EE服务器,不支持HttpServletRequest调用getParameterMap()方法,此时可以设置该属性值为true来解决该问题。
        // 该属性的默认值是false。对于WebLogic、Orion和OC4J服务器,通常应该设置该属性为true。
        if (paramsWorkaroundEnabled) {
            request.getParameter("foo"); // simply read any parameter (existing or not) to "prime" the request
        }
}

0x02 设置Action上下文

prepare.createActionContext(request, response); 代码实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
    /**
     *
     * 创建action的context和初始化本地线程
     */
    public ActionContext createActionContext(HttpServletRequest request, HttpServletResponse response) {
        ActionContext ctx;

        // 这个计数器是来干什么的???
        Integer counter = 1;

        // 计数器累加
        Integer oldCounter = (Integer) request.getAttribute(CLEANUP_RECURSION_COUNTER);
        if (oldCounter != null) {
            counter = oldCounter + 1;
        }
        


        ActionContext oldContext = ActionContext.getContext();
        if (oldContext != null) {
            // detected existing context, so we are probably in a forward
            ctx = new ActionContext(new HashMap<String, Object>(oldContext.getContextMap()));
        } else {

            // 创建ValueStack对象,关于该对象之前我们分析过, 
            // ValueStack是XWork对OGNL的计算进行扩展的一个特殊的数据结构, 主要是对OGNL三要素中的Root对象进行扩展.
            ValueStack stack = dispatcher.getContainer().getInstance(ValueStackFactory.class).createValueStack();

            // 首先看到的createContextMap是将Request对象转换成map上下文对象对象,具体我们下方分析.
            // 并将这个context放进stack内,也就是放到root对象内
            stack.getContext().putAll(dispatcher.createContextMap(request, response, null, servletContext));

            //  初始化个Action上下文对象
            ctx = new ActionContext(stack.getContext());
        }


        // 存放计数器        
        request.setAttribute(CLEANUP_RECURSION_COUNTER, counter);

        // 设置当前线程的操作上下文
        ActionContext.setContext(ctx);
        return ctx;
    }

上面我们说到了createContextMap,我们看一下代码实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
/**
 * Create a context map containing all the wrapped request objects 
 *
 * @param request The servlet request
 * @param response The servlet response
 * @param mapping The action mapping
 * @param context The servlet context
 * @return A map of context objects
 */
public Map<String,Object> createContextMap(HttpServletRequest request, HttpServletResponse response,
    ActionMapping mapping, ServletContext context) {

    // 下方注释写的非常清晰,也就是对reqeust对象进行map封装(包括request的参数、session、等等)最后创建个上下文。


    // request map wrapping the http request objects
    Map requestMap = new RequestMap(request);


    // 下面注释写的都非常清晰,没有什么特殊关注点

    // parameters map wrapping the http parameters.  ActionMapping parameters are now handled and applied separately
    Map params = new HashMap(request.getParameterMap());

    // session map wrapping the http session
    Map session = new SessionMap(request);

    // application map wrapping the ServletContext
    Map application = new ApplicationMap(context);

    Map<String,Object> extraContext = createContextMap(requestMap, params, session, application, request, response, context);

    if (mapping != null) {
        extraContext.put(ServletActionContext.ACTION_MAPPING, mapping);
    }
    return extraContext;
}

/**
 * Merge all application and servlet attributes into a single <tt>HashMap</tt> to represent the entire
 * <tt>Action</tt> context.
 *
 * @param requestMap     a Map of all request attributes.
 * @param parameterMap   a Map of all request parameters.
 * @param sessionMap     a Map of all session attributes.
 * @param applicationMap a Map of all servlet context attributes.
 * @param request        the HttpServletRequest object.
 * @param response       the HttpServletResponse object.
 * @param servletContext the ServletContextmapping object.
 * @return a HashMap representing the <tt>Action</tt> context.
 */
public HashMap<String,Object> createContextMap(Map requestMap,
                                Map parameterMap,
                                Map sessionMap,
                                Map applicationMap,
                                HttpServletRequest request,
                                HttpServletResponse response,
                                ServletContext servletContext) {
    HashMap<String,Object> extraContext = new HashMap<String,Object>();
    extraContext.put(ActionContext.PARAMETERS, new HashMap(parameterMap));
    extraContext.put(ActionContext.SESSION, sessionMap);
    extraContext.put(ActionContext.APPLICATION, applicationMap);

    Locale locale;
    if (defaultLocale != null) {
        locale = LocalizedTextUtil.localeFromString(defaultLocale, request.getLocale());
    } else {
        locale = request.getLocale();
    }

    extraContext.put(ActionContext.LOCALE, locale);
    //extraContext.put(ActionContext.DEV_MODE, Boolean.valueOf(devMode));

    extraContext.put(StrutsStatics.HTTP_REQUEST, request);
    extraContext.put(StrutsStatics.HTTP_RESPONSE, response);
    extraContext.put(StrutsStatics.SERVLET_CONTEXT, servletContext);

    // helpers to get access to request/session/application scope
    extraContext.put("request", requestMap);
    extraContext.put("session", sessionMap);
    extraContext.put("application", applicationMap);
    extraContext.put("parameters", parameterMap);

    AttributeMap attrMap = new AttributeMap(extraContext);
    extraContext.put("attr", attrMap);

    return extraContext;
}

代码很直观,我们在Struts2的模板里用到的session对象就是在这里初始化的,例如

1
<s:property value="#session.user.name">

0x03 Dispatcher加入线程管理

prepare.assignDispatcherToThread(); 代码实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
 * 将调度程序分配给调度程序线程本地
 */
public void assignDispatcherToThread() {
    Dispatcher.setInstance(dispatcher);
}





// Dispatcher.java

/**
 * Provide a thread local instance.
 */
private static ThreadLocal<Dispatcher> instance = new ThreadLocal<Dispatcher>();

/**
 * Store the dispatcher instance for this thread.
 *
 * @param instance The instance
 */
public static void setInstance(Dispatcher instance) {
    Dispatcher.instance.set(instance);
}

0x04 排除除拦截路径

prepare.isUrlExcluded(request, excludedPatterns) 实现代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
    /**
     * Check whether the request matches a list of exclude patterns.
     *
     * @param request          The request to check patterns against
     * @param excludedPatterns list of patterns for exclusion
     *
     * @return <tt>true</tt> if the request URI matches one of the given patterns
     */
    public boolean isUrlExcluded( HttpServletRequest request, List<Pattern> excludedPatterns ) {
        if (excludedPatterns != null) {
            String uri = getUri(request);
            for ( Pattern pattern : excludedPatterns ) {
                if (pattern.matcher(uri).matches()) {
                    return true;
                }
            }
        }
        return false;
    }

也就是个正则match, 我们看一下excludedPatterns是在哪里初始化的

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class StrutsPrepareAndExecuteFilter implements StrutsStatics, Filter {
    // protected PrepareOperations prepare;
    // protected ExecuteOperations execute;
	protected List<Pattern> excludedPatterns = null;

    public void init(FilterConfig filterConfig) throws ServletException {
        InitOperations init = new InitOperations();
        try {
           // FilterHostConfig config = new FilterHostConfig(filterConfig);
           //  init.initLogging(config);
           //  Dispatcher dispatcher = init.initDispatcher(config);
           //  init.initStaticContentLoader(config, dispatcher);

          //   prepare = new PrepareOperations(filterConfig.getServletContext(), dispatcher);
          //   execute = new ExecuteOperations(filterConfig.getServletContext(), dispatcher);
	this.excludedPatterns = init.buildExcludedPatternsList(dispatcher);

            // postInit(dispatcher, filterConfig);
        } finally {
           //  init.cleanup();
        }

    }

init.buildExcludedPatternsList(dispatcher);的实现代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
 * Extract a list of patterns to exclude from request filtering
 *
 * @param dispatcher The dispatcher to check for exclude pattern configuration
 *
 * @return a List of Patterns for request to exclude if apply, or <tt>null</tt>
 *
 * @see org.apache.struts2.StrutsConstants#STRUTS_ACTION_EXCLUDE_PATTERN
 */
public List<Pattern> buildExcludedPatternsList( Dispatcher dispatcher ) {

	// 设置排除拦截路径
    return buildExcludedPatternsList(dispatcher.getContainer().getInstance(String.class, StrutsConstants.STRUTS_ACTION_EXCLUDE_PATTERN));
}

private List<Pattern> buildExcludedPatternsList( String patterns ) {
    if (null != patterns && patterns.trim().length() != 0) {
        List<Pattern> list = new ArrayList<Pattern>();
        String[] tokens = patterns.split(",");
        for ( String token : tokens ) {
            list.add(Pattern.compile(token.trim()));
        }
        return Collections.unmodifiableList(list);
    } else {
        return null;
    }
}

看到是通过StrutsConstants.STRUTS_ACTION_EXCLUDE_PATTERN也就是struts.action.excludePattern配置的,如:

1
<constant name="struts.action.excludePattern" value="/res/.*,/css/.*,/images/.*,/js/.*,/services/.*" />  

0x05 对request进行封装

request = prepare.wrapRequest(request); 代码实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
    /**
     * Wraps the request with the Struts wrapper that handles multipart requests better
     * @return The new request, if there is one
     * @throws ServletException
     */
    public HttpServletRequest wrapRequest(HttpServletRequest oldRequest) throws ServletException {
        HttpServletRequest request = oldRequest;
        try {
            // Wrap request first, just in case it is multipart/form-data
            // parameters might not be accessible through before encoding (ww-1278)
            request = dispatcher.wrapRequest(request, servletContext);
        } catch (IOException e) {
            String message = "Could not wrap servlet request with MultipartRequestWrapper!";
            throw new ServletException(message, e);
        }
        return request;
    }

看一下 request = dispatcher.wrapRequest(request, servletContext); 的实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
/**
 * Wrap and return the given request or return the original request object.
 * </p>
 * This method transparently handles multipart data as a wrapped class around the given request.
 * Override this method to handle multipart requests in a special way or to handle other types of requests.
 * Note, {@link org.apache.struts2.dispatcher.multipart.MultiPartRequestWrapper} is
 * flexible - look first to that object before overriding this method to handle multipart data.
 *
 * @param request the HttpServletRequest object.
 * @param servletContext Our ServletContext object
 * @return a wrapped request or original request.
 * @see org.apache.struts2.dispatcher.multipart.MultiPartRequestWrapper
 * @throws java.io.IOException on any error.
 */
public HttpServletRequest wrapRequest(HttpServletRequest request, ServletContext servletContext) throws IOException {
    // don't wrap more than once
    if (request instanceof StrutsRequestWrapper) {
        return request;
    }

    String content_type = request.getContentType();



    // 如果request的header中content-type的value包含multipart/form-data
    // 封装成MultiPartRequestWrapper对象
    // 这部分代码太经典,s2-045安全漏洞就是从这里开始的
    if (content_type != null && content_type.indexOf("multipart/form-data") != -1) {

        MultiPartRequest mpr = null;


        //check for alternate implementations of MultiPartRequest
        Set<String> multiNames = getContainer().getInstanceNames(MultiPartRequest.class);
        if (multiNames != null) {
            for (String multiName : multiNames) {
                if (multiName.equals(multipartHandlerName)) {
                    mpr = getContainer().getInstance(MultiPartRequest.class, multiName);
                }
            }
        }
        if (mpr == null ) {
            mpr = getContainer().getInstance(MultiPartRequest.class);
        }
        request = new MultiPartRequestWrapper(mpr, request, getSaveDir(servletContext));
    } else {

    	// 封装成StrutsRequestWrapper
        request = new StrutsRequestWrapper(request);
    }

    return request;
}

上面代码我们拆成两个部分进行分析:

1. 封装成MultiPartRequestWrapper

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
if (content_type != null && content_type.indexOf("multipart/form-data") != -1) {

    MultiPartRequest mpr = null;

     
    //check for alternate implementations of MultiPartRequest
    // 获取Struts-default.xml里的配置,也就是
    // <bean type="org.apache.struts2.dispatcher.multipart.MultiPartRequest" name="struts" class="org.apache.struts2.dispatcher.multipart.JakartaMultiPartRequest" scope="default"/>
    // <bean type="org.apache.struts2.dispatcher.multipart.MultiPartRequest" name="jakarta" class="org.apache.struts2.dispatcher.multipart.JakartaMultiPartRequest" scope="default" />
    Set<String> multiNames = getContainer().getInstanceNames(MultiPartRequest.class);
    if (multiNames != null) {
        for (String multiName : multiNames) {

        	// 根据multipartHandlerName配置的multipart Name决定采用那个MultiPartRequest类的实现类
        	// 通过阅读代码,我们知道multipartHandlerName是由  <constant name="struts.multipart.handler" value="jakarta" />
        	// 也就是默认是jakarta,实现类是org.apache.struts2.dispatcher.multipart.JakartaMultiPartRequest, 再次怀念一下s2-045神洞
            if (multiName.equals(multipartHandlerName)) {

            	// 获取org.apache.struts2.dispatcher.multipart.JakartaMultiPartRequest类的实例化
                mpr = getContainer().getInstance(MultiPartRequest.class, multiName);
            }
        }
    }
    if (mpr == null ) {
        mpr = getContainer().getInstance(MultiPartRequest.class);
    }


    // 这里的第三个参数是文件保存目录
    // 下方我们看一下MultiPartRequestWrapper的大致实现
    request = new MultiPartRequestWrapper(mpr, request, getSaveDir(servletContext));
} else {

	....
}




// MultiPartRequestWrapper.java 实现

public class MultiPartRequestWrapper extends StrutsRequestWrapper {
    protected static final Logger LOG = LoggerFactory.getLogger(MultiPartRequestWrapper.class);

    Collection<String> errors;
    MultiPartRequest multi;

    /**
     * Process file downloads and log any errors.
     *
     * @param request Our HttpServletRequest object
     * @param saveDir Target directory for any files that we save
     * @param multiPartRequest Our MultiPartRequest object
     */
    public MultiPartRequestWrapper(MultiPartRequest multiPartRequest, HttpServletRequest request, String saveDir) {
        super(request);
        
        multi = multiPartRequest;
        try {

        	// 用.JakartaMultiPartRequest类的parse方法解析reqeust对象,具体代码就不看了,也就是调用commons-fileupload组件进行上传文件
            multi.parse(request, saveDir);
            for (Object o : multi.getErrors()) {
                String error = (String) o;
                addError(error);
            }
        } catch (IOException e) {
            addError("Cannot parse request: "+e.toString());
        } 
    }

Content-Type为multipart/form-data的情况,我们已经分析完了,我们看一下非上传请求的正常http请求是怎么处理的吧.

2. 封装成StrutsRequestWrapper

1
2
3
4
5
6
7
8
9
if (content_type != null && content_type.indexOf("multipart/form-data") != -1) {

    // .... 省略
  
} else {

	// 封装成StrutsRequestWrapper
    request = new StrutsRequestWrapper(request);
}

看一下StrutsRequestWrapper的实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
public class StrutsRequestWrapper extends HttpServletRequestWrapper {

    /**
     * The constructor
     * @param req The request
     */
    public StrutsRequestWrapper(HttpServletRequest req) {
        super(req);
    }

    /**
     * Gets the object, looking in the value stack if not found
     *
     * @param s The attribute key
     */
    public Object getAttribute(String s) {
        if (s != null && s.startsWith("javax.servlet")) {
            // don't bother with the standard javax.servlet attributes, we can short-circuit this
            // see WW-953 and the forums post linked in that issue for more info
            return super.getAttribute(s);
        }
      
        ActionContext ctx = ActionContext.getContext();
        Object attribute = super.getAttribute(s);
        if (ctx != null) {

        	// 如果原生的getAttribute获取不到数据,则从ValueStack中获取
            if (attribute == null) {
                boolean alreadyIn = false;
                Boolean b = (Boolean) ctx.get("__requestWrapper.getAttribute");
                if (b != null) {
                    alreadyIn = b.booleanValue();
                }
    
                // note: we don't let # come through or else a request for
                // #attr.foo or #request.foo could cause an endless loop
                if (!alreadyIn && s.indexOf("#") == -1) {
                    try {
                        // If not found, then try the ValueStack
                        ctx.put("__requestWrapper.getAttribute", Boolean.TRUE);
                        ValueStack stack = ctx.getValueStack();
                        if (stack != null) {
                            attribute = stack.findValue(s);
                        }
                    } finally {
                        ctx.put("__requestWrapper.getAttribute", Boolean.FALSE);
                    }
                }
            }
        }
        return attribute;
    }
}

也就是对servlet的HttpServletRequestWrapper对象的实现,目的就是为了实现了getAttribute方法, 如果原生的getAttribute获取不到数据,则从ValueStack中获取。

0x06 根据request获取action映射

这种分析思路并不完美,容易让人阅读起来,思路中断,建议多看几遍

ActionMapping mapping = prepare.findActionMapping(request, response, true); 具体实现代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/**
 *   Finds and optionally creates an {@link ActionMapping}.  It first looks in the current request to see if one
 * has already been found, otherwise, it creates it and stores it in the request.  No mapping will be created in the
 * case of static resource requests or unidentifiable requests for other servlets, for example.
 */
public ActionMapping findActionMapping(HttpServletRequest request, HttpServletResponse response) {
    return findActionMapping(request, response, false);
}


private static final String STRUTS_ACTION_MAPPING_KEY = "struts.actionMapping";


/**
 * Finds and optionally creates an {@link ActionMapping}.  if forceLookup is false, it first looks in the current request to see if one
 * has already been found, otherwise, it creates it and stores it in the request.  No mapping will be created in the
 * case of static resource requests or unidentifiable requests for other servlets, for example.
 * @param forceLookup if true, the action mapping will be looked up from the ActionMapper instance, ignoring if there is one
 * in the request or not 
 */
public ActionMapping findActionMapping(HttpServletRequest request, HttpServletResponse response, boolean forceLookup) {

    ActionMapping mapping = (ActionMapping) request.getAttribute(STRUTS_ACTION_MAPPING_KEY);
    if (mapping == null || forceLookup) {
        try {

        	// 根据reqeust获取action映射,存放到Attribute,目的是为了性能优化
        	// 这里的ActionMapper.class的具体实现类是org.apache.struts2.dispatcher.mapper.DefaultActionMapper
        	// 下方我们分析一下DefaultActionMapper的getMapping实现
            mapping = dispatcher.getContainer().getInstance(ActionMapper.class).getMapping(request, dispatcher.getConfigurationManager());
            if (mapping != null) {
                request.setAttribute(STRUTS_ACTION_MAPPING_KEY, mapping);
            }
        } catch (Exception ex) {
            dispatcher.sendError(request, response, servletContext, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, ex);
        }
    }

    return mapping;
}

分析一下DefaultActionMapper的getMapping实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/*
 * (non-Javadoc)
 *
 * @see org.apache.struts2.dispatcher.mapper.ActionMapper#getMapping(javax.servlet.http.HttpServletRequest)
 */

public ActionMapping getMapping(HttpServletRequest request,
                                ConfigurationManager configManager) {


    ActionMapping mapping = new ActionMapping();


    // 获取uri
    // 例如我们访问的是http://localhost/index.action,那么uri就是/index.action
    String uri = getUri(request);

    // 这段代码的意思应该是为了处理这样的uri /index;xxxxx.action
    // 具体可以看tomcat的uri处理
    int indexOfSemicolon = uri.indexOf(";");
    uri = (indexOfSemicolon > -1) ? uri.substring(0, indexOfSemicolon) : uri;

    // 删除扩展,如传递的是/index.action,最后保留的是index
    uri = dropExtension(uri, mapping);
    if (uri == null) {
        return null;
    }

    // 根据uri解析name和Namespace,
    parseNameAndNamespace(uri, mapping, configManager);

    // 这个函数实现了Struts2的DynamicMethod功能,具体下方有详细分析
    handleSpecialParameters(request, mapping);

    if (mapping.getName() == null) {
        return null;
    }

    
    // 这里实现了Struts2另外一种动态方法调用功能,具体下方有详细分析
    parseActionName(mapping);

    return mapping;
}

上方代码我们分为2个部分分析

1. parseNameAndNamespace方法分析

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
/**
 * Parses the name and namespace from the uri
 *
 * @param uri     The uri
 * @param mapping The action mapping to populate
 */
protected void parseNameAndNamespace(String uri, ActionMapping mapping,
                                     ConfigurationManager configManager) {
    String namespace, name;
    int lastSlash = uri.lastIndexOf("/");
    if (lastSlash == -1) {
        namespace = "";
        name = uri;
    } else if (lastSlash == 0) {
        // ww-1046, assume it is the root namespace, it will fallback to
        // default
        // namespace anyway if not found in root namespace.
        namespace = "/";
        name = uri.substring(lastSlash + 1);
    } else if (alwaysSelectFullNamespace) {
        // Simply select the namespace as everything before the last slash
        namespace = uri.substring(0, lastSlash);
        name = uri.substring(lastSlash + 1);
    } else {
        // Try to find the namespace in those defined, defaulting to ""

        // 获取配置信息
        Configuration config = configManager.getConfiguration();
        String prefix = uri.substring(0, lastSlash);
        namespace = "";
        boolean rootAvailable = false;
        // Find the longest matching namespace, defaulting to the default
        // 通过配置文件,根据uri获取到的namespace,去查找配置里的namespace
        for (Object cfg : config.getPackageConfigs().values()) {
            String ns = ((PackageConfig) cfg).getNamespace();
            if (ns != null && prefix.startsWith(ns) && (prefix.length() == ns.length() || prefix.charAt(ns.length()) == '/')) {
                if (ns.length() > namespace.length()) {
                    namespace = ns;
                }
            }
            if ("/".equals(ns)) {
                rootAvailable = true;
            }
        }

        // 得到name
        name = uri.substring(namespace.length() + 1);

        // Still none found, use root namespace if found
        if (rootAvailable && "".equals(namespace)) {
            namespace = "/";
        }
    }

    if (!allowSlashesInActionNames && name != null) {
        int pos = name.lastIndexOf('/');
        if (pos > -1 && pos < name.length() - 1) {
            name = name.substring(pos + 1);
        }
    }

    mapping.setNamespace(namespace);
    mapping.setName(name);
}

这里我们不得不贴上我们的应用Struts.xml配置:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE struts PUBLIC
        "-//Apache Software Foundation//DTD Struts Configuration 2.5//EN"
        "http://struts.apache.org/dtds/struts-2.5.dtd">

<struts>

    <constant name="struts.enable.DynamicMethodInvocation" value="true"/>
    <constant name="struts.devMode" value="true" />

    <package name="myPackage" extends="struts-default">

        <default-action-ref name="index" />

        <action name="index" class="com.jd.IndexAction">
            <result>/WEB-INF/jsp/index.jsp</result>
        </action>

    </package>

</struts>

所以当我们访问/index.action的时候,parseNameAndNamespace方法处理过后的name是”index”,而namespace是”/“.

2. handleSpecialParameters方法分析

handleSpecialParameters(request, mapping); 代码实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/**
 * Special parameters, as described in the class-level comment, are searched
 * for and handled.
 * 
 *
 * 搜索和处理特殊参数,如 class-level 注释中所述
 *
 * @param request The request
 * @param mapping The action mapping
 */
public void handleSpecialParameters(HttpServletRequest request,
                                    ActionMapping mapping) {
    // handle special parameter prefixes.
    Set<String> uniqueParameters = new HashSet<String>();
    Map parameterMap = request.getParameterMap();
    for (Iterator iterator = parameterMap.keySet().iterator(); iterator
            .hasNext();) {
        String key = (String) iterator.next();

        // Strip off the image button location info, if found
        // 翻译:剥离图像按钮位置信息(如果找到)
        // 懵逼了,这是什么鬼? 可能是兼容某个struts2的image标签库的吧。。我猜的,总之也不是关键代码,不会造成什么影响
        if (key.endsWith(".x") || key.endsWith(".y")) {
            key = key.substring(0, key.length() - 2);
        }

        // Ensure a parameter doesn't get processed twice
        // 参数去重
        if (!uniqueParameters.contains(key)) {

        	// 这是是个骚操作,我们单独分析
            ParameterAction parameterAction = (ParameterAction) prefixTrie.get(key);
            if (parameterAction != null) {
                parameterAction.execute(key, mapping);
                uniqueParameters.add(key);
                break;
            }
        }
    }
}

上面这个函数中有一段特殊代码,如下:

1
2
3
4
5
6
ParameterAction parameterAction = (ParameterAction) prefixTrie.get(key);
if (parameterAction != null) {
    parameterAction.execute(key, mapping);
    uniqueParameters.add(key);
    break;
}

首先我们看一下这个prefixTrie是什么鬼?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
public class DefaultActionMapper implements ActionMapper {

    protected static final String METHOD_PREFIX = "method:";

    protected static final String ACTION_PREFIX = "action:";

    protected static final String REDIRECT_PREFIX = "redirect:";

    protected static final String REDIRECT_ACTION_PREFIX = "redirectAction:";

    protected boolean allowDynamicMethodCalls = true;

    protected boolean allowSlashesInActionNames = false;

    protected boolean alwaysSelectFullNamespace = false;

    protected PrefixTrie prefixTrie = null;

     public DefaultActionMapper() {


        // 初始化prefixTrie
        // 注意这里的key, 分别为method:、action:、redirect:
        // 以及这些key对应的ParameterAction对象的实现
        // 这里的allowDynamicMethodCalls变量是由配置文件中struts.enable.DynamicMethodInvocation设置,默认开启
        prefixTrie = new PrefixTrie() {
            {
                put(METHOD_PREFIX, new ParameterAction() {
                    public void execute(String key, ActionMapping mapping) {

                    	// 设置Action映射的方法为method:后的value
                    	// 访问http://localhost/index.action?method:add,则
                    	// 调用indexAction的add方法
                        if (allowDynamicMethodCalls) {
                            mapping.setMethod(key.substring(
                                    METHOD_PREFIX.length()));
                        }
                    }
                });

                put(ACTION_PREFIX, new ParameterAction() {
                    public void execute(String key, ActionMapping mapping) {

                    	// 这里和method有所不同
                    	// 访问http://localhost/index.action?action:user!add , 则
                    	// 调用userAction的add方法
                        String name = key.substring(ACTION_PREFIX.length());
                        if (allowDynamicMethodCalls) {
                            int bang = name.indexOf('!');
                            if (bang != -1) {
                                String method = name.substring(bang + 1);
                                mapping.setMethod(method);
                                name = name.substring(0, bang);
                            }
                        }
                        mapping.setName(name);
                    }
                });

                put(REDIRECT_PREFIX, new ParameterAction() {

                    // 这里不是方法的调用
                    // 访问 http://localhost/index.action?redirect:user , 则
                    // 重定向到user.jsp
                    public void execute(String key, ActionMapping mapping) {
                        ServletRedirectResult redirect = new ServletRedirectResult();
                        container.inject(redirect);
                        redirect.setLocation(key.substring(REDIRECT_PREFIX
                                .length()));
                        mapping.setResult(redirect);
                    }
                });

                put(REDIRECT_ACTION_PREFIX, new ParameterAction() {

                    // 这里的redirectAction: 和 redirect: 的区别是带不带后缀
                    public void execute(String key, ActionMapping mapping) {
                        String location = key.substring(REDIRECT_ACTION_PREFIX
                                .length());
                        ServletRedirectResult redirect = new ServletRedirectResult();
                        container.inject(redirect);
                        String extension = getDefaultExtension();
                        if (extension != null && extension.length() > 0) {
                            location += "." + extension;
                        }
                        redirect.setLocation(location);
                        mapping.setResult(redirect);
                    }
                });
            }
        };
    }

额…这是Struts2的奇葩设计,”动态方法调用“功能,通过struts.enable.DynamicMethodInvocation设置为true大概该功能, 这里简单介绍一下,具体功能使用google一下:

也就是当我们访问http://localhost/index.action?method:foo的时候,就会调用index这个Action中的foo方法, 具体阅读上方代码注释.

话说s2-16漏洞问题就是出在redirect:这个重定向功能, 具体参考[1]

TIPS: 这里的PrefixTrie值得研究一下,里面涉及到了Trie树的实现,具体参考[2]

3. parseActionName方法分析

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
protected ActionMapping parseActionName(ActionMapping mapping) {
    if (mapping.getName() == null) {
        return mapping;
    }
    if (allowDynamicMethodCalls) {
        // handle "name!method" convention.
        String name = mapping.getName();
        int exclamation = name.lastIndexOf("!");
        if (exclamation != -1) {
            mapping.setName(name.substring(0, exclamation));
            mapping.setMethod(name.substring(exclamation + 1));
        }
    }
    return mapping;
}

从代码中可以看得到,也是一种Dynamic Method的调用方式,例如我们访问:http://locahost/index!add.action , 则调用了indexAction的add方法.

TIPS: 实在受不了为毛一个Dynamic Method功能用各种花样方式实现???

好了,我们已经分析完prepare.findActionMapping(request, response, true)的实现了,成功得到Action的映射关系了,下面开始最后一个环节,执行Action

0x07 执行Action

execute.executeAction(request, response, mapping); 的代码实现

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
/**
 * Contains execution operations for filters
 */
public class ExecuteOperations {

	/**
	 * Executes an action
	 * @throws ServletException
	 */
	public void executeAction(HttpServletRequest request, HttpServletResponse response, ActionMapping mapping) throws ServletException {
	    dispatcher.serviceAction(request, response, servletContext, mapping);
	}





// Dispatcher.java

public class Dispatcher {

    /**
     * Load Action class for mapping and invoke the appropriate Action method, or go directly to the Result.
     * <p/>
     * This method first creates the action context from the given parameters,
     * and then loads an <tt>ActionProxy</tt> from the given action name and namespace.
     * After that, the Action method is executed and output channels through the response object.
     * Actions not found are sent back to the user via the {@link Dispatcher#sendError} method,
     * using the 404 return code.
     * All other errors are reported by throwing a ServletException.
     *
     * @param request  the HttpServletRequest object
     * @param response the HttpServletResponse object
     * @param mapping  the action mapping object
     * @throws ServletException when an unknown error occurs (not a 404, but typically something that
     *                          would end up as a 5xx by the servlet container)
     * @param context Our ServletContext object
     */
    public void serviceAction(HttpServletRequest request, HttpServletResponse response, ServletContext context,
                              ActionMapping mapping) throws ServletException {



        // 上方分析过这个createContextMap,当时的mapping为空,现在已经拿到mapping,重新设置一下上下文
        Map<String, Object> extraContext = createContextMap(request, response, mapping, context);

        // If there was a previous value stack, then create a new copy and pass it in to be used by the new Action
        ValueStack stack = (ValueStack) request.getAttribute(ServletActionContext.STRUTS_VALUESTACK_KEY);
        boolean nullStack = stack == null;
        if (nullStack) {
            ActionContext ctx = ActionContext.getContext();
            if (ctx != null) {
                stack = ctx.getValueStack();
            }
        }
        if (stack != null) {

        	// 这里使用了OgnlValueStackFactory对象,该对象实例化了OgnlValueStack对象,并对OgnlValueStack对象进行初始化
        	// 之前我们对OgnlValueStack做过分析,OgnlValueStack其实就是对ValueStack接口的具体实现,用来对root对象进行ognl计算使用
        	// 具体可以翻看之前的文章 http://dean.csoio.com/posts/struts2-internals-readbook-note_06/
            extraContext.put(ActionContext.VALUE_STACK, valueStackFactory.createValueStack(stack));
        }


        String timerKey = "Handling request from Dispatcher";
        try {

            UtilTimerStack.push(timerKey);

            String namespace = mapping.getNamespace();
            String name = mapping.getName();
            String method = mapping.getMethod();

            Configuration config = configurationManager.getConfiguration();

            // 这里的ActionProxyFactory的实现类是org.apache.struts2.impl.StrutsActionProxyFactory"
            // StrutsActionProxyFactory的createActionProxy方法我们下方会做具体分析, 这里就认为返回了一个ActionProxy类,
            // 具体实现类是org.apache.struts2.impl.StrutsActionProxyFactory
            ActionProxy proxy = config.getContainer().getInstance(ActionProxyFactory.class).createActionProxy(
                    namespace, name, method, extraContext, true, false);

            
            request.setAttribute(ServletActionContext.STRUTS_VALUESTACK_KEY, proxy.getInvocation().getStack());



            // 这里代码太关键,下方我们作为第二部分单独分析.
            // if the ActionMapping says to go straight to a result, do it!
            if (mapping.getResult() != null) {
                Result result = mapping.getResult();
                result.execute(proxy.getInvocation());
            } else {

            	// 调用org.apache.struts2.impl.StrutsActionProxyFactory的execute方法执行
                proxy.execute();
            }

            // If there was a previous value stack then set it back onto the request
            if (!nullStack) {
                request.setAttribute(ServletActionContext.STRUTS_VALUESTACK_KEY, stack);
            }
        } catch (ConfigurationException e) {
        	// WW-2874 Only log error if in devMode
        	if(devMode) {
                String reqStr = request.getRequestURI();
                if (request.getQueryString() != null) {
                    reqStr = reqStr + "?" + request.getQueryString();
                }
                LOG.error("Could not find action or result\n" + reqStr, e);
            }
        	else {
        		LOG.warn("Could not find action or result", e);
        	}
            sendError(request, response, context, HttpServletResponse.SC_NOT_FOUND, e);
        } catch (Exception e) {
            sendError(request, response, context, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e);
        } finally {
            UtilTimerStack.pop(timerKey);
        }
    }

上方我们做了简单分析,其中有多个疑惑,下方我们逐个详细分析.

1. StrutsActionProxyFactory类的createActionProxy方法分析

首先我们看一下createActionProxy方法的调用:

1
2
 ActionProxy proxy = config.getContainer().getInstance(ActionProxyFactory.class).createActionProxy(
                    namespace, name, method, extraContext, true, false);

通过调用可以看到这个createActionProxy方法并未在StrutsActionProxyFactory中实现,而是在其父类DefaultActionProxyFactory中实现,我们看一下DefaultActionProxyFactory类的createActionProxy方法实现代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// DefaultActionProxyFactory.java

public ActionProxy createActionProxy(String namespace, String actionName, String methodName, Map<String, Object> extraContext, boolean executeResult, boolean cleanupContext) {
    

    // 将实例化的Action调用器存入容器
    ActionInvocation inv = new DefaultActionInvocation(extraContext, true);
    container.inject(inv);

    // 这里调用的是StrutsActionProxyFactory类的createActionProxy方法
    return createActionProxy(inv, namespace, actionName, methodName, executeResult, cleanupContext);
}

我们看一下 StrutsActionProxyFactory类的createActionProxy方法实现:


// 其实就是重写了DefaultActionProxyFactory的createActionProxy方法
public class StrutsActionProxyFactory extends DefaultActionProxyFactory {

    @Override
    public ActionProxy createActionProxy(ActionInvocation inv, String namespace, String actionName, String methodName, boolean executeResult, boolean cleanupContext) {
        

        StrutsActionProxy proxy = new StrutsActionProxy(inv, namespace, actionName, methodName, executeResult, cleanupContext);
        container.inject(proxy);

        // 预处理StrutsActionProxy对象,下方我们看一下prepare方法的具体实现
        proxy.prepare();

        // 返回预处理完成的StrutsActionProxy对象
        return proxy;
    }
}

StrutsActionProxy类的prepare方法的具体实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class StrutsActionProxy extends DefaultActionProxy {

    private static final long serialVersionUID = -2434901249671934080L;

    public StrutsActionProxy(ActionInvocation inv, String namespace, String actionName, String methodName,
                             boolean executeResult, boolean cleanupContext) {
        super(inv, namespace, actionName, methodName, executeResult, cleanupContext);
    }

    public String execute() throws Exception {
        ActionContext previous = ActionContext.getContext();
        ActionContext.setContext(invocation.getInvocationContext());
        try {
            return invocation.invoke();
        } finally {
            if (cleanupContext)
                ActionContext.setContext(previous);
        }
    }

    @Override
    protected void prepare() {
        super.prepare();
    }

}

额欧~ 其实就是调用了DefaultActionProxy父类的prepare方法预处理,我们在回去看看了DefaultActionProxy父类的prepare方法实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
public class DefaultActionProxy implements ActionProxy, Serializable {
	
    protected DefaultActionProxy(ActionInvocation inv, String namespace, String actionName, String methodName, boolean executeResult, boolean cleanupContext) {
        
        this.invocation = inv;
        this.cleanupContext = cleanupContext;
        if (LOG.isDebugEnabled()) {
            LOG.debug("Creating an DefaultActionProxy for namespace " + namespace + " and action name " + actionName);
        }

        this.actionName = actionName;
        this.namespace = namespace;
        this.executeResult = executeResult;
        this.method = methodName;
    }


     protected void prepare()  {
        String profileKey = "create DefaultActionProxy: ";
        try {
            UtilTimerStack.push(profileKey);


            // 得到action的配置信息
            config = configuration.getRuntimeConfiguration().getActionConfig(namespace, actionName);
    
            if (config == null && unknownHandlerManager.hasUnknownHandlers()) {
                config = unknownHandlerManager.handleUnknownAction(namespace, actionName);
            }



            // 没有配置action就抛出异常
            if (config == null) {
                String message;
                if ((namespace != null) && (namespace.trim().length() > 0)) {
                    message = LocalizedTextUtil.findDefaultText(XWorkMessages.MISSING_PACKAGE_ACTION_EXCEPTION, Locale.getDefault(), new String[]{
                        namespace, actionName
                    });
                } else {
                    message = LocalizedTextUtil.findDefaultText(XWorkMessages.MISSING_ACTION_EXCEPTION, Locale.getDefault(), new String[]{
                        actionName
                    });
                }
                throw new ConfigurationException(message);
            }
           

            // 这里比较有意思,如果访问的action没有指定method,那么就调用action的execute方法
            // this.method = "execute";
            resolveMethod();
            
            if (!config.isAllowedMethod(method)) {
                throw new ConfigurationException("Invalid method: "+method+" for action "+actionName);
            }


            // 这里的invocation就是DefaultActionInvocation对象,调用DefaultActionInvocation的init方法
            // 这里init做了太多的事情,里面最关键的是初始化了Struts2拦截器。
            // 具体下方会贴出代码,简单看一下就行,就是个action执行前的初始化
            invocation.init(this);

        } finally {
            UtilTimerStack.pop(profileKey);
        }
    }

DefaultActionInvocation对象的init方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
  public void init(ActionProxy proxy) {
        this.proxy = proxy;
        Map<String, Object> contextMap = createContextMap();

        // Setting this so that other classes, like object factories, can use the ActionProxy and other
        // contextual information to operate
        ActionContext actionContext = ActionContext.getContext();

        if (actionContext != null) {
            actionContext.setActionInvocation(this);
        }

        createAction(contextMap);

        if (pushAction) {
            stack.push(action);
            contextMap.put("action", action);
        }

        invocationContext = new ActionContext(contextMap);

        // 设置要调用的ActionName
        invocationContext.setName(proxy.getActionName());

        // get a new List so we don't get problems with the iterator if someone changes the list
        // 初始化拦截器列表
        List<InterceptorMapping> interceptorList = new ArrayList<InterceptorMapping>(proxy.getConfig().getInterceptors());
        interceptors = interceptorList.iterator();
    }

Action代理类实例化已分析完成, 我们回去继续看Dispatcher的serviceAction的实现.

2. Action的执行

我们在回顾一下serviceAction的代码实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// Dispatcher.java

public class Dispatcher {


    // ... 省略n行代码

    public void serviceAction(HttpServletRequest request, HttpServletResponse response, ServletContext context,
                              ActionMapping mapping) throws ServletException {

        // ... 省略n行代码
        
        try {

            // ... 省略n行代码
    
            ActionProxy proxy = config.getContainer().getInstance(ActionProxyFactory.class).createActionProxy(
                    namespace, name, method, extraContext, true, false);


            // ... 省略n行代码


            // result不为空,则调用result类的execute方法执行Action
            if (mapping.getResult() != null) {
                Result result = mapping.getResult();
                result.execute(proxy.getInvocation());
            } else {

                // 调用org.apache.struts2.impl.StrutsActionProxyFactory的execute方法执行
                proxy.execute();
            }


            // If there was a previous value stack then set it back onto the request
            if (!nullStack) {
                request.setAttribute(ServletActionContext.STRUTS_VALUESTACK_KEY, stack);
            }

        } catch (ConfigurationException e) {
          // ... 省略n行代码
        }
    }

如上我们拆分为2个部分分析:

1. result.execute(proxy.getInvocation());

如果我们正常访问http://localhost/index.action,这里的result一定为空,只有我们在用Struts2动态方法调用的时候,才会设置result,下方贴上动态方法调用的部分代码,回顾一下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
     put(REDIRECT_PREFIX, new ParameterAction() {
            public void execute(String key, ActionMapping mapping) {
                ServletRedirectResult redirect = new ServletRedirectResult();
                container.inject(redirect);
                redirect.setLocation(key.substring(REDIRECT_PREFIX
                        .length()));
                mapping.setResult(redirect);
            }
        });

        put(REDIRECT_ACTION_PREFIX, new ParameterAction() {
            public void execute(String key, ActionMapping mapping) {
                String location = key.substring(REDIRECT_ACTION_PREFIX
                        .length());
                ServletRedirectResult redirect = new ServletRedirectResult();
                container.inject(redirect);
                String extension = getDefaultExtension();
                if (extension != null && extension.length() > 0) {
                    location += "." + extension;
                }
                redirect.setLocation(location);
                mapping.setResult(redirect);
            }
        });

我们看一ServletRedirectResult类的execute方法实现:

1
2
3
4
5
6
7
8
public class ServletRedirectResult extends StrutsResultSupport implements ReflectionExceptionHandler {    
    public void execute(ActionInvocation invocation) throws Exception {
        if (anchor != null) {
            anchor = conditionalParse(anchor, invocation);
        }

        super.execute(invocation);
    }

可以看到其实是调用其父类StrutsResultSupport的execute方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public abstract class StrutsResultSupport implements Result, StrutsStatics {

    public void execute(ActionInvocation invocation) throws Exception {

        //  执行location得到string返回结果
        lastFinalLocation = conditionalParse(location, invocation);

        // 调用其子类ServletRedirectResult的doExecute
        doExecute(lastFinalLocation, invocation);
    }



    protected String conditionalParse(String param, ActionInvocation invocation) {
        if (parse && param != null && invocation != null) {
   
            // 执行location得到string返回结果
            // 通过OGNL表达式从invocation对象的stack中进行查找location
            // 这里也就是s2-016漏洞出现的原因
            // 这里重写了ParsedValueEvaluator的evaluate,目的是为了对stack.findValue(localtion)返回的string进行编码
            // 关于stack.findValue之前有分析过,就是调用Ognl表达式
            return TextParseUtil.translateVariables(param, invocation.getStack(),
                    new TextParseUtil.ParsedValueEvaluator() {
                        public Object evaluate(Object parsedValue) {
                            if (encode) {
                                if (parsedValue != null) {
                                    try {
                                        // use UTF-8 as this is the recommended encoding by W3C to
                                        // avoid incompatibilities.
                                        return URLEncoder.encode(parsedValue.toString(), "UTF-8");
                                    }
                                    catch(UnsupportedEncodingException e) {
                                        LOG.warn("error while trying to encode ["+parsedValue+"]", e);
                                    }
                                }
                            }
                            return parsedValue;
                        }
            });

        } else {
            return param;
        }
    }


   protected abstract void doExecute(String finalLocation, ActionInvocation invocation) throws Exception;

从上方代码我们得出执行location得到的stirng。

TIPS : 这里之所以用ognl进行查找,我猜想可能是加强一下重定向功能,但是拿localtion作为表达式。。。呵呵哒

我们继续看一下ServletRedirectResult类的doExecute方法对这个string都做了什么?

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
 /**
     * Redirects to the location specified by calling {@link HttpServletResponse#sendRedirect(String)}.
     *
     * @param finalLocation the location to redirect to.
     * @param invocation    an encapsulation of the action execution state.
     * @throws Exception if an error occurs when redirecting.
     */
    protected void doExecute(String finalLocation, ActionInvocation invocation) throws Exception {
        ActionContext ctx = invocation.getInvocationContext();
        HttpServletRequest request = (HttpServletRequest) ctx.get(ServletActionContext.HTTP_REQUEST);
        HttpServletResponse response = (HttpServletResponse) ctx.get(ServletActionContext.HTTP_RESPONSE);

        // 这里确定是我们重定向的不是一个外部地址
        if (isPathUrl(finalLocation)) {

            // 加入redirect:的value前没有/, 则获取配置文件中的namespace,追加到location前方
            if (!finalLocation.startsWith("/")) {

                ActionMapping mapping = actionMapper.getMapping(request, Dispatcher.getInstance().getConfigurationManager()); 
                String namespace = null;
                if (mapping != null) {
                    namespace = mapping.getNamespace();
                }

                if ((namespace != null) && (namespace.length() > 0) && (!"/".equals(namespace))) {
                    finalLocation = namespace + "/" + finalLocation;
                } else {
                    finalLocation = "/" + finalLocation;
                }
            }



            // if the URL's are relative to the servlet context, append the servlet context path
            // 将Request的上下文路径也追加到location前方
            if (prependServletContext && (request.getContextPath() != null) && (request.getContextPath().length() > 0)) {
                finalLocation = request.getContextPath() + finalLocation;
            }



            // 获取配置文件中Result标签里param标签的配置,进行解析参数
            ResultConfig resultConfig = invocation.getProxy().getConfig().getResults().get(invocation.getResultCode());
            if (resultConfig != null ) {
                Map resultConfigParams = resultConfig.getParams();
                for (Iterator i = resultConfigParams.entrySet().iterator(); i.hasNext();) {
                    Map.Entry e = (Map.Entry) i.next();

                    if (!getProhibitedResultParams().contains(e.getKey())) {

                        // 解析参数

                        // <result name="success" type="redirect">  
                        //       <param name="location">foo.jsp</param>
                        //       <param name="parse">false</param><!--不解析OGNL-->
                        // </result>


                        // 又调用conditionalParse解析
                        requestParameters.put(e.getKey().toString(), e.getValue() == null ? "" : conditionalParse(e.getValue().toString(), invocation));
                        
                        String potentialValue = e.getValue() == null ? "" : conditionalParse(e.getValue().toString(), invocation);
                        
                        if (!supressEmptyParameters || ((potentialValue != null) && (potentialValue.length() > 0))) {
                            requestParameters.put(e.getKey().toString(), potentialValue);
                        }
                    }
                }
            }


            // 将localtion和参数合并
            StringBuilder tmpLocation = new StringBuilder(finalLocation);
            UrlHelper.buildParametersString(requestParameters, tmpLocation, "&");

            // add the anchor
            if (anchor != null) {
                tmpLocation.append('#').append(anchor);
            }
         
            // 对要重定向的location进行编码
            finalLocation = response.encodeRedirectURL(tmpLocation.toString());
        }

        if (LOG.isDebugEnabled()) {
            LOG.debug("Redirecting to finalLocation " + finalLocation);
        }

    
        // 开始重定向    
        sendRedirect(response, finalLocation);
    }


     // 写入response
     protected void sendRedirect(HttpServletResponse response, String finalLocation) throws IOException {
        if (SC_FOUND == statusCode) {
            response.sendRedirect(finalLocation);
        } else {
            response.setStatus(statusCode);
            response.setHeader("Location", finalLocation);
            response.getWriter().write(finalLocation);
            response.getWriter().close();
        }

    }

result执行,分析结束.

2. proxy.execute();

终于要大结局了,来,我们看一下 StrutsActionProxy.execute方法的实现,

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class StrutsActionProxy extends DefaultActionProxy {

    private static final long serialVersionUID = -2434901249671934080L;

    public StrutsActionProxy(ActionInvocation inv, String namespace, String actionName, String methodName,
                             boolean executeResult, boolean cleanupContext) {
        super(inv, namespace, actionName, methodName, executeResult, cleanupContext);
    }

    public String execute() throws Exception {
        ActionContext previous = ActionContext.getContext();
        ActionContext.setContext(invocation.getInvocationContext());
        try {
            
            // 这里我们调用的是DefaultActionInvocation的invoke方法
            return invocation.invoke();
        } finally {
            if (cleanupContext)
                ActionContext.setContext(previous);
        }
    }

    @Override
    protected void prepare() {
        super.prepare();
    }

}

DefaultActionInvocation的invoke方法的实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
public class DefaultActionInvocation implements ActionInvocation {

 public String invoke() throws Exception {
        String profileKey = "invoke: ";
        try {
            UtilTimerStack.push(profileKey);

            if (executed) {
                throw new IllegalStateException("Action has already executed");
            }



            // 遍历Struts2的拦截器,并调用每个拦截器的intercept方法,返回每个拦截器的resultCode
            // 由于拦截器太多了,我们这里就不分析了
            if (interceptors.hasNext()) {
                final InterceptorMapping interceptor = (InterceptorMapping) interceptors.next();
                String interceptorMsg = "interceptor: " + interceptor.getName();
                UtilTimerStack.push(interceptorMsg);
                try {
                                resultCode = interceptor.getInterceptor().intercept(DefaultActionInvocation.this);
                            }
                finally {
                    UtilTimerStack.pop(interceptorMsg);
                }
            } else {



                // 拦截器调用完后,才是我们用户自定义action的调用,下方我们好好分析一下这个方法
                resultCode = invokeActionOnly();
            }

             
            // this is needed because the result will be executed, then control will return to the Interceptor, which will
            // return above and flow through again
            if (!executed) {
                if (preResultListeners != null) {
                    for (Object preResultListener : preResultListeners) {
                        PreResultListener listener = (PreResultListener) preResultListener;

                        String _profileKey = "preResultListener: ";
                        try {
                            UtilTimerStack.push(_profileKey);
                            listener.beforeResult(this, resultCode);
                        }
                        finally {
                            UtilTimerStack.pop(_profileKey);
                        }
                    }
                }

                // 当执行完了用户的indexAction,开始对result进行处理
                if (proxy.getExecuteResult()) {
                    executeResult();
                }

                executed = true;
            }

            return resultCode;
        }
        finally {
            UtilTimerStack.pop(profileKey);
        }
    }

我们看一下invokeActionOnly的实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
    public String invokeActionOnly() throws Exception {
        return invokeAction(getAction(), proxy.getConfig());
    }

    public Object getAction() {

        // 拿到我们的indexAction对象
        return action;
    }


    protected String invokeAction(Object action, ActionConfig actionConfig) throws Exception {

        // 得到indexAction的执行方法名
        String methodName = proxy.getMethod();

        if (LOG.isDebugEnabled()) {
            LOG.debug("Executing action method = " + actionConfig.getMethodName());
        }

        String timerKey = "invokeAction: " + proxy.getActionName();
        try {
            UtilTimerStack.push(timerKey);

            boolean methodCalled = false;
            Object methodResult = null;
            Method method = null;
            try {

                // 得到method对象
                method = getAction().getClass().getMethod(methodName, EMPTY_CLASS_ARRAY);

            } catch (NoSuchMethodException e) {
                // hmm -- OK, try doXxx instead
                try {
                    String altMethodName = "do" + methodName.substring(0, 1).toUpperCase() + methodName.substring(1);

                    // 额。。看懂了吧? 这又是一个风骚的操作,带有do的方法名
                    method = getAction().getClass().getMethod(altMethodName, EMPTY_CLASS_ARRAY);

                } catch (NoSuchMethodException e1) {
                    // well, give the unknown handler a shot
                    if (unknownHandlerManager.hasUnknownHandlers()) {
                        try {
                            methodResult = unknownHandlerManager.handleUnknownMethod(action, methodName);
                            methodCalled = true;
                        } catch (NoSuchMethodException e2) {
                            // throw the original one
                            throw e;
                        }
                    } else {
                        throw e;
                    }
                }
            }

            if (!methodCalled) {

                // 用java的反射,调用indexAction的方法,得到result结果
                methodResult = method.invoke(action, new Object[0]);
            }

        

            // 这里扩展了返回结果的类型,具体可以看struts-default.xml中的配置, 好多好多result扩展类型
            if (methodResult instanceof Result) {
                this.explicitResult = (Result) methodResult;

                // Wire the result automatically
                container.inject(explicitResult);
                return null;
            } else {

                // 返回普通的string结果
                return (String) methodResult;
            }

        } catch (NoSuchMethodException e) {
            throw new IllegalArgumentException("The " + methodName + "() is not defined in action " + getAction().getClass() + "");
        } catch (InvocationTargetException e) {
            // We try to return the source exception.
            Throwable t = e.getTargetException();

            if (actionEventListener != null) {
                String result = actionEventListener.handleException(t, getStack());
                if (result != null) {
                    return result;
                }
            }
            if (t instanceof Exception) {
                throw (Exception) t;
            } else {
                throw e;
            }
        } finally {
            UtilTimerStack.pop(timerKey);
        }
    }

好了,分析完了action的执行,我们看一下结果处理,回顾一下

1
2
3
4
5
6
7
8
    resultCode = invokeActionOnly();

    
    // 省略n行代码

    if (proxy.getExecuteResult()) {
                    executeResult();
    }

看一下executeResult的实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
    /**
     * Uses getResult to get the final Result and executes it
     *
     * @throws ConfigurationException If not result can be found with the returned code
     */
    private void executeResult() throws Exception {


        // 创建ServletDispchterResult对象,
        // 通过配置文件获取result配置信息,查找到location
        // 设置ServletDispchterResult对象的location为配置文件获得location
        result = createResult();

        String timerKey = "executeResult: " + getResultCode();
        try {
            UtilTimerStack.push(timerKey);
            if (result != null) {

                // 执行result
                // 最终还是调用的ServletDispchterResult的doExecute方法
                // 下方我们看一下这个方法的具体实现
                result.execute(this);

            } else if (resultCode != null && !Action.NONE.equals(resultCode)) {
                throw new ConfigurationException("No result defined for action " + getAction().getClass().getName()
                        + " and result " + getResultCode(), proxy.getConfig());
            } else {
                if (LOG.isDebugEnabled()) {
                    LOG.debug("No result returned for action " + getAction().getClass().getName() + " at " + proxy.getConfig().getLocation());
                }
            }
        } finally {
            UtilTimerStack.pop(timerKey);
        }
    }

ServletDispchterResult的doExecute方法的实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// 加入我们的配置文件里的location是:/WEB-INF/jsp/index.jsp
public void doExecute(String finalLocation, ActionInvocation invocation) throws Exception {
        if (LOG.isDebugEnabled()) {
            LOG.debug("Forwarding to location " + finalLocation);
        }

        PageContext pageContext = ServletActionContext.getPageContext();

        if (pageContext != null) {
            pageContext.include(finalLocation);
        } else {
            HttpServletRequest request = ServletActionContext.getRequest();
            HttpServletResponse response = ServletActionContext.getResponse();

            // 将配置文件中的location传递给系统Servlet的getRequestDispatcher方法
            // 关于Servlet的getRequestDispatcher方法,参考[3]
            // 这里仅是构造dispatcher,(这是java的servlet的编程, 非struts2)
            RequestDispatcher dispatcher = request.getRequestDispatcher(finalLocation);

            //add parameters passed on the location to #parameters
            // see WW-2120
            if (invocation != null && finalLocation != null && finalLocation.length() > 0
                    && finalLocation.indexOf("?") > 0) {
                String queryString = finalLocation.substring(finalLocation.indexOf("?") + 1);
                Map parameters = (Map) invocation.getInvocationContext().getContextMap().get("parameters");
                Map queryParams = UrlHelper.parseQueryString(queryString, true);
                if (queryParams != null && !queryParams.isEmpty())
                    parameters.putAll(queryParams);
            }

            // if the view doesn't exist, let's do a 404
            if (dispatcher == null) {
                response.sendError(404, "result '" + finalLocation + "' not found");
                return;
            }

            //if we are inside an action tag, we always need to do an include
            Boolean insideActionTag = (Boolean) ObjectUtils.defaultIfNull(request.getAttribute(StrutsStatics.STRUTS_ACTION_TAG_INVOCATION), Boolean.FALSE);

            // If we're included, then include the view
            // Otherwise do forward
            // This allow the page to, for example, set content type
            if (!insideActionTag && !response.isCommitted() && (request.getAttribute("javax.servlet.include.servlet_path") == null)) {
                request.setAttribute("struts.view_uri", finalLocation);
                request.setAttribute("struts.request_uri", request.getRequestURI());

                // 转发到 /WEB-INF/jsp/index.jsp ,由jsp进行相应操作
                dispatcher.forward(request, response);
            } else {
                dispatcher.include(request, response);
            }
        }
    }

目前我们已经分析完成Action的执行和Result处理的所有流程,最后可以看到Result结果处理使用的是Servlet的API,具体可以参考[4].

下一篇我们分析一下Struts2的标签库

参考

  1. S2-016远程代码执行漏洞分析
  2. struts2实现的简单的Trie树
  3. getRequestDispatcher 和sendRedirect区别及路径问题
  4. Servlet中关于RequestDispatcher的原理