1. 程式人生 > >誰說ParameterMap只能讀不能寫?

誰說ParameterMap只能讀不能寫?

tomcat8.0 循環 new 怎麽 org 展示 構造 servlets nic

開發過javaweb項目的同學,應該都接觸過ServeltRequest吧?ServletRequest接口中有一個方法叫做getParameterMap(),他會返回一個Map<String, String[]>對象,裏面含有Request的請求參數,例如GET請求時?後邊的一堆參數。那如果我們能修改Map<String, String[]>對象,豈不是能篡改瀏覽器請求時的一些參數?

1 ParameterMap

1.1 ServletRequest接口

服務器能從ServletRequest中篡改瀏覽器請求的參數?想想都令人興奮,我們又多了一個可以個性化的地方。然而實際上是不可以的,我們來看看ServeltRequest

getParameterMap()方法的註釋吧。


    /**
     * 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&lt;String, String[]&gt; getParameterMap();

人家說了,返回的這個Map對象一定是不可變的。所以呢,就死了這條心吧。咱們還是看看tomcat中ServletRequest的實現類裏面到底是怎麽構造不可變的Map。

1.2 Tomcat中的Request實現類

註意,以下凡是沒有特殊說明的tomcat,其版本都是7.0.52

tomcat中ServletRequest的實現類是org.apache.catalina.connector.Request。在這個實現類中,方法getParameterMap()是這樣實現的。


    /**
     * Returns a &lt;code&gt;Map&lt;/code&gt; 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 &lt;code&gt;Map&lt;/code&gt; containing parameter names as keys
     *  and parameter values as map values.
     */
    @Override
    public Map&lt;String, String[]&gt; getParameterMap() {

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

        Enumeration&lt;String&gt; 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&lt;String, String[]&gt; parameterMap = new ParameterMap&lt;&gt;();

它就是在對象創建的時候創建了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 &lt;code&gt;getParameterMap()&lt;/code&gt; method of the
     * wrapped request.
     */
    @Override
    public Map&lt;String, String[]&gt; 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&lt;String, String[]&gt;();
        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&lt;String, String[]&gt; copyMap(Map&lt;String, String[]&gt; orig) {

        if (orig == null)
            return (new HashMap&lt;String, String[]&gt;());
        HashMap&lt;String, String[]&gt; dest = new HashMap&lt;String, String[]&gt;();
        
        for (Map.Entry&lt;String, String[]&gt; 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(&quot;/forward0&quot;)
    @ResponseBody
    public String forward0(ServletRequest request, ServletResponse response) {
        return request.getParameterMap().getClass().getCanonicalName();
    }

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

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

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

    @RequestMapping(&quot;/forward6&quot;)
    @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(&quot;ForwardFilter&quot;) ==null){
            hsr.setAttribute(&quot;ForwardFilter&quot;,ForwardFilter.class);

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

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

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

    }

    public void destroy() {

    }
}

3.3 最終結果

上述代碼怎麽組織運行,筆者就不細講了,網上例子很多。結果我來展示一下:

  1. /forward0:org.apache.catalina.util.ParameterMap
  2. /forward1:java.util.HashMap
  3. /forward2:java.util.HashMap
  4. /forward4:java.util.HashMap
  5. /forward5:java.util.HashMap
  6. /forward6:java.util.HashMap

神奇不神奇?一個javaEE標準聲明了是不可變對象,在這個實驗裏變成了可變對象。

4 奇襲ApplicationHttpRequest

話說到這裏,大家對getParameterMap方法也有了個簡單了解,如果想改變Map對象的K-V,你就搞一個轉發請求。喝一杯咖啡,優雅地實現一些奇怪的邏輯。

不過筆者沒有就此罷手,手賤登上了github,看了一下tomcat項目中的這個類。這個類代碼的HashMap竟然被替代成了ParameterMap!!!

我就納悶類,是誰改了這個bug?導致我不能利用這個bug做一些邪惡的事情。

當當當當,blame一下,找到了,ApplicationHttpRequest修改記錄,是一個年輕小夥子16年左右修復了這個bug。Tomcat7.0.68版本,Tomcat8.0.14版本開始,這個bug被修復了。

是不是很氣人,原先這個功能用的好好的,升了級竟然用不了了。

生氣生氣生氣??????,怎麽辦怎麽辦怎麽辦,我想同學們已經有辦法了。那就是反射ParameterMap,射射射,把locked屬性,設置為可訪問,然後將locked設置成false。

5 總結

筆者在這裏和大家分享了一個小功能,小bug,耽誤了大家的一些時間。但上邊這些內容完全是筆者在生產開發中遇到的一些問題,筆者以有趣的方式來展示這些問題,以期和大家深入地探討技術。

總結一下吧,ParameterMap這個Map是不可變的,建議大家還是別打這個對象的主意。為什麽?javaEE標準裏說了它是不可變的,那麽各大Servlet容器廠商自然會以不同地方式實現這個不可變Map,今天你可以修改locked,明天一升級,人家改叫isLocked,那你的代碼還能正常運行嗎?

那有沒有別的方式我們可以讓它可變?有的,你寫一個filter,在裏面對request做一個包裝,在getParameterMap時候,返回一個HashMap就可以了。

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

以上同步自誰說ParameterMap只能讀不能寫?

誰說ParameterMap只能讀不能寫?