2

Tomcat 系列篇十二-番外介绍下 Tomcat 的上传文件限制

 8 months ago
source link: https://nicksxs.me/2023/12/17/Tomcat-%E7%B3%BB%E5%88%97%E7%AF%87%E5%8D%81%E4%BA%8C-%E7%95%AA%E5%A4%96%E4%BB%8B%E7%BB%8D%E4%B8%8B-Tomcat-%E7%9A%84%E4%B8%8A%E4%BC%A0%E6%96%87%E4%BB%B6%E9%99%90%E5%88%B6/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Tomcat 系列篇十二-番外介绍下 Tomcat 的上传文件限制

最近同学在把 springboot 升级到 2.x 版本的过程中碰到了小问题,可能升级变更里能找到信息,不过我们以学习为目的,可以看看代码是怎么样的
报错是在这段代码里的
org.apache.tomcat.util.http.fileupload.util.LimitedInputStream#checkLimit

private void checkLimit() throws IOException {
    if (count > sizeMax) {
        raiseError(sizeMax, count);
    }
}

其中的 raiseError 是个抽象方法

protected abstract void raiseError(long pSizeMax, long pCount)
            throws IOException;

具体的实现是在

public FileItemStreamImpl(FileItemIteratorImpl pFileItemIterator, String pName, String pFieldName, String pContentType, boolean pFormField, long pContentLength) throws FileUploadException, IOException {
        this.fileItemIteratorImpl = pFileItemIterator;
        this.name = pName;
        this.fieldName = pFieldName;
        this.contentType = pContentType;
        this.formField = pFormField;
        long fileSizeMax = this.fileItemIteratorImpl.getFileSizeMax();
        if (fileSizeMax != -1L && pContentLength != -1L && pContentLength > fileSizeMax) {
            FileSizeLimitExceededException e = new FileSizeLimitExceededException(String.format("The field %s exceeds its maximum permitted size of %s bytes.", this.fieldName, fileSizeMax), pContentLength, fileSizeMax);
            e.setFileName(pName);
            e.setFieldName(pFieldName);
            throw new FileUploadIOException(e);
        } else {
            final MultipartStream.ItemInputStream itemStream = this.fileItemIteratorImpl.getMultiPartStream().newInputStream();
            InputStream istream = itemStream;
            if (fileSizeMax != -1L) {
                istream = new LimitedInputStream(itemStream, fileSizeMax) {
                    protected void raiseError(long pSizeMax, long pCount) throws IOException {
                        itemStream.close(true);
                        FileSizeLimitExceededException e = new FileSizeLimitExceededException(String.format("The field %s exceeds its maximum permitted size of %s bytes.", FileItemStreamImpl.this.fieldName, pSizeMax), pCount, pSizeMax);
                        e.setFieldName(FileItemStreamImpl.this.fieldName);
                        e.setFileName(FileItemStreamImpl.this.name);
                        throw new FileUploadIOException(e);
                    }
                };
            }

            this.stream = (InputStream)istream;
        }
    }

后面也会介绍到,这里我们其实主要是要找到这个 pSizeMax 是哪里来的
通过阅读代码会发现跟这个类 MultipartConfigElement 有关系
而在升级后的 springboot 中这个类已经有了自动装配类,也就是
org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration

有了这个自动装配

@Configuration(
    proxyBeanMethods = false
)
@ConditionalOnClass({Servlet.class, StandardServletMultipartResolver.class, MultipartConfigElement.class})
@ConditionalOnProperty(
    prefix = "spring.servlet.multipart",
    name = {"enabled"},
    matchIfMissing = true
)
@ConditionalOnWebApplication(
    type = Type.SERVLET
)
@EnableConfigurationProperties({MultipartProperties.class})
public class MultipartAutoConfiguration {
    private final MultipartProperties multipartProperties;

    public MultipartAutoConfiguration(MultipartProperties multipartProperties) {
        this.multipartProperties = multipartProperties;
    }

