无法再从 HttpServletRequest SpringBoot 2.2、Jersey 2.29 获取表单数据
Can no longer obtain form data from HttpServletRequest SpringBoot 2.2, Jersey 2.29
我们有一个 SpringBoot 应用程序,正在使用 Jersey 来审核传入的 HTTP 请求。
我们实现了一个 Jersey ContainerRequestFilter 来检索传入的 HttpServletRequest
并使用 HttpServletRequest 的 getParameterMap() 方法来提取查询和表单数据并将其放入我们的审计中。
这与 getParameterMap() 的 javadoc 一致:
"Request parameters are extra information sent with the request. For
HTTP servlets, parameters are contained in the query string or posted
form data."
这里是与过滤器相关的文档:
更新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() {
...
}
}
完整示例
以下是使用我们的示例项目重新创建的步骤:
- 从 github 此处下载源代码:
git clone https://github.com/fei0x/so-jerseyBodyIssue
- 导航到包含 pom.xml 文件的项目目录
- 运行 项目:
mvn -Prun
- 在一个新的终端运行下面的curl命令来测试web服务
curl -X POST \
http://localhost:8012/api/jerseyBody/ping \
-H 'content-type: application/x-www-form-urlencoded' \
-d param=Test%20String
- 在日志中你会看到表单参数
- 停止 运行ning 项目,ctrl-C
- 将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>
- 运行再次项目:
mvn -Prun
- 再次调用 curl 调用:
curl -X POST \
http://localhost:8012/api/jerseyBody/ping \
-H 'content-type: application/x-www-form-urlencoded' \
-d param=Test%20String
- 这次日志将缺少表单参数
好吧,经过大量调试代码和挖掘 github 存储库后,我发现了以下内容:
有一个过滤器,如果它是 POST request
,它会读取请求的正文输入流,使其无法进一步使用。这就是HiddenHttpMethodFilter
。然而,这个过滤器将正文的内容放入请求 parameterMap
.
中,如果它是 application/x-www-form-urlencoded
查看此 github 问题:https://github.com/spring-projects/spring-framework/issues/21439
此过滤器在 spring-boot 2.1.X.
中默认处于活动状态
由于这种行为在大多数情况下是不需要的,因此为 enable/disable 创建了一个 属性 并使用 spring-boot 2.2.X 默认情况下已停用。
由于您的代码依赖于此过滤器,您可以通过以下 属性:
启用它
spring.mvc.hiddenmethod.filter.enabled=true
我在本地测试过它,它对我有用。
编辑:
以下是使该解决方案起作用的原因:
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
检查参数是否已经被解析,如果没有解析则执行。
此时请求body输入流还没有被调用,所以请求数字也要解析body:
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
是错误的,因此该方法在解析查询参数后不会 return。
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());
...
public ServletInputStream getInputStream() throws IOException {
...
usingInputStream = true;
...
因为 HiddenHttpMethodFilter
是访问参数的唯一过滤器,如果没有这个过滤器,参数永远不会被解析,直到我们在 RequestInterceptorModel
中调用 request.getParameterMap()
。但是此时request body的inputStream已经被访问了所以
我会 post 这个答案,尽管 。原因是我不太确定 为什么 该解决方案有效。当然,我宁愿有一个只需要将 属性 添加到 属性 文件的解决方案,而不是像我的解决方案那样必须更改代码。但我不确定我是否对与我的逻辑认为它应该工作的方式相反的解决方案感到满意。这就是我的意思。在您当前的代码(SBv 2.1.15)中,如果您发出请求,请查看日志,您将看到 Jersey 日志
2020-12-15 11:43:04.858 WARN 5045 --- [nio-8012-exec-1] o.g.j.s.WebComponent : A servlet request to the URI http://localhost:8012/api/jerseyBody/ping
contains form parameters in the request body but the request body has been consumed by the servlet or a servlet filter accessing the request parameters. Only resource methods using @FormParam will work as expected. Resource methods consuming the request body by other means will not work as expected.
这是 Jersey 的一个已知问题,我在这里看到一些人询问为什么他们无法从 HttpServletRequest 获取参数(此消息几乎总是在他们的日志中)。但是在您的应用程序中,即使已记录,您也可以获取参数。只是升级了你的SB版本,然后没有看日志,参数不可用。所以你明白我为什么困惑了。
这是另一个不需要弄乱过滤器的解决方案。您可以做的是使用 Jersey 用于获取 @FormParam
的相同方法。只需将以下方法添加到您的 RequestInterceptorModel
class
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));
希望有人能解释这个令人费解的案例。
我们有一个 SpringBoot 应用程序,正在使用 Jersey 来审核传入的 HTTP 请求。
我们实现了一个 Jersey ContainerRequestFilter 来检索传入的 HttpServletRequest 并使用 HttpServletRequest 的 getParameterMap() 方法来提取查询和表单数据并将其放入我们的审计中。
这与 getParameterMap() 的 javadoc 一致:
"Request parameters are extra information sent with the request. For HTTP servlets, parameters are contained in the query string or posted form data."
这里是与过滤器相关的文档:
更新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() {
...
}
}
完整示例
以下是使用我们的示例项目重新创建的步骤:
- 从 github 此处下载源代码:
git clone https://github.com/fei0x/so-jerseyBodyIssue
- 导航到包含 pom.xml 文件的项目目录
- 运行 项目:
mvn -Prun
- 在一个新的终端运行下面的curl命令来测试web服务
curl -X POST \ http://localhost:8012/api/jerseyBody/ping \ -H 'content-type: application/x-www-form-urlencoded' \ -d param=Test%20String
- 在日志中你会看到表单参数
- 停止 运行ning 项目,ctrl-C
- 将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>
- 运行再次项目:
mvn -Prun
- 再次调用 curl 调用:
curl -X POST \ http://localhost:8012/api/jerseyBody/ping \ -H 'content-type: application/x-www-form-urlencoded' \ -d param=Test%20String
- 这次日志将缺少表单参数
好吧,经过大量调试代码和挖掘 github 存储库后,我发现了以下内容:
有一个过滤器,如果它是 POST request
,它会读取请求的正文输入流,使其无法进一步使用。这就是HiddenHttpMethodFilter
。然而,这个过滤器将正文的内容放入请求 parameterMap
.
application/x-www-form-urlencoded
查看此 github 问题:https://github.com/spring-projects/spring-framework/issues/21439
此过滤器在 spring-boot 2.1.X.
中默认处于活动状态由于这种行为在大多数情况下是不需要的,因此为 enable/disable 创建了一个 属性 并使用 spring-boot 2.2.X 默认情况下已停用。
由于您的代码依赖于此过滤器,您可以通过以下 属性:
启用它spring.mvc.hiddenmethod.filter.enabled=true
我在本地测试过它,它对我有用。
编辑:
以下是使该解决方案起作用的原因:
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
检查参数是否已经被解析,如果没有解析则执行。
此时请求body输入流还没有被调用,所以请求数字也要解析body:
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
是错误的,因此该方法在解析查询参数后不会 return。
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());
...
public ServletInputStream getInputStream() throws IOException {
...
usingInputStream = true;
...
因为 HiddenHttpMethodFilter
是访问参数的唯一过滤器,如果没有这个过滤器,参数永远不会被解析,直到我们在 RequestInterceptorModel
中调用 request.getParameterMap()
。但是此时request body的inputStream已经被访问了所以
我会 post 这个答案,尽管
2020-12-15 11:43:04.858 WARN 5045 --- [nio-8012-exec-1] o.g.j.s.WebComponent : A servlet request to the URI
http://localhost:8012/api/jerseyBody/ping
contains form parameters in the request body but the request body has been consumed by the servlet or a servlet filter accessing the request parameters. Only resource methods using @FormParam will work as expected. Resource methods consuming the request body by other means will not work as expected.
这是 Jersey 的一个已知问题,我在这里看到一些人询问为什么他们无法从 HttpServletRequest 获取参数(此消息几乎总是在他们的日志中)。但是在您的应用程序中,即使已记录,您也可以获取参数。只是升级了你的SB版本,然后没有看日志,参数不可用。所以你明白我为什么困惑了。
这是另一个不需要弄乱过滤器的解决方案。您可以做的是使用 Jersey 用于获取 @FormParam
的相同方法。只需将以下方法添加到您的 RequestInterceptorModel
class
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));
希望有人能解释这个令人费解的案例。