转载

谁说ParameterMap只能读不能写? 原 荐

开发过javaweb项目的同学,应该都接触过 ServeltRequest 吧?ServletRequest接口中有一个方法叫做 getParameterMap() ,他会返回一个 Map<String, String[]> 对象,里面含有Request的请求参数,例如GET请求时?后边的一堆参数。那如果我们能修改 Map<String, String[]> 对象,岂不是能篡改浏览器请求时的一些参数?

1 ParameterMap

1.1 ServletRequest接口

服务器能从ServletRequest中篡改浏览器请求的参数?想想都令人兴奋,我们又多了一个可以个性化的地方。然而实际上是不可以的,我们来看看 ServeltRequestgetParameterMap() 方法的注释吧。

/**
     * Returns a java.util.Map of the parameters of this request. Request
     * parameters are extra information sent with the request. For HTTP
     * servlets, parameters are contained in the query string or posted form
     * data.
     *
     * @return an immutable java.util.Map containing parameter names as keys and
     *         parameter values as map values. The keys in the parameter map are
     *         of type String. The values in the parameter map are of type
     *         String array.
     */
    public Map<String, String[]> getParameterMap();

人家说了,返回的这个Map对象一定是不可变的。所以呢,就死了这条心吧。咱们还是看看tomcat中 ServletRequest 的实现类里面到底是怎么构造不可变的Map。

1.2 Tomcat中的Request实现类

注意,以下凡是没有特殊说明的tomcat,其版本都是7.0.52

tomcat中 ServletRequest 的实现类是 org.apache.catalina.connector.Request 。在这个实现类中,方法 getParameterMap() 是这样实现的。

/**
     * Returns a <code>Map</code> of the parameters of this request.
     * Request parameters are extra information sent with the request.
     * For HTTP servlets, parameters are contained in the query string
     * or posted form data.
     *
     * @return A <code>Map</code> containing parameter names as keys
     *  and parameter values as map values.
     */
    @Override
    public Map<String, String[]> getParameterMap() {

        if (parameterMap.isLocked()) {
            return parameterMap;
        }

        Enumeration<String> enumeration = getParameterNames();
        while (enumeration.hasMoreElements()) {
            String name = enumeration.nextElement();
            String[] values = getParameterValues(name);
            parameterMap.put(name, values);
        }

        parameterMap.setLocked(true);

        return parameterMap;

    }

如果属性parameterMap是上锁的,就返回这个属性。否则填充这个属性,然后上锁,返回属性。我就纳闷来,这个parameterMap高端啊,怎么就有一个判断是否上锁的方法,还有,这个属性在对象生成的时候已经做了初始化,所以它才可以直接调用这个属性的方法。

带着这些疑问,咱们来看看这个属性的初始化,其实很容易就能找到。

/**
     * Hash map used in the getParametersMap method.
     */
    protected ParameterMap<String, String[]> parameterMap = new ParameterMap<>();

它就是在对象创建的时候创建了 ParameterMap 类型的对象。

1.3 ParameterMap类

这个 org.apache.catalina.util.ParameterMap 类,看来我们得重点关注了。打开这个类,我们发现它其实就是一个代理类,里边包含一个 private final Map<K,V> delegatedMap; 属性,其次,还有一个 private boolean locked = false; 。看到这里大家可能就明白了,无非是在做增删改操作的时候,先判断有没有锁,再执行操作,如果有锁,就抛出异常。

2 奇特的ApplicationHttpRequest

2.1 ApplicationHttpRequest

其实,上一小结已经点明,ServletRequest声明返回的Map是不可修改的,tomcat里也做到了不可修改。我们以后使用的时候注意一下就行,别自作聪明修改ParameterMap里的属性。

但是笔者是个较真的人,利用IDE,笔者也看到了别的实现类,其中 org.apache.catalina.core.ApplicationHttpRequest 引起了笔者的注意。这个类翻译成中文就是应用级别的HTTP请求,那他有什么特殊点呢?它实际上也是一个代理类,里面包含类实际的Request对象,来看他的 getParameterMap() 方法。

/**
     * Override the <code>getParameterMap()</code> method of the
     * wrapped request.
     */
    @Override
    public Map<String, String[]> getParameterMap() {

        parseParameters();
        return (parameters);

    }
    
    /**
     * Parses the parameters of this request.
     *
     * If parameters are present in both the query string and the request
     * content, they are merged.
     */
    void parseParameters() {

        if (parsedParams) {
            return;
        }

        parameters = new HashMap<String, String[]>();
        parameters = copyMap(getRequest().getParameterMap());
        mergeParameters();
        parsedParams = true;
    }
    
    /**
     * Perform a shallow copy of the specified Map, and return the result.
     *
     * @param orig Origin Map to be copied
     */
    Map<String, String[]> copyMap(Map<String, String[]> orig) {

        if (orig == null)
            return (new HashMap<String, String[]>());
        HashMap<String, String[]> dest = new HashMap<String, String[]>();
        
        for (Map.Entry<String, String[]> entry : orig.entrySet()) {
            dest.put(entry.getKey(), entry.getValue());
        }

        return (dest);

    }

这三个方法依次看下来, org.apache.catalina.util.ParameterMap 毛的没见到,只有HashMap, 啥情况? ,tomcat怎么留了这么一个口子?他是干什么用的?什么时候我们的程序能得到这个Request?

2.2 ApplicationDispatcher

带着这个疑问,笔者又深入的搜寻类一番,发现 org.apache.catalina.core.ApplicationDispatcher 中在方法forward()和方法include()里对原始的Request包装上了 ApplicationHttpRequest

