文章目录
前言
我那么大的InputStream呢...需求奇葩,不啰嗦,直接说问题:对请求体进行缓存时,发现ContentType=application/x-www-form-urlencoded
时InputSream不见了,越想越不对劲,我都缓存了,还能消失?
一、问题分析
我的SpringBoot版本
xml
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
废话不多说,直接说分析过程。。。
Spring配置
yml
spring:
mvc:
hidden-method: # 解析 urlencoded 类型的参数
filter:
enabled: true
java
@Bean
@ConditionalOnMissingBean(HiddenHttpMethodFilter.class)
@ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter", name = "enabled")
public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
return new OrderedHiddenHttpMethodFilter();
}
上边代码可以看出来,当spring.mvc.hiddenmethod.filter
中存在enabled
时会注册org.springframework.boot.web.servlet.filter.OrderedHiddenHttpMethodFilter
当有application/x-www-form-urlencoded
请求时,OrderedHiddenHttpMethodFilter
过滤器会被触发,通过DEFAULT_ORDER
变量可以看出来执行时机是比较靠前的。可能Spring Security
有什么对请求参数的分析动作,我没用过,不做分析。
java
public class OrderedHiddenHttpMethodFilter extends HiddenHttpMethodFilter implements OrderedFilter {
/**
* The default order is high to ensure the filter is applied before Spring Security.
*/
public static final int DEFAULT_ORDER = REQUEST_WRAPPER_FILTER_MAX_ORDER - 10000;
private int order = DEFAULT_ORDER;
@Override
public int getOrder() {
return this.order;
}
public void setOrder(int order) {
this.order = order;
}
}
它的父类org.springframework.web.filter.HiddenHttpMethodFilter
中有这么一段代码
java
@Override
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);
if (StringUtils.hasLength(paramValue)) {
String method = paramValue.toUpperCase(Locale.ENGLISH);
if (ALLOWED_METHODS.contains(method)) {
requestToUse = new HttpMethodRequestWrapper(request, method);
}
}
}
filterChain.doFilter(requestToUse, response);
}
这里String paramValue = request.getParameter(this.methodParam);
这句代码会执行request.getParameter("_method");
getParameter
代码逻辑在org.apache.catalina.connector.RequestFacade#getParameter
java
@Override
public String getParameter(String name) {
checkFacade();
if (Globals.IS_SECURITY_ENABLED) {
return AccessController.doPrivileged(new GetParameterPrivilegedAction(name));
} else {
return request.getParameter(name); // 这里执行 org.apache.catalina.connector.Request#getParameter
}
}
org.apache.catalina.connector.Request#getParameter
代码中判断,如果没有解析过参数,开始执行parseParameters
函数进行解析
java
/**
* @return the value of the specified request parameter, if any; otherwise, return <code>null</code>. If there is
* more than one value defined, return only the first one.
*
* @param name Name of the desired request parameter
*/
@Override
public String getParameter(String name) {
if (!parametersParsed) {
parseParameters();
}
return coyoteRequest.getParameters().getParameter(name);
}
parseParameters
代码在org.apache.catalina.connector.Request#parseParameters
,代码可以看出来当contentType=application/x-www-form-urlencoded
时就把InputStream
获取走了
java
protected void parseParameters() {
parametersParsed = true;
Parameters parameters = coyoteRequest.getParameters();
boolean success = false;
try {
// Set this every time in case limit has been changed via JMX
int maxParameterCount = getConnector().getMaxParameterCount();
if (parts != null && maxParameterCount > 0) {
maxParameterCount -= parts.size();
}
parameters.setLimit(maxParameterCount);
// getCharacterEncoding() may have been overridden to search for
// hidden form field containing request encoding
Charset charset = getCharset();
boolean useBodyEncodingForURI = connector.getUseBodyEncodingForURI();
parameters.setCharset(charset);
if (useBodyEncodingForURI) {
parameters.setQueryStringCharset(charset);
}
// Note: If !useBodyEncodingForURI, the query string encoding is
// that set towards the start of CoyoteAdapter.service()
parameters.handleQueryParameters();
if (usingInputStream || usingReader) {
success = true;
return;
}
String contentType = getContentType();
if (contentType == null) {
contentType = "";
}
int semicolon = contentType.indexOf(';');
if (semicolon >= 0) {
contentType = contentType.substring(0, semicolon).trim();
} else {
contentType = contentType.trim();
}
if ("multipart/form-data".equals(contentType)) {
parseParts(false);
success = true;
return;
}
if (!getConnector().isParseBodyMethod(getMethod())) {
success = true;
return;
}
if (!("application/x-www-form-urlencoded".equals(contentType))) {
success = true;
return;
}
int len = getContentLength();
if (len > 0) {
int maxPostSize = connector.getMaxPostSize();
if ((maxPostSize >= 0) && (len > maxPostSize)) {
Context context = getContext();
if (context != null && context.getLogger().isDebugEnabled()) {
context.getLogger().debug(sm.getString("coyoteRequest.postTooLarge"));
}
checkSwallowInput();
parameters.setParseFailedReason(FailReason.POST_TOO_LARGE);
return;
}
byte[] formData = null;
if (len < CACHED_POST_LEN) {
if (postData == null) {
postData = new byte[CACHED_POST_LEN];
}
formData = postData;
} else {
formData = new byte[len];
}
try {
readPostBodyFully(formData, len);
} catch (IOException e) {
// Client disconnect
Context context = getContext();
if (context != null && context.getLogger().isDebugEnabled()) {
context.getLogger().debug(sm.getString("coyoteRequest.parseParameters"), e);
}
parameters.setParseFailedReason(FailReason.CLIENT_DISCONNECT);
return;
}
parameters.processParameters(formData, 0, len);
} else if ("chunked".equalsIgnoreCase(coyoteRequest.getHeader("transfer-encoding"))) {
byte[] formData = null;
try {
formData = readChunkedPostBody();
} catch (IllegalStateException ise) {
// chunkedPostTooLarge error
parameters.setParseFailedReason(FailReason.POST_TOO_LARGE);
Context context = getContext();
if (context != null && context.getLogger().isDebugEnabled()) {
context.getLogger().debug(sm.getString("coyoteRequest.parseParameters"), ise);
}
return;
} catch (IOException e) {
// Client disconnect
parameters.setParseFailedReason(FailReason.CLIENT_DISCONNECT);
Context context = getContext();
if (context != null && context.getLogger().isDebugEnabled()) {
context.getLogger().debug(sm.getString("coyoteRequest.parseParameters"), e);
}
return;
}
if (formData != null) {
parameters.processParameters(formData, 0, formData.length);
}
}
success = true;
} finally {
if (!success) {
parameters.setParseFailedReason(FailReason.UNKNOWN);
}
}
}
二、解决方案
这不就好办了吗。。。。既然知道了Spring要解析参数,那我们就多余分析后边的代码(当然~多看一点多一点大局观),如果需求还是要对body进行操作,那就。。。
思路及3个类的代码
写一个过滤器,放到OrderedHiddenHttpMethodFilter
之前,application/x-www-form-urlencoded
请求时触发getParameter
提前缓存数据使InputSream
得以保留
这里因为我有对body进行修改的操作,所以我多余封装一层...如果你能看懂,建议你把我多余的代码去掉
创建CachedRequestWrapper.java
java
public class CachedRequestWrapper extends HttpServletRequestWrapper {
private byte[] bytes = null; // 因为我要setBody,所以这里又放了一个缓存
private BufferedReader reader;
public CachedRequestWrapper(ContentCachingRequestWrapper request) {
super(request);
cacheBody();
}
/**
* <p>
* 只在 contentType 为 application/x-www-form-urlencoded 时,才缓存请求体
*/
private void cacheBody() {
if (HttpMethod.GET.matches(getMethod())) {
return;
}
String contentType = getContentType();
if (contentType == null) {
contentType = "";
}
int semicolon = contentType.indexOf(';');
if (semicolon >= 0) {
contentType = contentType.substring(0, semicolon).trim();
} else {
contentType = contentType.trim();
}
if ("multipart/form-data".equals(contentType)) {
return;
}
if ("application/x-www-form-urlencoded".equals(contentType)) {
final ContentCachingRequestWrapper request = (ContentCachingRequestWrapper) getRequest();
request.getParameter(""); // 调用 ContentCachingRequestWrapper 解析参数并缓存
bytes = request.getContentAsByteArray(); // 再次缓存数据
return;
}
try {
bytes = IOUtils.toByteArray(getInputStream()); // 其它类型请求缓存数据
} catch (IOException e) {
log.error("缓存请求体出错", e);
}
}
public String getBody() {
if (null == bytes) {
return null;
}
return IOUtils.toString(bytes, getCharacterEncoding());
}
public byte[] getBytes() {
return bytes;
}
public void setBytes(byte[] bytes) throws Exception {
this.bytes = bytes;
}
@Override
public @NotNull String getParameter(String name) {
if (name.equals("request")) {
return requestBody;
}
return super.getParameter(name);
}
@Override
public @NotNull ServletInputStream getInputStream() throws IOException {
if (bytes != null) {
return new CachedServletInputStream(bytes);
}
return super.getInputStream();
}
@Override
public @NotNull BufferedReader getReader() throws IOException {
if (reader == null) {
reader = new BufferedReader(new InputStreamReader(this.getInputStream(), getCharacterEncoding()));
}
return reader;
}
}
创建CachedServletInputStream.java
java
public class CachedServletInputStream extends ServletInputStream {
private final ByteArrayInputStream inputStream;
public CachedServletInputStream(byte[] content) {
inputStream = new ByteArrayInputStream(content);
}
@Override
public int read() throws IOException {
return inputStream.read();
}
@Override
public boolean isFinished() {
return inputStream.available() == 0;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener readListener) {
throw new UnsupportedOperationException();
}
}
创建MyFilter.java,设置过滤器Order
过滤器使用CachedRequestWrapper
进行传递
java
@Component
public class MyFilter extends OncePerRequestFilter implements OrderedFilter {
@Override
protected void doFilterInternal(@NotNull HttpServletRequest httpReq, @NotNull HttpServletResponse httpRes, @NotNull FilterChain chain) throws IOException, ServletException {
chain.doFilter(
new CachedRequestWrapper(new ContentCachingRequestWrapper(request)),
new ContentCachingResponseWrapper(response));
}
@Override
public int getOrder() {
return OrderedHiddenHttpMethodFilter.DEFAULT_ORDER - 1;
}
}
是不是感觉OrderedFilter
似曾相识...没错就是从OrderedHiddenHttpMethodFilter
里边抄的
总结
- 开启
spring.mvc.hiddenmethod.filter.enable=true
后,当有application/x-www-form-urlencoded
请求时,过滤器会触发参数的解析,方便映射到方法的参数上 inputStream
只能被读取一次,业务不能再次使用,需要在使用之前就套上缓存
瑕疵很多,欢迎各位大佬提出宝贵意见