    @Bean
    @ConditionalOnMissingBean({MultipartConfigElement.class, CommonsMultipartResolver.class})
    public MultipartConfigElement multipartConfigElement() {
        return this.multipartProperties.createMultipartConfig();
    }

而这个 MultipartProperties 类中

@ConfigurationProperties(
    prefix = "spring.servlet.multipart",
    ignoreUnknownFields = false
)
public class MultipartProperties {
    private boolean enabled = true;
    private String location;
    private DataSize maxFileSize = DataSize.ofMegabytes(1L);
    private DataSize maxRequestSize = DataSize.ofMegabytes(10L);
    private DataSize fileSizeThreshold = DataSize.ofBytes(0L);
    private boolean resolveLazily = false;

并且在前面 createMultipartConfig 中就使用了这个maxFileSize 的默认值

public MultipartConfigElement createMultipartConfig() {
    MultipartConfigFactory factory = new MultipartConfigFactory();
    PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
    map.from(this.fileSizeThreshold).to(factory::setFileSizeThreshold);
    map.from(this.location).whenHasText().to(factory::setLocation);
    map.from(this.maxRequestSize).to(factory::setMaxRequestSize);
    map.from(this.maxFileSize).to(factory::setMaxFileSize);
    return factory.createMultipartConfig();
}

而在 org.apache.catalina.connector.Request#parseParts 中,会判断 mce 的配置

private void parseParts(boolean explicit) {

        // 省略一部分代码

            ServletFileUpload upload = new ServletFileUpload();
            upload.setFileItemFactory(factory);
            upload.setFileSizeMax(mce.getMaxFileSize());
            upload.setSizeMax(mce.getMaxRequestSize());

            parts = new ArrayList<>();
            try {
                List<FileItem> items =
                        upload.parseRequest(new ServletRequestContext(this));
                int maxPostSize = getConnector().getMaxPostSize();
                int postSize = 0;
                Charset charset = getCharset();

主要 org.apache.tomcat.util.http.fileupload.FileUploadBase#parseRequest

public List<FileItem> parseRequest(final RequestContext ctx)
            throws FileUploadException {
        final List<FileItem> items = new ArrayList<>();
        boolean successful = false;
        try {
            final FileItemIterator iter = getItemIterator(ctx);
            final FileItemFactory fileItemFactory = Objects.requireNonNull(getFileItemFactory(),
                    "No FileItemFactory has been set.");
            final byte[] buffer = new byte[Streams.DEFAULT_BUFFER_SIZE];
            while (iter.hasNext()) {
                final FileItemStream item = iter.next();
                // Don't use getName() here to prevent an InvalidFileNameException.
                final String fileName = item.getName();
                final FileItem fileItem = fileItemFactory.createItem(item.getFieldName(), item.getContentType(),
                                                   item.isFormField(), fileName);
                items.add(fileItem);
                try {
                    Streams.copy(item.openStream(), fileItem.getOutputStream(), true, buffer);
                } catch (final FileUploadIOException e) {

其中 org.apache.tomcat.util.http.fileupload.FileUploadBase#getItemIterator

public FileItemIterator getItemIterator(final RequestContext ctx)
throws FileUploadException, IOException {
    try {
        return new FileItemIteratorImpl(this, ctx);
    } catch (final FileUploadIOException e) {
        // unwrap encapsulated SizeException
        throw (FileUploadException) e.getCause();
    }
}

这里就创建了 org.apache.tomcat.util.http.fileupload.impl.FileItemIteratorImpl

public FileItemIteratorImpl(final FileUploadBase fileUploadBase, final RequestContext requestContext)
    throws FileUploadException, IOException {
    this.fileUploadBase = fileUploadBase;
    sizeMax = fileUploadBase.getSizeMax();
    fileSizeMax = fileUploadBase.getFileSizeMax();
    ctx = Objects.requireNonNull(requestContext, "requestContext");
    skipPreamble = true;
    findNextItem();
}

内部使用了前面给 upload 设置的文件大小上限 upload.setFileSizeMax(mce.getMaxFileSize());

然后在 findNextItem 里执行了初始化

private boolean findNextItem() throws FileUploadException, IOException {
        if (eof) {
            return false;
        }
        if (currentItem != null) {
            currentItem.close();
            currentItem = null;
        }
        final MultipartStream multi = getMultiPartStream();
        for (;;) {
            final boolean nextPart;
            if (skipPreamble) {
                nextPart = multi.skipPreamble();
            } else {
                nextPart = multi.readBoundary();
            }
            if (!nextPart) {
                if (currentFieldName == null) {
                    // Outer multipart terminated -> No more data
                    eof = true;
                    return false;
                }
                // Inner multipart terminated -> Return to parsing the outer
                multi.setBoundary(multiPartBoundary);
                currentFieldName = null;
                continue;
            }
            final FileItemHeaders headers = fileUploadBase.getParsedHeaders(multi.readHeaders());
            if (currentFieldName == null) {
                // We're parsing the outer multipart
                final String fieldName = fileUploadBase.getFieldName(headers);
                if (fieldName != null) {
                    final String subContentType = headers.getHeader(FileUploadBase.CONTENT_TYPE);
                    if (subContentType != null
                            &&  subContentType.toLowerCase(Locale.ENGLISH)
                                    .startsWith(FileUploadBase.MULTIPART_MIXED)) {
                        currentFieldName = fieldName;
                        // Multiple files associated with this field name
                        final byte[] subBoundary = fileUploadBase.getBoundary(subContentType);
                        multi.setBoundary(subBoundary);
                        skipPreamble = true;
                        continue;
                    }
                    final String fileName = fileUploadBase.getFileName(headers);
                    currentItem = new FileItemStreamImpl(this, fileName,
                            fieldName, headers.getHeader(FileUploadBase.CONTENT_TYPE),
                            fileName == null, getContentLength(headers));
                    currentItem.setHeaders(headers);
                    progressNotifier.noteItem();
                    itemValid = true;
                    return true;
                }
            } else {
                final String fileName = fileUploadBase.getFileName(headers);
                if (fileName != null) {
                    currentItem = new FileItemStreamImpl(this, fileName,
                            currentFieldName,
                            headers.getHeader(FileUploadBase.CONTENT_TYPE),
                            false, getContentLength(headers));
                    currentItem.setHeaders(headers);
                    progressNotifier.noteItem();
                    itemValid = true;
                    return true;
                }
            }
            multi.discardBodyData();
        }
    }

这里面就会 new 这个 FileItemStreamImpl

currentItem = new FileItemStreamImpl(this, fileName,
                            fieldName, headers.getHeader(FileUploadBase.CONTENT_TYPE),
                            fileName == null, getContentLength(headers));

构造方法比较长

public FileItemStreamImpl(final FileItemIteratorImpl pFileItemIterator, final String pName, final String pFieldName,
            final String pContentType, final boolean pFormField,
            final long pContentLength) throws FileUploadException, IOException {
        fileItemIteratorImpl = pFileItemIterator;
        name = pName;
        fieldName = pFieldName;
        contentType = pContentType;
        formField = pFormField;
        final long fileSizeMax = fileItemIteratorImpl.getFileSizeMax();
        if (fileSizeMax != -1 && pContentLength != -1
                && pContentLength > fileSizeMax) {
            final FileSizeLimitExceededException e =
                    new FileSizeLimitExceededException(
                            String.format("The field %s exceeds its maximum permitted size of %s bytes.",
                                    fieldName, Long.valueOf(fileSizeMax)),
                            pContentLength, fileSizeMax);
            e.setFileName(pName);
            e.setFieldName(pFieldName);
            throw new FileUploadIOException(e);
        }
        // OK to construct stream now
        final ItemInputStream itemStream = fileItemIteratorImpl.getMultiPartStream().newInputStream();
        InputStream istream = itemStream;
        if (fileSizeMax != -1) {
            istream = new LimitedInputStream(istream, fileSizeMax) {
                @Override
                protected void raiseError(final long pSizeMax, final long pCount)
                        throws IOException {
                    itemStream.close(true);
                    final FileSizeLimitExceededException e =
                        new FileSizeLimitExceededException(
                            String.format("The field %s exceeds its maximum permitted size of %s bytes.",
                                   fieldName, Long.valueOf(pSizeMax)),
                            pCount, pSizeMax);
                    e.setFieldName(fieldName);
                    e.setFileName(name);
                    throw new FileUploadIOException(e);
                }
            };
        }
        stream = istream;
    }

fileSizeMax != 0 的时候就会初始化 LimitedInputStream,这就就是会在前面的

org.apache.tomcat.util.http.fileupload.FileUploadBase#parseRequest

Streams.copy(item.openStream(), fileItem.getOutputStream(), true, buffer);

这里的 item

final FileItemIterator iter = getItemIterator(ctx);
            final FileItemFactory fileItemFactory = Objects.requireNonNull(getFileItemFactory(),
                    "No FileItemFactory has been set.");
            final byte[] buffer = new byte[Streams.DEFAULT_BUFFER_SIZE];
            while (iter.hasNext()) {
                final FileItemStream item = iter.next();

调用了 FileItemIterator 迭代器的 next

@Override
    public FileItemStream next() throws FileUploadException, IOException {
        if (eof  ||  (!itemValid && !hasNext())) {
            throw new NoSuchElementException();
        }
        itemValid = false;
        return currentItem;
    }

这个 currentItem 就是前面 new 的 FileItemStreamImpl

然后在 Streams.copy 的时候调用 openStream 也就是 org.apache.tomcat.util.http.fileupload.impl.FileItemStreamImpl#openStream

@Override
    public InputStream openStream() throws IOException {
        if (((Closeable) stream).isClosed()) {
            throw new FileItemStream.ItemSkippedException();
        }
        return stream;
    }

这里的 stream 就是 FileItemStreamImpl 构造方法最后赋值的 stream,会在大小超过限制时抛出错误

而这个可以通过设置 properties 来修改,spring.servlet.multipart.max-file-size 和 spring.servlet.multipart.max-request-size

spring.servlet.multipart.max-file-size=100MB
spring.servlet.multipart.max-request-size=100MB

而老版本的 spring.http.multipart.maxFileSize
其实就是配置名称改了下,但是能看一下代码也是有点收获的。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK