Java 使用 servlet 过滤器修改请求参数

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/1413129/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-12 11:49:09  来源:igfitidea点击:

Modify request parameter with servlet filter

javaservlet-filters

提问by Jeremy Stein

An existing web application is running on Tomcat 4.1. There is an XSS issue with a page, but I can't modify the source. I've decided to write a servlet filter to sanitize the parameter before it is seen by the page.

现有的 Web 应用程序正在 Tomcat 4.1 上运行。页面存在 XSS 问题,但我无法修改源。我决定编写一个 servlet 过滤器来在页面看到参数之前对其进行清理。

I would like to write a Filter class like this:

我想写一个像这样的过滤器类:

import java.io.*;
import javax.servlet.*;

public final class XssFilter implements Filter {

  public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
      throws IOException, ServletException
  {
    String badValue = request.getParameter("dangerousParamName");
    String goodValue = sanitize(badValue);
    request.setParameter("dangerousParamName", goodValue);
    chain.doFilter(request, response);
  }

  public void destroy() {
  }

  public void init(FilterConfig filterConfig) {
  }
}

But ServletRequest.setParameterdoesn't exist.

ServletRequest.setParameter不存在。

How can I change the value of the request parameter before passing the request down the chain?

如何在将请求向下传递之前更改请求参数的值?

采纳答案by skaffman

As you've noted HttpServletRequestdoes not have a setParameter method. This is deliberate, since the class represents the request as it came from the client, and modifying the parameter would not represent that.

正如您所指出的HttpServletRequest,没有 setParameter 方法。这是故意的,因为该类表示来自客户端的请求,而修改参数并不代表该请求。

One solution is to use the HttpServletRequestWrapperclass, which allows you to wrap one request with another. You can subclass that, and override the getParametermethod to return your sanitized value. You can then pass that wrapped request to chain.doFilterinstead of the original request.

一种解决方案是使用HttpServletRequestWrapper类,它允许您将一个请求与另一个请求包装在一起。您可以将其子类化,并覆盖该getParameter方法以返回您的消毒值。然后,您可以将包装好的请求传递给chain.doFilter而不是原始请求。

It's a bit ugly, but that's what the servlet API says you should do. If you try to pass anything else to doFilter, some servlet containers will complain that you have violated the spec, and will refuse to handle it.

这有点难看,但这就是 servlet API 所说的你应该做的。如果您尝试将其他任何内容传递给doFilter,一些 servlet 容器会抱怨您违反了规范,并拒绝处理它。

A more elegant solution is more work - modify the original servlet/JSP that processes the parameter, so that it expects a request attributeinstead of a parameter. The filter examines the parameter, sanitizes it, and sets the attribute (using request.setAttribute) with the sanitized value. No subclassing, no spoofing, but does require you to modify other parts of your application.

更优雅的解决方案是更多的工作 - 修改处理参数的原始 servlet/JSP,使其期望请求属性而不是参数。过滤器检查参数,对其进行清理,并使用request.setAttribute清理后的值设置属性(使用)。没有子类化,没有欺骗,但确实需要您修改应用程序的其他部分。

回答by Asaph

Write a simple class that subcalsses HttpServletRequestWrapperwith a getParameter() method that returns the sanitized version of the input. Then pass an instance of your HttpServletRequestWrapperto Filter.doChain()instead of the request object directly.

编写一个简单的类,该类HttpServletRequestWrapper使用 getParameter() 方法进行子类,该方法返回输入的清理版本。然后通过你的实例HttpServletRequestWrapperFilter.doChain()直接代替请求对象。

回答by Jeremy Stein

For the record, here is the class I ended up writing:

作为记录,这是我最终编写的课程:

import java.io.IOException;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;

public final class XssFilter implements Filter {

    static class FilteredRequest extends HttpServletRequestWrapper {

        /* These are the characters allowed by the Javascript validation */
        static String allowedChars = "+-0123456789#*";

        public FilteredRequest(ServletRequest request) {
            super((HttpServletRequest)request);
        }

        public String sanitize(String input) {
            String result = "";
            for (int i = 0; i < input.length(); i++) {
                if (allowedChars.indexOf(input.charAt(i)) >= 0) {
                    result += input.charAt(i);
                }
            }
            return result;
        }

        public String getParameter(String paramName) {
            String value = super.getParameter(paramName);
            if ("dangerousParamName".equals(paramName)) {
                value = sanitize(value);
            }
            return value;
        }

        public String[] getParameterValues(String paramName) {
            String values[] = super.getParameterValues(paramName);
            if ("dangerousParamName".equals(paramName)) {
                for (int index = 0; index < values.length; index++) {
                    values[index] = sanitize(values[index]);
                }
            }
            return values;
        }
    }

