fei*_*i0x 5 jax-rs jersey jersey-2.0 spring-boot spring-jersey
我们有一个 SpringBoot 应用程序,并使用 Jersey 来审核传入的 HTTP 请求。
我们实现了 Jersey ContainerRequestFilter来检索传入的HttpServletRequest 并使用 HttpServletRequest 的getParameterMap()方法来提取查询和表单数据并将其放入我们的审核中。
这与 getParameterMap() 的 javadoc 一致:
“请求参数是随请求发送的额外信息。对于 HTTP servlet,参数包含在查询字符串或发布的表单数据中。”
这是与过滤器相关的文档:
更新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);
...
}
...
}
Run Code Online (Sandbox Code Playgroud)
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());
...
}
...
}
Run Code Online (Sandbox Code Playgroud)
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() {
...
}
}
Run Code Online (Sandbox Code Playgroud)
完整示例
以下是使用我们的示例项目重新创建的步骤:
Run Code Online (Sandbox Code Playgroud)git clone https://github.com/fei0x/so-jerseyBodyIssue
Run Code Online (Sandbox Code Playgroud)mvn -Prun
Run Code Online (Sandbox Code Playgroud)curl -X POST \ http://localhost:8012/api/jerseyBody/ping \ -H 'content-type: application/x-www-form-urlencoded' \ -d param=Test%20String
Run Code Online (Sandbox Code Playgroud)<groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.15.RELEASE</version>
到
Run Code Online (Sandbox Code Playgroud)<groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.2.9.RELEASE</version>
Run Code Online (Sandbox Code Playgroud)mvn -Prun
Run Code Online (Sandbox Code Playgroud)curl -X POST \ http://localhost:8012/api/jerseyBody/ping \ -H 'content-type: application/x-www-form-urlencoded' \ -d param=Test%20String
好吧,经过大量调试代码并挖掘 github 存储库后,我发现了以下内容:
有一个过滤器,如果请求的正文输入流是 a POST request,则它会读取该请求的正文输入流,使其无法进一步使用。这是HiddenHttpMethodFilter. 但是,此过滤器会将正文的内容(如果它位于application/x-www-form-urlencodedrequests 中)放入其中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
Run Code Online (Sandbox Code Playgroud)
我在本地进行了测试,它对我有用。
编辑:
该解决方案的工作原理如下:
HiddenHttpMethodFilter调用
spring.mvc.hiddenmethod.filter.enabled=true
Run Code Online (Sandbox Code Playgroud)
request.getParameter检查参数是否已经被解析,如果没有则解析。
此时,请求体输入流还没有被调用,所以请求体也要解析:
org.apache.catalina.connector.Request#parseParameters
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);
...
Run Code Online (Sandbox Code Playgroud)
问题是,usingInputStream在这种情况下是 false,因此该方法在解析查询参数后不会返回。
usingInputStream仅当第一次检索请求正文的输入流时才设置为 true。只有在我们脱离过滤器链的末端并为请求提供服务之后才会完成此操作。ContainerRequest当 jersey初始化org.glassfish.jersey.servlet.WebComponent#initContainerRequest时,将调用 inputStream
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
Run Code Online (Sandbox Code Playgroud)
public ServletInputStream getInputStream() throws IOException {
...
usingInputStream = true;
...
Run Code Online (Sandbox Code Playgroud)
由于是访问参数的唯一过滤器,如果没有此过滤器,则在我们调用HiddenHttpMethodFilter之前永远不会解析参数。但此时请求体的inputStream已经被访问了,所以request.getParameterMap()RequestInterceptorModel
我将发布这个答案,尽管@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 获取 s 相同的方法@FormParam。只需将以下方法添加到您的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;
}
Run Code Online (Sandbox Code Playgroud)
HttpServletRequest你根本不需要这个。现在您可以通过调用此方法来设置参数映射
setParameterMap(getFormParameterMap(context));
Run Code Online (Sandbox Code Playgroud)
希望有人能解释这个令人困惑的案例。
| 归档时间: |
|
| 查看次数: |
1791 次 |
| 最近记录: |