mirror of
https://github.com/continew-org/continew-starter.git
synced 2025-09-08 16:57:09 +08:00
fix(core): 修复 application/x-www-form-urlencoded 请求体数据无法在 Controller 层获取的问题
This commit is contained in:
@@ -16,89 +16,216 @@
|
||||
|
||||
package top.continew.starter.core.wrapper;
|
||||
|
||||
import cn.hutool.core.io.IoUtil;
|
||||
import jakarta.servlet.ReadListener;
|
||||
import jakarta.servlet.ServletInputStream;
|
||||
import jakarta.servlet.ServletRequest;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletRequestWrapper;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.util.FastByteArrayOutputStream;
|
||||
import org.springframework.util.StreamUtils;
|
||||
import top.continew.starter.core.constant.StringConstants;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.*;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 可重复读取请求体的包装器
|
||||
* 支持文件流直接透传,非文件流可重复读取
|
||||
* 可重复读取请求体的包装器 支持文件流直接透传,非文件流可重复读取
|
||||
* <p>
|
||||
* 虽然这里可以多次读取流里面的数据, 但是建议还是调用getContentAsString()/getCachedContent() 方法, 已经把内容缓存在内存中了。
|
||||
* </p>
|
||||
*
|
||||
* @author echo
|
||||
* @since 2.10.0
|
||||
* @author Jasmine
|
||||
* @since 2.12.1
|
||||
*/
|
||||
public class RepeatReadRequestWrapper extends HttpServletRequestWrapper {
|
||||
|
||||
private byte[] cachedBody;
|
||||
private final HttpServletRequest originalRequest;
|
||||
/**
|
||||
* 缓存内容
|
||||
*/
|
||||
private final FastByteArrayOutputStream cachedContent;
|
||||
|
||||
/*** 用于缓存输入流 */
|
||||
private ContentCachingInputStream contentCachingInputStream;
|
||||
|
||||
/**
|
||||
* 字符编码
|
||||
*/
|
||||
private final String characterEncoding;
|
||||
|
||||
// private BufferedReader reader;
|
||||
|
||||
/**
|
||||
* Constructs a request object wrapping the given request.
|
||||
*
|
||||
* @param request the {@link HttpServletRequest} to be wrapped.
|
||||
* @throws IllegalArgumentException if the request is null
|
||||
*/
|
||||
public RepeatReadRequestWrapper(HttpServletRequest request) throws IOException {
|
||||
super(request);
|
||||
this.originalRequest = request;
|
||||
|
||||
this.characterEncoding = request.getCharacterEncoding() != null
|
||||
? request.getCharacterEncoding()
|
||||
: StandardCharsets.UTF_8.name();
|
||||
int contentLength = super.getRequest().getContentLength();
|
||||
cachedContent = (contentLength > 0)
|
||||
? new FastByteArrayOutputStream(contentLength)
|
||||
: new FastByteArrayOutputStream();
|
||||
// 判断是否为文件上传请求
|
||||
if (!isMultipartContent(request)) {
|
||||
this.cachedBody = IoUtil.readBytes(request.getInputStream(), false);
|
||||
if (isFormRequest()) {
|
||||
writeRequestParametersToCachedContent();
|
||||
} else {
|
||||
StreamUtils.copy(request.getInputStream(), cachedContent);
|
||||
}
|
||||
contentCachingInputStream = new ContentCachingInputStream(cachedContent.toByteArray());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServletInputStream getInputStream() throws IOException {
|
||||
// 如果是文件上传,直接返回原始输入流
|
||||
if (isMultipartContent(originalRequest)) {
|
||||
return originalRequest.getInputStream();
|
||||
if (isMultipartContent(super.getRequest())) {
|
||||
return super.getRequest().getInputStream();
|
||||
}
|
||||
synchronized (this) {
|
||||
contentCachingInputStream.reset();
|
||||
return contentCachingInputStream;
|
||||
}
|
||||
|
||||
// 非文件上传,返回可重复读取的输入流
|
||||
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(cachedBody);
|
||||
|
||||
return new ServletInputStream() {
|
||||
@Override
|
||||
public boolean isFinished() {
|
||||
return byteArrayInputStream.available() == 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReady() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setReadListener(ReadListener readListener) {
|
||||
// 非阻塞I/O,这里可以根据需要实现
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() {
|
||||
return byteArrayInputStream.read();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public BufferedReader getReader() throws IOException {
|
||||
// 如果是文件上传,直接返回原始Reader
|
||||
if (isMultipartContent(originalRequest)) {
|
||||
new BufferedReader(new InputStreamReader(originalRequest.getInputStream(), StandardCharsets.UTF_8));
|
||||
if (isMultipartContent(super.getRequest())) {
|
||||
return super.getRequest().getReader();
|
||||
}
|
||||
return new BufferedReader(new InputStreamReader(getInputStream()));
|
||||
|
||||
// BufferedReader不支持多次reset()(除非手动调用 mark() 并控制其生命周期),最安全的方式是每次调用getReader()时基于缓存内容重新创建一个新的BufferedReader实例。
|
||||
synchronized (this) {
|
||||
return new BufferedReader(new InputStreamReader(getInputStream(), getCharacterEncoding()));
|
||||
}
|
||||
}
|
||||
|
||||
private void writeRequestParametersToCachedContent() {
|
||||
try {
|
||||
if (this.cachedContent.size() == 0) {
|
||||
Map<String, String[]> form = super.getParameterMap();
|
||||
for (Iterator<String> nameIterator = form.keySet().iterator(); nameIterator.hasNext();) {
|
||||
String name = nameIterator.next();
|
||||
List<String> values = Arrays.asList(form.get(name));
|
||||
for (Iterator<String> valueIterator = values.iterator(); valueIterator.hasNext();) {
|
||||
String value = valueIterator.next();
|
||||
this.cachedContent.write(URLEncoder.encode(name, characterEncoding).getBytes());
|
||||
if (value != null) {
|
||||
this.cachedContent.write(StringConstants.EQUALS.getBytes());
|
||||
this.cachedContent.write(URLEncoder.encode(value, characterEncoding).getBytes());
|
||||
if (valueIterator.hasNext()) {
|
||||
this.cachedContent.write(StringConstants.AMP.getBytes());
|
||||
}
|
||||
}
|
||||
}
|
||||
if (nameIterator.hasNext()) {
|
||||
this.cachedContent.write(StringConstants.AMP.getBytes());
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("Failed to write request parameters to cached content", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCharacterEncoding() {
|
||||
return this.characterEncoding;
|
||||
}
|
||||
|
||||
public String getContentAsString() {
|
||||
return this.cachedContent.toString(Charset.forName(getCharacterEncoding()));
|
||||
}
|
||||
|
||||
public FastByteArrayOutputStream getCachedContent() {
|
||||
return cachedContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为文件上传请求
|
||||
* 判断当前请求是否为 multipart/form-data 类型的文件上传请求。 该类型一般用于表单上传文件的场景,例如 enctype="multipart/form-data"。
|
||||
*
|
||||
* @param request 请求对象
|
||||
* @return 是否为文件上传请求
|
||||
* @param request 当前 HTTP 请求对象
|
||||
* @return true 表示为 multipart 文件上传请求;否则为 false
|
||||
*/
|
||||
public boolean isMultipartContent(HttpServletRequest request) {
|
||||
return request.getContentType() != null && request.getContentType().toLowerCase().startsWith("multipart/");
|
||||
public boolean isMultipartContent(ServletRequest request) {
|
||||
String contentType = request.getContentType();
|
||||
return contentType != null && contentType.toLowerCase().startsWith("multipart/");
|
||||
}
|
||||
|
||||
private boolean isFormRequest() {
|
||||
String contentType = getContentType();
|
||||
return (contentType != null && contentType.contains(MediaType.APPLICATION_FORM_URLENCODED_VALUE));
|
||||
}
|
||||
|
||||
/**
|
||||
* Body 缓存的ServletInputStream实现 DefaultServerRequestBuilder.BodyInputStream
|
||||
*/
|
||||
private static class ContentCachingInputStream extends ServletInputStream {
|
||||
|
||||
private final InputStream delegate;
|
||||
|
||||
public ContentCachingInputStream(byte[] body) {
|
||||
this.delegate = new ByteArrayInputStream(body);
|
||||
}
|
||||
|
||||
public boolean isFinished() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isReady() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public void setReadListener(ReadListener readListener) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
public int read() throws IOException {
|
||||
return this.delegate.read();
|
||||
}
|
||||
|
||||
public int read(byte[] b, int off, int len) throws IOException {
|
||||
return this.delegate.read(b, off, len);
|
||||
}
|
||||
|
||||
public int read(byte[] b) throws IOException {
|
||||
return this.delegate.read(b);
|
||||
}
|
||||
|
||||
public long skip(long n) throws IOException {
|
||||
return this.delegate.skip(n);
|
||||
}
|
||||
|
||||
public int available() throws IOException {
|
||||
return this.delegate.available();
|
||||
}
|
||||
|
||||
public void close() throws IOException {
|
||||
this.delegate.close();
|
||||
}
|
||||
|
||||
public synchronized void mark(int readlimit) {
|
||||
this.delegate.mark(readlimit);
|
||||
}
|
||||
|
||||
public synchronized void reset() throws IOException {
|
||||
this.delegate.reset();
|
||||
}
|
||||
|
||||
public boolean markSupported() {
|
||||
return this.delegate.markSupported();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user