13

我正在处理遗留代码,需要打一个补丁。

问题:一个古老的应用程序发送了错误的 HTTP POST 请求。其中一个参数不是 URL 编码的。我知道这个参数总是排在最后,而且我知道它的名字。我现在正试图在运行在 tomcat 中的服务器端修复它。

此参数无法通过 HttpServletRequest 的标准 getParameter 方法访问,因为它格式错误。方法只是返回 null。但是当我通过 ServletInputStream 手动读取整个请求体时,所有其他参数都消失了。看起来底层类无法解析 ServletInputStream 的内容,因为它已经耗尽。

到目前为止,我已经设法制作了一个从正文读取所有参数并覆盖所有参数访问方法的包装器。但是,如果我之前的链中的任何过滤器将尝试访问任何参数,那么一切都会中断,因为 ServletInputStream 将为空。

我能以某种方式逃避这个问题吗?可能有不同的方法吗?

总而言之,如果我在过滤器中读取原始请求正文,参数将从请求中消失。如果我读取单个参数,ServletInputStream 将变为空,并且无法手动处理。此外,通过 getParameter 方法无法读取格式错误的参数。

4

5 回答 5

13

我找到的解决方案:

仅仅重新定义参数访问方法是不够的。必须做几件事。

  1. 需要一个过滤器来包装请求。
  2. 重写所有参数访问方法需要自定义HttpRequestWrapper 。请求正文应在构造函数中解析并存储为字段。
  3. getInputStreamgetReader方法也应该重新定义。它们返回值取决于存储的请求正文。
  4. 需要扩展ServletInputStream的自定义类,因为这是抽象的。

这 4 个组合将允许您使用getParameter而不会干扰getInputStreamgetReader方法。

请注意,手动请求参数解析可能会因多部分请求而变得复杂。但这是另一个话题。

为了澄清,我重新定义了参数访问方法,因为我的请求已损坏,如问题中所述。你可能不需要那个。

于 2009-03-31T08:39:19.123 回答
10

为什么不安装重写请求的 servlet 过滤器,而不是覆盖方法?

Jason Hunter 有一篇关于过滤器的非常好的文章。

于 2009-03-25T12:04:27.417 回答
6

我做了一个更完整的包装器,在 Content-Type 是 application/x-www-form-urlencoded 并且您已经调用了 getParameterXXX 方法之一的情况下,您仍然可以访问内容:

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;  
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.security.Principal;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Locale;
import java.util.Map;
import java.util.StringTokenizer;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletInputStream;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

/**
 * This class implements the Wrapper or Decorator pattern.<br/> 
 * Methods default to calling through to the wrapped request object, 
 * except the ones that read the request's content (parameters, stream or reader).
 * <p>
 * This class provides a buffered content reading that allows the methods
 * {@link #getReader()}, {@link #getInputStream()} and any of the getParameterXXX to be     called
 * safely and repeatedly with the same results.
 * <p>
 * This class is intended to wrap relatively small HttpServletRequest instances.
 * 
 * @author pgurov
 */
public class HttpServletRequestWrapper implements HttpServletRequest {

private class ServletInputStreamWrapper extends ServletInputStream {

