【问题标题】:Can no longer obtain form data from HttpServletRequest SpringBoot 2.2, Jersey 2.29无法再从 HttpServletRequest SpringBoot 2.2、Jersey 2.29 获取表单数据
【发布时间】:2020-12-10 18:35:45
【问题描述】:

我们有一个 SpringBoot 应用程序,并且正在使用 Jersey 来审核传入的 HTTP 请求。

我们实现了一个 Jersey ContainerRequestFilter 来检索传入的 HttpServletRequest 并使用 HttpServletRequest 的 getParameterMap() 方法提取查询和表单数据并将其放入我们的审计中。

这与 getParameterMap() 的 javadoc 一致:

"请求参数是与请求一起发送的额外信息。对于 HTTP servlet,参数包含在查询字符串中或发布 表单数据。”

这里是与过滤器有关的文档:

https://eclipse-ee4j.github.io/jersey.github.io/documentation/latest/user-guide.html#filters-and-interceptors

更新SpringBoot后发现getParameterMap()不再返回表单数据,但还是返回了查询数据。

我们发现 SpringBoot 2.1 是支持我们代码的最后一个版本。在 SpringBoot 2.2 中,Jersey 的版本更新为 2.29,但在查看发行说明后,我们没有看到任何与此相关的内容。

发生了什么变化?为了支持 SpringBoot 2.2 / Jersey 2.29,我们需要进行哪些更改?

这是我们代码的简化版本:

JerseyRequestFilter - 我们的过滤器

import javax.annotation.Priority;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.Priorities;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.container.ResourceInfo;
import javax.ws.rs.core.Context;
import javax.ws.rs.ext.Provider;
...

@Provider
@Priority(Priorities.AUTHORIZATION)
public class JerseyRequestFilter implements ContainerRequestFilter {

    @Context
    private ResourceInfo resourceInfo;

    @Context
    private HttpServletRequest httpRequest;
    ...
    
    public void filter(ContainerRequestContext context) throws IOException {
        ...
        requestData =  new RequestInterceptorModel(context, httpRequest, resourceInfo);
        ...
    }   
    ...
}   

RequestInterceptorModel - 地图不填充表单数据,仅查询数据

import lombok.Data;
import org.glassfish.jersey.server.ContainerRequest;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ResourceInfo;
...

@Data
public class RequestInterceptorModel {

    private Map<String, String[]> parameterMap;
    ...
    
    public RequestInterceptorModel(ContainerRequestContext context, HttpServletRequest httpRequest, ResourceInfo resourceInfo) throws AuthorizationException, IOException {
        ...
        setParameterMap(httpRequest.getParameterMap());
        ...
    }
    ...     
}

JerseyConfig - 我们的配置

import com.xyz.service.APIService;
import io.swagger.jaxrs.config.BeanConfig;
import io.swagger.jaxrs.listing.ApiListingResource;
import io.swagger.jaxrs.listing.SwaggerSerializers;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.wadl.internal.WadlResource;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
...

@Component
public class JerseyConfig extends ResourceConfig {
    ...

    public JerseyConfig() {
        this.register(APIService.class);
        ...
        // Access through /<Jersey's servlet path>/application.wadl
        this.register(WadlResource.class);
        this.register(AuthFilter.class);
        this.register(JerseyRequestFilter.class);
        this.register(JerseyResponseFilter.class);
        this.register(ExceptionHandler.class);
        this.register(ClientAbortExceptionWriterInterceptor.class);
    }

    @PostConstruct
    public void init() 
        this.configureSwagger();
    }

    private void configureSwagger() {
        ...
    }
}

完整示例

以下是使用我们的示例项目重新创建的步骤:

  1. 在此处从 github 下载源代码:
 git clone https://github.com/fei0x/so-jerseyBodyIssue
  1. 使用 pom.xml 文件导航到项目目录
  2. 运行项目:
 mvn -Prun
  1. 在新终端中运行以下 curl 命令来测试 Web 服务
  curl -X POST \
  http://localhost:8012/api/jerseyBody/ping \
  -H 'content-type: application/x-www-form-urlencoded' \
  -d param=Test%20String
  1. 在日志中您会看到表单参数
  2. 停止正在运行的项目,ctrl-C
  3. 将 pom 的父版本更新为 SpringBoot 的较新版本
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.15.RELEASE</version>

<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.9.RELEASE</version>
  1. 再次运行项目:
 mvn -Prun
  1. 再次调用 curl 调用:
  curl -X POST \
  http://localhost:8012/api/jerseyBody/ping \
  -H 'content-type: application/x-www-form-urlencoded' \
  -d param=Test%20String
  1. 这次日志会缺少表单参数

【问题讨论】:

  • 是否可以创建minimal reproducible example 并将其发布到 github 上?只有bare_minimum_ 才能重现问题。我想玩弄它。
  • @PaulSamsotha 我已经用示例项目和重现步骤更新了问题。谢谢。

标签: spring-boot jersey jax-rs jersey-2.0 spring-jersey


【解决方案1】:

好的,经过大量调试代码和挖掘 github 存储库后,我发现了以下内容:

有一个过滤器,如果它是POST request,它会读取请求的主体输入流,使其无法用于进一步使用。这是HiddenHttpMethodFilter。但是,此过滤器会将正文的内容(如果是 application/x-www-form-urlencoded)放入请求 parameterMap 中。

查看这个 github 问题:https://github.com/spring-projects/spring-framework/issues/21439

这个过滤器在spring-boot 2.1.X中默认是激活的。

由于这种行为在大多数情况下是不需要的,因此创建了一个属性来启用/禁用它,并且在 spring-boot 2.2.X 中默认禁用它。

由于您的代码依赖于此过滤器,您可以通过以下属性启用它:

spring.mvc.hiddenmethod.filter.enabled=true

我在本地对其进行了测试,它对我有用。

编辑:

这是使该解决方案起作用的原因:

HiddenHttpMethodFilter 来电

protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {

    HttpServletRequest requestToUse = request;

    if ("POST".equals(request.getMethod()) && request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) == null) {
        String paramValue = request.getParameter(this.methodParam);
    ...

request.getParameter 检查参数是否已被解析,如果不是,则执行此操作。
此时请求体输入流还没有被调用,所以请求体也要解析:
org.apache.catalina.connector.Request#parseParameters

protected void parseParameters() {

    parametersParsed = true;

    Parameters parameters = coyoteRequest.getParameters();
    boolean success = false;
    try {
        ...
        // this is the bit that parses the actual query parameters
        parameters.handleQueryParameters();
            
        // here usingInputStream is false, and so the body is parsed aswell
        if (usingInputStream || usingReader) {
            success = true;
            return;
        }
        ... // the actual body parsing is done here 

问题是,在这种情况下usingInputStream 是错误的,因此该方法在解析查询参数后不会返回。 usingInputStream 仅在第一次检索请求正文的输入流时设置为 true。这只有在我们离开 filterChain 的末端并为请求提供服务之后才能完成。当jersey初始化org.glassfish.jersey.servlet.WebComponent#initContainerRequest中的ContainerRequest时调用inputStream

private void initContainerRequest(
            final ContainerRequest requestContext,
            final HttpServletRequest servletRequest,
            final HttpServletResponse servletResponse,
            final ResponseWriter responseWriter) throws IOException {

    requestContext.setEntityStream(servletRequest.getInputStream());
    ...

Request#getInputStream

public ServletInputStream getInputStream() throws IOException {
    ...
    usingInputStream = true;
    ...

由于HiddenHttpMethodFilter 是访问参数的唯一过滤器,如果没有此过滤器,则在我们在RequestInterceptorModel 中调用request.getParameterMap() 之前,永远不会解析参数。但是此时请求体的inputStream已经被访问过,所以

【讨论】:

  • 您的解决方案有效,但我不太清楚为什么。您的声明“这是OrderedHiddenHttpMethodFilter。但是,此过滤器将正文的内容,如果它是application / x-www-form-urlencoded放入请求参数映射中”,我实际上并没有在源代码的任何地方看到这个。我只是想了解为什么会这样。还有你的声明“因为你的代码依赖于这个过滤器”,我不太确定。该应用程序不需要过滤器的用途。 HttpServletRequestWrapper 是否重置了流或其他东西。我对这个解决方案感到困惑。
  • 您链接到的问题也解释了过滤器如何读取输入流,“使其无法进一步使用”,就像您说的那样。为了解决这个问题,他们所做的只是添加一个属性来禁用过滤器。没有按照您的建议添加将主体放入 paramMap 的代码。所以我不明白为什么 adding 过滤器在这里起作用。同样,您的解决方案有效,但我正在努力找出原因。不是你的问题,只是想知道你是否有一些答案。
  • @PaulSamsotha 你完全正确,我只是假设 OP 依赖于这段代码,如果不是这种情况,那么你的答案应该是去。我还添加了一些解释为什么会在我的答案中发生这种情况,如果这仍然不能解决任何问题,请告诉我
  • @AmirSchnell 是的,尽管这确实有效。我们也看到了 Paul 的担忧,并认为最好尽可能坚持 Spring 的默认设置。感谢您的帮助。
【解决方案2】:

即使@Amir Schnell already posted a working solution,我也会发布这个答案。原因是我不太确定为什么该解决方案有效。当然,我宁愿有一个只需要向属性文件添加属性的解决方案,而不是像我的解决方案那样更改代码。但我不确定我是否对与我的逻辑认为它应该工作的方式相反的解决方案感到满意。这就是我的意思。在您当前的代码(SBv 2.1.15)中,如果您提出请求,请查看日志,您将看到 Jersey 日志

2020-12-15 11:43:04.858 WARN 5045 --- [nio-8012-exec-1] o.g.j.s.WebComponent :对 URI http://localhost:8012/api/jerseyBody/ping 的 servlet 请求包含请求正文中的表单参数,但请求正文已被 servlet 或访问请求参数的 servlet 过滤器使用。只有使用 @FormParam 的资源方法才能按预期工作。通过其他方式消耗请求正文的资源方法将无法按预期工作。

这是 Jersey 的一个已知问题,我在这里看到一些人问他们为什么无法从 HttpServletRequest 获取参数(此消息几乎总是在他们的日志中)。但是,在您的应用程序中,即使已记录,您也可以获取参数。只有在升级您的 SB 版本之后,然后没有看到日志,参数不可用。所以你明白我为什么感到困惑了。

这是另一个不需要弄乱过滤器的解决方案。你可以做的是使用与 Jersey 相同的方法来获取@FormParams。只需将以下方法添加到您的 RequestInterceptorModel 类中

private static Map<String, String[]> getFormParameterMap(ContainerRequestContext context) {
    Map<String, String[]> paramMap = new HashMap<>();
    ContainerRequest request = (ContainerRequest) context;
    if (MediaTypes.typeEqual(MediaType.APPLICATION_FORM_URLENCODED_TYPE, request.getMediaType())) {
        request.bufferEntity();
        Form form = request.readEntity(Form.class);
        MultivaluedMap<String, String> multiMap = form.asMap();
        multiMap.forEach((key, list) -> paramMap.put(key, list.toArray(new String[0])));
    }
    return paramMap;
}

您根本不需要HttpServletRequest。现在您可以通过调用此方法来设置参数映射

setParameterMap(getFormParameterMap(context));

希望有人能解释这个莫名其妙的案例。

【讨论】:

  • 是的,我们也很困惑。这个解决方案似乎运行良好,没有偏离 Springs 的默认值,我们认为这是更可取的。谢谢。
  • 不客气。这个解决方案很酷的是,默认情况下,Jersey 将查询字符串参数转换为 POST 表单参数。因此,您将获得与使用 HttpServletRequest 时相同的行为,并且不需要任何进一步的修改。如果需要,您还可以设置 property 以禁用此行为。
猜你喜欢
  • 1970-01-01
  • 2020-11-04
  • 2011-10-14
  • 1970-01-01
  • 1970-01-01
  • 2021-04-29
  • 2017-02-07
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多