誰說ParameterMap只能讀不能寫?
開發過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<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,然後給瀏覽器中顯示它的類型。怎麽對比呢?`
/forward0
直接獲取ParameterMap類型/forward1
調用forward轉發請求到/forward3
/forward2
調用include包含請求到/forward3
/forward4
(沒有對應的RequestMapping)在Filter中調用forward轉發請求到/forward3
/forward5
(沒有對應的RequestMapping)在Filter中調用include包含請求到/forward3
/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
:org.apache.catalina.util.ParameterMap/forward1
:java.util.HashMap/forward2
:java.util.HashMap/forward4
:java.util.HashMap/forward5
:java.util.HashMap/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只能讀不能寫?