    private byte[] data;
    private int idx = 0;
    ServletInputStreamWrapper(byte[] data) {
        if(data == null)
            data = new byte[0];
        this.data = data;
    }
    @Override
    public int read() throws IOException {
        if(idx == data.length)
            return -1;
        return data[idx++];
    }

}

private HttpServletRequest req;
private byte[] contentData;
private HashMap<String, String[]> parameters;

public HttpServletRequestWrapper() {
    //a trick for Groovy
    throw new IllegalArgumentException("Please use HttpServletRequestWrapper(HttpServletRequest request) constructor!");
}

private HttpServletRequestWrapper(HttpServletRequest request, byte[] contentData, HashMap<String, String[]> parameters) {
    req = request;
    this.contentData = contentData;
    this.parameters = parameters;
}

public HttpServletRequestWrapper(HttpServletRequest request) {
    if(request == null)
        throw new IllegalArgumentException("The HttpServletRequest is null!");
    req = request;
}

/**
 * Returns the wrapped HttpServletRequest.
 * Using the getParameterXXX(), getInputStream() or getReader() methods may interfere
 * with this class operation.
 * 
 * @return 
 *      The wrapped HttpServletRequest.
 */
public HttpServletRequest getRequest() {
    try {
        parseRequest();
    } catch (IOException e) {
        throw new IllegalStateException("Cannot parse the request!", e);
    }
    return new HttpServletRequestWrapper(req, contentData, parameters);
}

/**
 * This method is safe to use multiple times.
 * Changing the returned array will not interfere with this class operation.
 * 
 * @return 
 *      The cloned content data.
 */
public byte[] getContentData() {
    return contentData.clone();
}

/**
 * This method is safe to use multiple times.
 * Changing the returned map or the array of any of the map's values will not
 * interfere with this class operation.
 * 
 * @return
 *      The clonned parameters map.
 */
public HashMap<String, String[]> getParameters() {
    HashMap<String, String[]> map = new HashMap<String, String[]>(parameters.size() * 2);
    for(String key : parameters.keySet()) {
        map.put(key, parameters.get(key).clone());
    }
    return map;
}

private void parseRequest() throws IOException {
    if(contentData != null)
        return; //already parsed

    byte[] data = new byte[req.getContentLength()];
    int len = 0, totalLen = 0;
    InputStream is = req.getInputStream();
    while(totalLen < data.length) {
        totalLen += (len = is.read(data, totalLen, data.length - totalLen));
        if(len < 1)
            throw new IOException("Cannot read more than " + totalLen + (totalLen == 1 ? " byte!" : " bytes!"));
    }
    contentData = data;
    String enc = req.getCharacterEncoding();
    if(enc == null)
        enc = "UTF-8";
    String s = new String(data, enc), name, value;
    StringTokenizer st = new StringTokenizer(s, "&");
    int i;
    HashMap<String, LinkedList<String>> mapA = new HashMap<String, LinkedList<String>>(data.length * 2);
    LinkedList<String> list;
    boolean decode = req.getContentType() != null && req.getContentType().equals("application/x-www-form-urlencoded");
    while(st.hasMoreTokens()) {
        s = st.nextToken();
        i = s.indexOf("=");
        if(i > 0 && s.length() > i + 1) {
            name = s.substring(0, i);
            value = s.substring(i+1);
            if(decode) {
                try {
                    name = URLDecoder.decode(name, "UTF-8");
                } catch(Exception e) {}
                try {
                    value = URLDecoder.decode(value, "UTF-8");
                } catch(Exception e) {}
            }
            list = mapA.get(name);
            if(list == null) {
                list = new LinkedList<String>();
                mapA.put(name, list);
            }
            list.add(value);
        }
    }
    HashMap<String, String[]> map = new HashMap<String, String[]>(mapA.size() * 2);
    for(String key : mapA.keySet()) {
        list = mapA.get(key);
        map.put(key, list.toArray(new String[list.size()]));
    }
    parameters = map;
}

/**
 * This method is safe to call multiple times.
 * Calling it will not interfere with getParameterXXX() or getReader().
 * Every time a new ServletInputStream is returned that reads data from the begining.
 * 
 * @return
 *      A new ServletInputStream.
 */
public ServletInputStream getInputStream() throws IOException {
    parseRequest();

    return new ServletInputStreamWrapper(contentData);
}

/**
 * This method is safe to call multiple times.
 * Calling it will not interfere with getParameterXXX() or getInputStream().
 * Every time a new BufferedReader is returned that reads data from the begining.
 * 
 * @return
 *      A new BufferedReader with the wrapped request's character encoding (or UTF-8 if null).
 */
public BufferedReader getReader() throws IOException {
    parseRequest();

    String enc = req.getCharacterEncoding();
    if(enc == null)
        enc = "UTF-8";
    return new BufferedReader(new InputStreamReader(new ByteArrayInputStream(contentData), enc));
}

/**
 * This method is safe to execute multiple times.
 * 
 * @see javax.servlet.ServletRequest#getParameter(java.lang.String)
 */
public String getParameter(String name) {
    try {
        parseRequest();
    } catch (IOException e) {
        throw new IllegalStateException("Cannot parse the request!", e);
    }
    String[] values = parameters.get(name);
    if(values == null || values.length == 0)
        return null;
    return values[0];
}

/**
 * This method is safe.
 * 
 * @see {@link #getParameters()}
 * @see javax.servlet.ServletRequest#getParameterMap()
 */
@SuppressWarnings("unchecked")
public Map getParameterMap() {
    try {
        parseRequest();
    } catch (IOException e) {
        throw new IllegalStateException("Cannot parse the request!", e);
    }
    return getParameters();
}

/**
 * This method is safe to execute multiple times.
 * 
 * @see javax.servlet.ServletRequest#getParameterNames()
 */
@SuppressWarnings("unchecked")
public Enumeration getParameterNames() {
    try {
        parseRequest();
    } catch (IOException e) {
        throw new IllegalStateException("Cannot parse the request!", e);
    }
    return new Enumeration<String>() {
        private String[] arr = getParameters().keySet().toArray(new String[0]); 
        private int idx = 0;

        public boolean hasMoreElements() {
            return idx < arr.length;
        }

        public String nextElement() {
            return arr[idx++];
        }

    };
}

/**
 * This method is safe to execute multiple times.
 * Changing the returned array will not interfere with this class operation.
 * 
 * @see javax.servlet.ServletRequest#getParameterValues(java.lang.String)
 */
public String[] getParameterValues(String name) {
    try {
        parseRequest();
    } catch (IOException e) {
        throw new IllegalStateException("Cannot parse the request!", e);
    }
    String[] arr = parameters.get(name);
    if(arr == null)
        return null;
    return arr.clone();
}

/* (non-Javadoc)
 * @see javax.servlet.http.HttpServletRequest#getAuthType()
 */
public String getAuthType() {
    return req.getAuthType();
}

/* (non-Javadoc)
 * @see javax.servlet.http.HttpServletRequest#getContextPath()
 */
public String getContextPath() {
    return req.getContextPath();
}

/* (non-Javadoc)
 * @see javax.servlet.http.HttpServletRequest#getCookies()
 */
public Cookie[] getCookies() {
    return req.getCookies();
}

/* (non-Javadoc)
 * @see javax.servlet.http.HttpServletRequest#getDateHeader(java.lang.String)
 */
public long getDateHeader(String name) {
    return req.getDateHeader(name);
}

/* (non-Javadoc)
 * @see javax.servlet.http.HttpServletRequest#getHeader(java.lang.String)
 */
public String getHeader(String name) {
    return req.getHeader(name);
}

/* (non-Javadoc)
 * @see javax.servlet.http.HttpServletRequest#getHeaderNames()
 */
@SuppressWarnings("unchecked")
public Enumeration getHeaderNames() {
    return req.getHeaderNames();
}

/* (non-Javadoc)
 * @see javax.servlet.http.HttpServletRequest#getHeaders(java.lang.String)
 */
@SuppressWarnings("unchecked")
public Enumeration getHeaders(String name) {
    return req.getHeaders(name);
}

/* (non-Javadoc)
 * @see javax.servlet.http.HttpServletRequest#getIntHeader(java.lang.String)
 */
public int getIntHeader(String name) {
    return req.getIntHeader(name);
}

/* (non-Javadoc)
 * @see javax.servlet.http.HttpServletRequest#getMethod()
 */
public String getMethod() {
    return req.getMethod();
}

/* (non-Javadoc)
 * @see javax.servlet.http.HttpServletRequest#getPathInfo()
 */
public String getPathInfo() {
    return req.getPathInfo();
}

/* (non-Javadoc)
 * @see javax.servlet.http.HttpServletRequest#getPathTranslated()
 */
public String getPathTranslated() {
    return req.getPathTranslated();
}

/* (non-Javadoc)
 * @see javax.servlet.http.HttpServletRequest#getQueryString()
 */
public String getQueryString() {
    return req.getQueryString();
}

/* (non-Javadoc)
 * @see javax.servlet.http.HttpServletRequest#getRemoteUser()
 */
public String getRemoteUser() {
    return req.getRemoteUser();
}

/* (non-Javadoc)
 * @see javax.servlet.http.HttpServletRequest#getRequestURI()
 */
public String getRequestURI() {
    return req.getRequestURI();
}

/* (non-Javadoc)
 * @see javax.servlet.http.HttpServletRequest#getRequestURL()
 */
public StringBuffer getRequestURL() {
    return req.getRequestURL();
}

/* (non-Javadoc)
 * @see javax.servlet.http.HttpServletRequest#getRequestedSessionId()
 */
public String getRequestedSessionId() {
    return req.getRequestedSessionId();
}

/* (non-Javadoc)
 * @see javax.servlet.http.HttpServletRequest#getServletPath()
 */
public String getServletPath() {
    return req.getServletPath();
}

/* (non-Javadoc)
 * @see javax.servlet.http.HttpServletRequest#getSession()
 */
public HttpSession getSession() {
    return req.getSession();
}

/* (non-Javadoc)
 * @see javax.servlet.http.HttpServletRequest#getSession(boolean)
 */
public HttpSession getSession(boolean create) {
    return req.getSession(create);
}

/* (non-Javadoc)
 * @see javax.servlet.http.HttpServletRequest#getUserPrincipal()
 */
public Principal getUserPrincipal() {
    return req.getUserPrincipal();
}

/* (non-Javadoc)
 * @see javax.servlet.http.HttpServletRequest#isRequestedSessionIdFromCookie()
 */
public boolean isRequestedSessionIdFromCookie() {
    return req.isRequestedSessionIdFromCookie();
}

/* (non-Javadoc)
 * @see javax.servlet.http.HttpServletRequest#isRequestedSessionIdFromURL()
 */
public boolean isRequestedSessionIdFromURL() {
    return req.isRequestedSessionIdFromURL();
}

/* (non-Javadoc)
 * @see javax.servlet.http.HttpServletRequest#isRequestedSessionIdFromUrl()
 */
@SuppressWarnings("deprecation")
public boolean isRequestedSessionIdFromUrl() {
    return req.isRequestedSessionIdFromUrl();
}

/* (non-Javadoc)
 * @see javax.servlet.http.HttpServletRequest#isRequestedSessionIdValid()
 */
public boolean isRequestedSessionIdValid() {
    return req.isRequestedSessionIdValid();
}

/* (non-Javadoc)
 * @see javax.servlet.http.HttpServletRequest#isUserInRole(java.lang.String)
 */
public boolean isUserInRole(String role) {
    return req.isUserInRole(role);
}

/* (non-Javadoc)
 * @see javax.servlet.ServletRequest#getAttribute(java.lang.String)
 */
public Object getAttribute(String name) {
    return req.getAttribute(name);
}

/* (non-Javadoc)
 * @see javax.servlet.ServletRequest#getAttributeNames()
 */
@SuppressWarnings("unchecked")
public Enumeration getAttributeNames() {
    return req.getAttributeNames();
}

/* (non-Javadoc)
 * @see javax.servlet.ServletRequest#getCharacterEncoding()
 */
public String getCharacterEncoding() {
    return req.getCharacterEncoding();
}

/* (non-Javadoc)
 * @see javax.servlet.ServletRequest#getContentLength()
 */
public int getContentLength() {
    return req.getContentLength();
}

/* (non-Javadoc)
 * @see javax.servlet.ServletRequest#getContentType()
 */
public String getContentType() {
    return req.getContentType();
}

/* (non-Javadoc)
 * @see javax.servlet.ServletRequest#getLocalAddr()
 */
public String getLocalAddr() {
    return req.getLocalAddr();
}

/* (non-Javadoc)
 * @see javax.servlet.ServletRequest#getLocalName()
 */
public String getLocalName() {
    return req.getLocalName();
}

/* (non-Javadoc)
 * @see javax.servlet.ServletRequest#getLocalPort()
 */
public int getLocalPort() {
    return req.getLocalPort();
}

/* (non-Javadoc)
 * @see javax.servlet.ServletRequest#getLocale()
 */
public Locale getLocale() {
    return req.getLocale();
}

/* (non-Javadoc)
 * @see javax.servlet.ServletRequest#getLocales()
 */
@SuppressWarnings("unchecked")
public Enumeration getLocales() {
    return req.getLocales();
}

/* (non-Javadoc)
 * @see javax.servlet.ServletRequest#getProtocol()
 */
public String getProtocol() {
    return req.getProtocol();
}

/* (non-Javadoc)
 * @see javax.servlet.ServletRequest#getRealPath(java.lang.String)
 */
@SuppressWarnings("deprecation")
public String getRealPath(String path) {
    return req.getRealPath(path);
}

/* (non-Javadoc)
 * @see javax.servlet.ServletRequest#getRemoteAddr()
 */
public String getRemoteAddr() {
    return req.getRemoteAddr();
}

/* (non-Javadoc)
 * @see javax.servlet.ServletRequest#getRemoteHost()
 */
public String getRemoteHost() {
    return req.getRemoteHost();
}

/* (non-Javadoc)
 * @see javax.servlet.ServletRequest#getRemotePort()
 */
public int getRemotePort() {
    return req.getRemotePort();
}

/* (non-Javadoc)
 * @see javax.servlet.ServletRequest#getRequestDispatcher(java.lang.String)
 */
public RequestDispatcher getRequestDispatcher(String path) {
    return req.getRequestDispatcher(path);
}

/* (non-Javadoc)
 * @see javax.servlet.ServletRequest#getScheme()
 */
public String getScheme() {
    return req.getScheme();
}

/* (non-Javadoc)
 * @see javax.servlet.ServletRequest#getServerName()
 */
public String getServerName() {
    return req.getServerName();
}

/* (non-Javadoc)
 * @see javax.servlet.ServletRequest#getServerPort()
 */
public int getServerPort() {
    return req.getServerPort();
}

/* (non-Javadoc)
 * @see javax.servlet.ServletRequest#isSecure()
 */
public boolean isSecure() {
    return req.isSecure();
}

/* (non-Javadoc)
 * @see javax.servlet.ServletRequest#removeAttribute(java.lang.String)
 */
public void removeAttribute(String name) {
    req.removeAttribute(name);
}

/* (non-Javadoc)
 * @see javax.servlet.ServletRequest#setAttribute(java.lang.String, java.lang.Object)
 */
public void setAttribute(String name, Object value) {
    req.setAttribute(name, value);
}

/* (non-Javadoc)
 * @see javax.servlet.ServletRequest#setCharacterEncoding(java.lang.String)
 */
public void setCharacterEncoding(String env)
        throws UnsupportedEncodingException {
    req.setCharacterEncoding(env);
}

}
于 2009-05-07T14:13:07.517 回答
3