这个ApplicationDispatcher实际上实现了 javax.servlet.RequestDispatcher.RequestDispatcher ,而RequestDispatcher的作用是转发或者包含别的资源,例如JSP,Servlet。

说了,那么多,那到底怎么用呢?实际上ServletRequest有一个方法能够获取RequestDispatcher,然后再调用RequestDispatcher的forward或者include方法。

3 一个简单的实验

3.1 说明

笔者做了一个简单的实验,先说一下实验内容,在Controller的方法中,获取ParameterMap,然后给浏览器中显示它的类型。怎么对比呢?`

  1. /forward0 直接获取ParameterMap类型
  2. /forward1 调用forward转发请求到 /forward3
  3. /forward2 调用include包含请求到 /forward3
  4. /forward4 (没有对应的RequestMapping)在Filter中调用forward转发请求到 /forward3
  5. /forward5 (没有对应的RequestMapping)在Filter中调用include包含请求到 /forward3
  6. /forward6 调用forward转发请求到 /forward6 ,注意这个只会调用一次,否则会进入死循环

3.2 代码

来看看Controller和Filter

ForwardController.java

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.IOException;

@Controller
public class ForwardController {

    @RequestMapping("/forward0")
    @ResponseBody
    public String forward0(ServletRequest request, ServletResponse response) {
        return request.getParameterMap().getClass().getCanonicalName();
    }

    @RequestMapping("/forward1")
    public void forward1(ServletRequest request, ServletResponse response) throws ServletException, IOException {
        RequestDispatcher rd = request.getRequestDispatcher("/forward3");
        rd.forward(request, response);
    }

    @RequestMapping("/forward2")
    public void forward2(ServletRequest request, ServletResponse response) throws ServletException, IOException {
        RequestDispatcher rd = request.getRequestDispatcher("/forward3");
        rd.include(request, response);
    }

    @RequestMapping("/forward3")
    @ResponseBody
    public String forward3(ServletRequest request, ServletResponse response) {
        return request.getParameterMap().getClass().getCanonicalName();
    }

    @RequestMapping("/forward6")
    @ResponseBody
    public String forward6(ServletRequest request, ServletResponse response) {
        return request.getParameterMap().getClass().getCanonicalName();
    }
}

ForwardFilter.java

package com.gavinzh.learn.web.filter;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

public class ForwardFilter implements Filter{
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest hsr = (HttpServletRequest) servletRequest;

        if (hsr.getAttribute("ForwardFilter") ==null){
            hsr.setAttribute("ForwardFilter",ForwardFilter.class);

            if (hsr.getRequestURI().equals("/forward4")){
                RequestDispatcher rd = servletRequest.getRequestDispatcher("/forward3");
                rd.forward(servletRequest,servletResponse);
                return;
            }
            if (hsr.getRequestURI().equals("/forward5")){
                RequestDispatcher rd = servletRequest.getRequestDispatcher("/forward3");
                rd.include(servletRequest,servletResponse);
                return;
            }

            if (hsr.getRequestURI().equals("/forward6")){
                RequestDispatcher rd = servletRequest.getRequestDispatcher("/forward6");
                rd.forward(servletRequest,servletResponse);
                return;
            }

            filterChain.doFilter(servletRequest,servletResponse);
        }
        filterChain.doFilter(servletRequest,servletResponse);

    }

    public void destroy() {

    }
}

3.3 最终结果

上述代码怎么组织运行,笔者就不细讲来,网上例子很多。结果我来展示一下:

/forward0
/forward1
/forward2
/forward4
/forward5
/forward6

神奇不神奇?一个javaEE标准声明了是不可变对象,在这个实验里变成了可变对象。

4 奇袭ApplicationHttpRequest

话说到这里,大家对getParameterMap方法也有了个简单了解,如果想改变Map对象的K-V,你就搞一个转发请求。喝一杯咖啡,优雅地实现一些奇怪的逻辑。

不过笔者没有就此罢手,手贱登上来github,看来一下tomcat项目中的这个类。 这个类代码的HashMap竟然被替代成了ParameterMap!!!

我就纳闷类,是谁改了这个bug?导致我不能利用这个bug做一些邪恶的事情。

当当当当,blame一下,找到了, ApplicationHttpRequest修改记录 ,是一个年轻小伙子16年左右修复了这个bug。Tomcat7.0.68版本,Tomcat8.0.14版本开始,这个bug被修复了。

是不是很气人,原先这个功能用的好好的,升了级竟然用不了了。

生气生气生气:angry::angry::angry:,怎么办怎么办怎么办,我想同学们已经有办法了。那就是反射ParameterMap,射射射,把locked属性,设置为可访问,然后将locked设置成false。

5 总结

笔者在这里和大家分享了一个小功能,小bug,耽误了大家的一些时间。但上边这些内容完全是笔者在生产开发中遇到的一些问题,笔者以有趣的方式来展示这些问题,以期和大家深入地探讨技术。

总结一下吧,ParameterMap这个Map是不可变的,建议大家还是别打这个对象的主意。为什么?javaEE标准里说了它是不可变的,那么各大Servlet容器厂商自然会以不同地方式实现这个不可变Map,今天你可以修改locked,明天一升级,人家改叫isLocked,那你的代码还能正常运行吗?

那有没有别的方式我们可以让它可变?有的,你写一个filter,在里面对request做一个包装,在getParameterMap时候,返回一个HashMap就可以了。

有趣吧?从可以修改ParameterMap到不能修改,到可以修改,再到建议不要修改,再到可以修改。每一步都是精华呀。

以上同步自 谁说ParameterMap只能读不能写?

原文  https://my.oschina.net/jiangmitiao/blog/2992162
正文到此结束
Loading...