Больше не удается получить данные формы из HttpServletRequest SpringBoot 2.2, Jersey 2.29.

У нас есть приложение SpringBoot, и мы используем Джерси для аудита входящих HTTP-запросов.

Мы внедрили фильтр Jersey ContainerRequestFilter для получения входящего HttpServletRequest и использовали метод getParameterMap() HttpServletRequest для извлечения данных запроса и формы и помещения их в наш аудит.

Это соответствует javadoc для getParameterMap():

Параметры запроса — это дополнительная информация, отправляемая вместе с запросом. Для сервлетов HTTP параметры содержатся в строке запроса или опубликованных данных формы.

И вот документация, относящаяся к фильтру:

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. скачать исходники с гитхаба здесь:
 git clone https://github.com/fei0x/so-jerseyBodyIssue
  1. перейдите в каталог проекта с файлом pom.xml
  2. запустить проект с помощью:
 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. в логе вы увидите параметры формы
  2. остановить запущенный проект, ctrl-C
  3. обновить родительскую версию pom до более новой версии SpringBoot
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.15.RELEASE</version>

to

<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. На этот раз в журнале будут отсутствовать параметры формы

person fei0x    schedule 10.12.2020    source источник
comment
Можно ли сделать минимально воспроизводимый пример и опубликовать его на github? Только голый_минимум_ для воспроизведения проблемы. Я хотел бы поиграть с ним.   -  person Paul Samsotha    schedule 13.12.2020
comment
@PaulSamsotha Я обновил вопрос, указав пример проекта и шаги для воспроизведения. Спасибо.   -  person fei0x    schedule 14.12.2020


Ответы (2)


Я опубликую этот ответ, хотя @Amir Schnell уже опубликовал рабочее решение. Причина в том, что я не совсем уверен, почему это решение работает. Определенно, я бы предпочел решение, которое просто требует добавления свойства в файл свойств, а не изменение кода, как это делает мое решение. Но я не уверен, устраивает ли меня решение, которое работает противоположно тому, как моя логика видит, что оно должно работать. Вот что я имею в виду. В вашем текущем коде (SBv 2.1.15), если вы сделаете запрос, посмотрите журнал, и вы увидите журнал Джерси.

2020-12-15 11:43:04.858 ПРЕДУПРЕЖДЕНИЕ 5045 --- [nio-8012-exec-1] ogjsWebComponent : запрос сервлета на URI http://localhost:8012/api/jerseyBody/ping содержит параметры формы в теле запроса, но тело запроса было использовано сервлет или фильтр сервлета, получающий доступ к параметрам запроса. Только методы ресурсов, использующие @FormParam, будут работать должным образом. Методы ресурса, потребляющие тело запроса другими способами, не будут работать должным образом.

Это была известная проблема с Джерси, и я видел здесь несколько человек, которые спрашивали, почему они не могут получить параметры из HttpServletRequest (это сообщение почти всегда находится в их журнале). Однако в вашем приложении, несмотря на то, что это зарегистрировано, вы можете получить параметры. Только после обновления вашей версии SB, а затем не просмотра журнала, параметры недоступны. Итак, вы видите, почему я в замешательстве.

Вот еще одно решение, которое не требует возиться с фильтрами. Что вы можете сделать, так это использовать тот же метод, который использует Джерси для получения @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));

Надеюсь, кто-то может объяснить этот сбивающий с толку случай.

person Paul Samsotha    schedule 15.12.2020
comment
Да, мы тоже в недоумении. Это решение, кажется, работает хорошо, не отклоняясь от значений по умолчанию Springs, что мы считаем предпочтительным. Спасибо. - person fei0x; 16.12.2020
comment
Пожалуйста. Отличительной особенностью этого решения является то, что по умолчанию Jersey преобразует параметры строки запроса в параметры формы POST. Таким образом, вы получаете то же поведение, что и при работе с HttpServletRequest, и вам не нужны дальнейшие модификации. Существует также свойство вы можете отключить это поведение, если вам когда-либо понадобится. - person Paul Samsotha; 16.12.2020

Хорошо, после тонны кода отладки и копания в репозиториях 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 и обработаем запрос. inputStream вызывается, когда трикотаж инициализирует ContainerRequest в org.glassfish.jersey.servlet.WebComponent#initContainerRequest

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 является единственным фильтром для доступа к параметрам, без этого фильтра параметры никогда не анализируются, пока мы не вызовем request.getParameterMap() в RequestInterceptorModel. Но в это время уже был получен доступ к inputStream тела запроса, и поэтому он

person Amir Schnell    schedule 15.12.2020
comment
Ваше решение работает, но я не совсем уверен, почему. Ваше утверждение Это фильтр OrderedHiddenHttpMethodFilter. Этот фильтр, однако, помещает содержимое тела, если оно закодировано в application/x-www-form-urlencoded, в карту параметров запроса, на самом деле я нигде этого не вижу в исходном коде. Я просто пытаюсь понять, почему это работает. Также ваше утверждение Поскольку ваш код использует этот фильтр, я не слишком уверен в этом. Приложению не нужно то, для чего создан фильтр. Сбрасывает ли HttpServletRequestWrapper поток или что-то в этом роде. Я сбит с толку этим решением. - person Paul Samsotha; 15.12.2020
comment
Также проблема, с которой вы связались, объясняла, как фильтр считывал входной поток, делая его непригодным для дальнейшего использования, как вы сказали. Чтобы решить эту проблему, все, что они сделали, это добавили свойство для отключения фильтра. Не было добавлено кода, помещающего тело в paramMap, как вы предлагаете. Поэтому я не понимаю, почему здесь работает добавление фильтра. Опять же, ваше решение работает, но я ломаю голову, пытаясь понять, почему. Не твоя проблема, просто интересно, есть ли у тебя ответы. - person Paul Samsotha; 15.12.2020
comment
@PaulSamsotha, вы совершенно правы, я просто предположил, что ОП полагался на этот фрагмент кода, если это не так, то ваш ответ должен быть первым. Также я добавил некоторое объяснение того, почему это происходит в моем ответе, дайте мне знать, если это все еще не устраняет какие-либо проблемы. - person Amir Schnell; 16.12.2020
comment
@AmirSchnell да, хотя на самом деле это работает. Мы также видели опасения Пола и считаем, что лучше придерживаться значений Spring по умолчанию, где это возможно. Спасибо за помощь. - person fei0x; 16.12.2020