我想将此作为评论发布,但我没有足够的代表。您的解决方案是不够的,因为 ServletInputStreamWrapper 将返回负整数。例如,模拟一个输入编码为 UTF-16 的请求,无论是大端还是小端。输入可能以指示字节顺序的字节顺序标记开始,并且在测试我的语句时请构造模拟请求内容以执行此操作。 http://en.wikipedia.org/wiki/Byte_order_mark#UTF-16 这些 BOM 中的任何一个都包含 0xFF 字节。由于 java 没有无符号字节,所以这个 0xFF 作为 -1 返回。要解决这个问题,只需像这样更改读取功能

    public int read() throws IOException {
        if (index == data.length) {
            return -1;
        }
        return data[index++] & 0xff;
    }

我有点喜欢您的解决方案,因为它适用于 Spring。起初,我试图通过从 HttpServletRequestWrapper 扩展来消除您编写的一些委托代码。然而,Spring 做了一些有趣的事情:当它遇到 ServletRequestWrapper 类型的请求时,它会解开它,调用 getRequest()。问题是我的 getRequest() 方法(从您的代码中复制)返回一个从 HttpServletRequestWrapper 扩展的新类...冲洗并无限重复。所以很遗憾地说,不使用接口就赢了!

于 2013-09-11T20:53:55.450 回答
1

您可以编写自己的 Servlet 过滤器,并希望确保它首先出现在链中。然后将 ServletRequest 对象包装在将在需要时处理重写的东西中。查看http://java.sun.com/products/servlet/Filters.html的 Programming Customized Requests and Responses 部分

- - - 更新 - - -

我肯定错过了什么。你说你可以读请求体,自己读参数。难道你不能确保你的过滤器是第一个,包装 ServletRequest 对象,读取,处理和存储参数,将你的请求对象传递到链上并提供你存储的参数而不是原始参数吗?

于 2009-03-25T12:09:10.730 回答