    public void doFilter(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException {
        chain.doFilter(new FilteredRequest(request), response);
    }

    public void destroy() {
    }

    public void init(FilterConfig filterConfig) {
    }
}

回答by Ankur Bag

Try request.setAttribute("param",value);. It worked fine for me.

试试request.setAttribute("param",value);。它对我来说很好。

Please find this code sample:

请找到此代码示例:

private void sanitizePrice(ServletRequest request){
        if(request.getParameterValues ("price") !=  null){
            String price[] = request.getParameterValues ("price");

            for(int i=0;i<price.length;i++){
                price[i] = price[i].replaceAll("[^\dA-Za-z0-9- ]", "").trim();
                System.out.println(price[i]);
            }
            request.setAttribute("price", price);
            //request.getParameter("numOfBooks").re
        }
    }

回答by Ajinkya Peshave

You can use Regular Expressionfor Sanitization. Inside filter before calling chain.doFilter(request, response)method, call this code. Here is Sample Code:

您可以使用正则表达式进行清理。在调用chain.doFilter(request, response)方法之前的过滤器内部,调用此代码。这是示例代码:

for (Enumeration en = request.getParameterNames(); en.hasMoreElements(); ) {
String name = (String)en.nextElement();
String values[] = request.getParameterValues(name);
int n = values.length;
    for(int i=0; i < n; i++) {
     values[i] = values[i].replaceAll("[^\dA-Za-z ]","").replaceAll("\s+","+").trim();   
    }
}

回答by Julien Kronegg

I had the same problem (changing a parameter from the HTTP request in the Filter). I ended up by using a ThreadLocal<String>. In the FilterI have:

我遇到了同样的问题(从过滤器中的 HTTP 请求更改参数)。我最终使用了一个ThreadLocal<String>. 在Filter我有:

class MyFilter extends Filter {
    public static final ThreadLocal<String> THREAD_VARIABLE = new ThreadLocal<>();
    public void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) {
        THREAD_VARIABLE.set("myVariableValue");
        chain.doFilter(request, response);
    }
}

In my request processor (HttpServlet, JSF controller or any other HTTP request processor), I get the current thread value back:

在我的请求处理器(HttpServlet、JSF 控制器或任何其他 HTTP 请求处理器)中,我获取了当前线程值:

...
String myVariable = MyFilter.THREAD_VARIABLE.get();
...

Advantages:

好处:

  • more versatile than passing HTTP parameters (you can pass POJO objects)
  • slightly faster (no need to parse the URL to extract the variable value)
  • more elegant thant the HttpServletRequestWrapperboilerplate
  • the variable scope is wider than just the HTTP request (the scope you have when doing request.setAttribute(String,Object), i.e. you can access the variable in other filtrers.
  • 比传递 HTTP 参数更通用(你可以传递 POJO 对象)
  • 稍微快一点(不需要解析 URL 来提取变量值)
  • HttpServletRequestWrapper样板更优雅
  • 变量范围比 HTTP 请求范围更广(您在执行时拥有的范围request.setAttribute(String,Object),即您可以访问其他过滤器中的变量。

Disadvantages:

缺点:

  • You can use this method only when the thread which process the filter is the same as the one which process the HTTP request (this is the case in all Java-based servers I know). Consequently, this will not work when
    • doing a HTTP redirect(because the browser does a new HTTP request and there is no way to guarantee that it will be processed by the same thread)
    • processing data in separate threads, e.g. when using java.util.stream.Stream.parallel, java.util.concurrent.Future, java.lang.Thread.
  • You must be able to modify the request processor/application
  • 仅当处理过滤器的线程与处理 HTTP 请求的线程相同时才可以使用此方法(我知道的所有基于 Java 的服务器都是这种情况)。因此,这在以下情况下不起作用
    • 做一个 HTTP 重定向(因为浏览器做了一个新的 HTTP 请求,并没有办法保证它会被同一个线程处理)
    • 在单独的线程中处理数据,例如在使用java.util.stream.Stream.parallel, java.util.concurrent.Future, 时java.lang.Thread
  • 您必须能够修改请求处理器/应用程序

Some side notes:

一些旁注:

  • The server has a Thread pool to process the HTTP requests. Since this is pool:

    1. a Thread from this thread pool will process many HTTP requests, but only one at a time (so you need either to cleanup you variable after usage or to define it for each HTTP request = pay attention to code such as if (value!=null) { THREAD_VARIABLE.set(value);}because you will reuse the value from the previous HTTP request when valueis null : side effects are guaranteed).
    2. There is no guarantee that two requests will be processed by the same thread (it may be the case but you have no guarantee). If you need to keep user data from one request to another request, it would be better to use HttpSession.setAttribute()
  • The JEE @RequestScopedinternally uses a ThreadLocal, but using the ThreadLocalis more versatile: you can use it in non JEE/CDI containers (e.g. in multithreaded JRE applications)
  • 服务器有一个线程池来处理 HTTP 请求。由于这是池:

    1. 这个线程池中的一个线程将处理许多 HTTP 请求,但一次只能处理一个(因此您需要在使用后清理您的变量或为每个 HTTP 请求定义它=注意代码,例如if (value!=null) { THREAD_VARIABLE.set(value);}因为您将重用该值来自之前的 HTTP 请求 whenvalue为 null :保证副作用)。
    2. 不能保证两个请求将由同一个线程处理(可能是这种情况,但您不能保证)。如果您需要将用户数据从一个请求保留到另一个请求,最好使用HttpSession.setAttribute()
  • JEE@RequestScoped内部使用ThreadLocal,但使用ThreadLocal更通用:您可以在非 JEE/CDI 容器中使用它(例如在多线程 JRE 应用程序中)

回答by agent47

This is what i ended up doing

这就是我最终做的

//import ../../Constants;

public class RequestFilter implements Filter {

    private static final Logger logger = LoggerFactory.getLogger(RequestFilter.class);

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
        throws IOException, ServletException {
        try {
            CustomHttpServletRequest customHttpServletRequest = new CustomHttpServletRequest((HttpServletRequest) servletRequest);
            filterChain.doFilter(customHttpServletRequest, servletResponse);
        } finally {
            //do something here
        }
    }



    @Override
    public void destroy() {

    }

     public static Map<String, String[]> ADMIN_QUERY_PARAMS = new HashMap<String, String[]>() {
        {
            put("diagnostics", new String[]{"false"});
            put("skipCache", new String[]{"false"});
        }
    };

    /*
        This is a custom wrapper over the `HttpServletRequestWrapper` which 
        overrides the various header getter methods and query param getter methods.
        Changes to the request pojo are
        => A custom header is added whose value is a unique id
        => Admin query params are set to default values in the url
    */
    private class CustomHttpServletRequest extends HttpServletRequestWrapper {
        public CustomHttpServletRequest(HttpServletRequest request) {
            super(request);
            //create custom id (to be returned) when the value for a
            //particular header is asked for
            internalRequestId = RandomStringUtils.random(10, true, true) + "-local";
        }

        public String getHeader(String name) {
            String value = super.getHeader(name);
            if(Strings.isNullOrEmpty(value) && isRequestIdHeaderName(name)) {
                value = internalRequestId;
            }
            return value;
        }

        private boolean isRequestIdHeaderName(String name) {
            return Constants.RID_HEADER.equalsIgnoreCase(name) || Constants.X_REQUEST_ID_HEADER.equalsIgnoreCase(name);
        }

        public Enumeration<String> getHeaders(String name) {
            List<String> values = Collections.list(super.getHeaders(name));
            if(values.size()==0 && isRequestIdHeaderName(name)) {
                values.add(internalRequestId);
            }
            return Collections.enumeration(values);
        }

        public Enumeration<String> getHeaderNames() {
            List<String> names = Collections.list(super.getHeaderNames());
            names.add(Constants.RID_HEADER);
            names.add(Constants.X_REQUEST_ID_HEADER);
            return Collections.enumeration(names);
        }

        public String getParameter(String name) {
            if (ADMIN_QUERY_PARAMS.get(name) != null) {
                return ADMIN_QUERY_PARAMS.get(name)[0];
            }
            return super.getParameter(name);
        }

        public Map<String, String[]> getParameterMap() {
            Map<String, String[]> paramsMap = new HashMap<>(super.getParameterMap());
            for (String paramName : ADMIN_QUERY_PARAMS.keySet()) {
                if (paramsMap.get(paramName) != null) {
                    paramsMap.put(paramName, ADMIN_QUERY_PARAMS.get(paramName));
                }
            }
            return paramsMap;
        }

        public String[] getParameterValues(String name) {
            if (ADMIN_QUERY_PARAMS.get(name) != null) {
                return ADMIN_QUERY_PARAMS.get(name);
            }
            return super.getParameterValues(name);
        }

        public String getQueryString() {
            Map<String, String[]> map = getParameterMap();
            StringBuilder builder = new StringBuilder();
            for (String param: map.keySet()) {
                for (String value: map.get(param)) {
                    builder.append(param).append("=").append(value).append("&");
                }
            }
            builder.deleteCharAt(builder.length() - 1);
            return builder.toString();
        }
    }
}

回答by benGiacomo

Based on all your remarks here is my proposal that worked for me :

根据您的所有评论,我的建议对我有用:

 private final class CustomHttpServletRequest extends HttpServletRequestWrapper {

    private final Map<String, String[]> queryParameterMap;
    private final Charset requestEncoding;

    public CustomHttpServletRequest(HttpServletRequest request) {
        super(request);
        queryParameterMap = getCommonQueryParamFromLegacy(request.getParameterMap());

        String encoding = request.getCharacterEncoding();
        requestEncoding = (encoding != null ? Charset.forName(encoding) : StandardCharsets.UTF_8);
    }

    private final Map<String, String[]> getCommonQueryParamFromLegacy(Map<String, String[]> paramMap) {
        Objects.requireNonNull(paramMap);

        Map<String, String[]> commonQueryParamMap = new LinkedHashMap<>(paramMap);

        commonQueryParamMap.put(CommonQueryParams.PATIENT_ID, new String[] { paramMap.get(LEGACY_PARAM_PATIENT_ID)[0] });
        commonQueryParamMap.put(CommonQueryParams.PATIENT_BIRTHDATE, new String[] { paramMap.get(LEGACY_PARAM_PATIENT_BIRTHDATE)[0] });
        commonQueryParamMap.put(CommonQueryParams.KEYWORDS, new String[] { paramMap.get(LEGACY_PARAM_STUDYTYPE)[0] });

        String lowerDateTime = null;
        String upperDateTime = null;

        try {
            String studyDateTime = new SimpleDateFormat("yyyy-MM-dd").format(new SimpleDateFormat("dd-MM-yyyy").parse(paramMap.get(LEGACY_PARAM_STUDY_DATE_TIME)[0]));

            lowerDateTime = studyDateTime + "T23:59:59";
            upperDateTime = studyDateTime + "T00:00:00";

        } catch (ParseException e) {
            LOGGER.error("Can't parse StudyDate from query parameters : {}", e.getLocalizedMessage());
        }

        commonQueryParamMap.put(CommonQueryParams.LOWER_DATETIME, new String[] { lowerDateTime });
        commonQueryParamMap.put(CommonQueryParams.UPPER_DATETIME, new String[] { upperDateTime });

        legacyQueryParams.forEach(commonQueryParamMap::remove);
        return Collections.unmodifiableMap(commonQueryParamMap);

    }

    @Override
    public String getParameter(String name) {
        String[] params = queryParameterMap.get(name);
        return params != null ? params[0] : null;
    }

    @Override
    public String[] getParameterValues(String name) {
        return queryParameterMap.get(name);
    }

    @Override
    public Map<String, String[]> getParameterMap() {
            return queryParameterMap; // unmodifiable to uphold the interface contract.
        }

        @Override
        public Enumeration<String> getParameterNames() {
            return Collections.enumeration(queryParameterMap.keySet());
        }

        @Override
        public String getQueryString() {
            // @see : https://stackoverflow.com/a/35831692/9869013
            // return queryParameterMap.entrySet().stream().flatMap(entry -> Stream.of(entry.getValue()).map(value -> entry.getKey() + "=" + value)).collect(Collectors.joining("&")); // without encoding !!
            return queryParameterMap.entrySet().stream().flatMap(entry -> encodeMultiParameter(entry.getKey(), entry.getValue(), requestEncoding)).collect(Collectors.joining("&"));
        }

        private Stream<String> encodeMultiParameter(String key, String[] values, Charset encoding) {
            return Stream.of(values).map(value -> encodeSingleParameter(key, value, encoding));
        }

        private String encodeSingleParameter(String key, String value, Charset encoding) {
            return urlEncode(key, encoding) + "=" + urlEncode(value, encoding);
        }

        private String urlEncode(String value, Charset encoding) {
            try {
                return URLEncoder.encode(value, encoding.name());
            } catch (UnsupportedEncodingException e) {
                throw new IllegalArgumentException("Cannot url encode " + value, e);
            }
        }

        @Override
        public ServletInputStream getInputStream() throws IOException {
            throw new UnsupportedOperationException("getInputStream() is not implemented in this " + CustomHttpServletRequest.class.getSimpleName() + " wrapper");
        }

    }

note : queryString() requires to process ALL the values for each KEY and don't forget to encodeUrl() when adding your own param values, if required

注意:queryString() 需要处理每个 KEY 的所有值,如果需要,在添加自己的参数值时不要忘记 encodeUrl()

As a limitation, if you call request.getParameterMap() or any method that would call request.getReader() and begin reading, you will prevent any further calls to request.setCharacterEncoding(...)

作为限制,如果您调用 request.getParameterMap() 或任何会调用 request.getReader() 并开始读取的方法,您将阻止对 request.setCharacterEncoding(...) 的任何进一步调用