3

我们有一个 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. 这次日志会缺少表单参数
4

2 回答 2

3

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

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

请参阅此 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#initContainerRequestContainerRequest时调用 inputStream

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

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

请求#getInputStream

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

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

于 2020-12-15T08:45:46.440 回答
2

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

2020-12-15 11:43:04.858 WARN 5045 --- [nio-8012-exec-1] ogjsWebComponent:对 URI 的 servlet 请求http://localhost:8012/api/jerseyBody/ping在请求正文中包含表单参数,但请求正文已被 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));

希望有人可以解释这个令人费解的案例。

于 2020-12-15T20:03:56.373 回答