Spring Boot 2.1 MultipartFile - 启用 SSL(https) 时文件上传几乎有 50% 的 http 400 错误请求

Got almost 50% http 400 bad request error on Spring Boot 2.1 MultipartFile - file upload when enable SSL(https)

我有一个 API 服务器,它在 public 域上使用 Spring Boot 2.1 构建,该域还提供 API 服务和文件上传。

最近几天,我们想升级此 Spring 引导服务器以使用 SSL (https)。 在我们设置 Spring Boot 中的 SSL 设置之前。文件上传的 API 非常好用(100% 上传成功)。

在 Spring Boot 中设置 SSL 设置之后。用于文件上传的 API 可以正常工作,但只有 50% 上传成功,其他 50% 收到 http 400 错误请求。 (我们确定问题与前端web无关,因为我们使用捆绑了Spring的Swagger启动测试可以得到相同的结果)

然后我们查找 Spring Boot 的服务器日志。当发生 http 400 错误请求时,没有任何关于 http 400 错误请求的日志。 我们研究了很多天,在网上进行了调查,但仍然无法解决这个问题。请给予帮助。

我们已经尝试禁用 csrf(在属性文件中或通过配置 class)和互联网上提供的许多其他解决方案,但仍然无效。

环境:Spring Boot 2.1.13(Spring Boot 2.1 的最新版本)

属性文件中的设置:(仅在属性文件中添加了 SSL 设置部分,并且 SSL (https) 已成功开启)

# SSL setup
server.ssl.key-store=classpath:keystore.p12
server.ssl.key-store-password=abcdef
server.ssl.key-store-type=PKCS12
server.ssl.key-alias=tomcat
server.ssl.enabled-protocols=TLSv1.1,TLSv1.2
server.http2.enabled=true
security.basic.enabled=false
security.enable-csrf=false

## MULTIPART (MultipartProperties)
# Enable multipart uploads
spring.servlet.multipart.enabled = true
# Threshold after which files are written to disk.
spring.servlet.multipart.file-size-threshold=2KB
# Max file size.
spring.servlet.multipart.max-file-size=100MB
# Max Request Size
spring.servlet.multipart.max-request-size=115MB

我的文件上传控制器:

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.SwaggerDefinition;
import io.swagger.annotations.Tag;
import java.util.Collections;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

@Slf4j
@RestController
@RequestMapping(value = "/v1/fileupload")
@Api(tags = {"fileupload api"}, value = "fileupload")
@SwaggerDefinition(tags = {
    @Tag(name = "fileupload api", description = "apis for file upload")
})
public class FileUploadController {

    @Autowired
    private FileUploadService fileUploadService;

    private ApiUtilHelper helper = new ApiUtilHelper();

    @ApiOperation(value = "upload single data import file")
    @RequestMapping(
        value = "/dataimport",
        method = RequestMethod.POST,,
        consumes = { MediaType.MULTIPART_FORM_DATA_VALUE },
        produces = { MediaType.APPLICATION_JSON_UTF8_VALUE }
    )
    public ResponseEntity<?> uploadSingleFileForDataImport(@RequestParam("file") MultipartFile file) throws FileStorageException {
        log.info("Enter into uploadSingleFileForDataImport");
        FileUploadResponse fileUploadResponse = fileUploadService.storeFile(file, "dataImport");
        Map<String, Object> additionals = Collections.singletonMap("filupload", fileUploadResponse);
        BasicResponse br = helper.createSuccessBaseResponse(ApiSuccessCode.CreateSuccess, additionals);
        return new ResponseEntity<BasicResponse>(br, ApiSuccessCode.CreateSuccess.getHttpStatus());
    }

Swagger 测试结果:

Request URL: https://example.com:8443/v1/fileupload/dataimport
Request Method: POST
Status Code: 400 
Remote Address: 111.222.111.222:8443
Referrer Policy: no-referrer-when-downgrade

**Response http header from Spring Boot**
Connection: close
Content-Length: 0
Date: Mon, 20 Apr 2020 13:13:02 GMT

**Request http header from Swagger**
accept: application/json;charset=UTF-8
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-TW,zh;q=0.9,en-US;q=0.8,en;q=0.7,zh-CN;q=0.6,ja;q=0.5
Connection: keep-alive
Content-Length: 484098
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryiXmuHnaNthhXowmb
Cookie: _ga=GA1.2.1299976434.1580821082; JSESSIONID=2C157019D6560405CC75A5F5083DE0AE
Host: example.com:8443
Origin: https://example.com:8443
Referer: https://example.com:8443/swagger-ui.html
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.113 Safari/537.36

2020.04.20 13:44(UTC时间)补充信息如下: 谢谢@nbalodi, 当我设置 logging.level.org.springframework.web=DEBUG 时。我现在得到了错误日志。附加日志如下:

HttpEntityMethodProcessor : No match for [application/json;charset=UTF-8], supported: []
ExceptionHandlerExceptionResolver : Resolved [org.springframework.web.multipart.support.MissingServletRequestPartException: Required request part 'file' is not present]
DispatcherServlet : Completed 400 BAD_REQUEST

奇怪的是,这种错误情况仅在我们使用上述 ssl 设置时才会发生。

是否可以将您的日志级别更改为 DEBUG?即 logging.level.org.springframework.web=DEBUG.

试试这个可能会有帮助。看不到您的代码有任何其他问题。

@RequestBody MultipartFile[] submissions

应该是

@RequestParam("file") MultipartFile[] submissions

文件不是请求 body,它们是请求的一部分,没有 built-in HttpMessageConverter 可以将请求转换为 MultiPartFile 的数组.

您也可以使用 MultipartHttpServletRequest,这样您就可以访问各个部分的 headers。

谢谢大家的回复。 我认为这是 SpringBoot 的错误或其在 SpringBoot v2.1.x 版本中嵌入 Tomcat 的错误。 SpringBoot v2.3.0正式版发布时。我使用相同的代码升级到 v2.3.0 现在一切都可行。我在文件上传中使用批量测试或称为压力测试。 现在已经100%成功了。

更新: 根本原因是 Tomcat 组件在 Spring 框架下不稳定 - 启用 https and/or http2 或 Spring 安全性(例如 OAuth2)下的多部分。

大多数情况下Spring Boot 2.3.0 - 2.3.2,文件上传失败率为4 ~ 14 ~ 50%。 Spring Boot 2.3.5到2.4.0版本后,文件上传失败率接近50%。 关键不同的是 Tomcat 版本和 Spring 安全版本在这些 Spring 引导版本中是不同的。

另一个问题是Tomcat支持的http2协议不稳定。 如果您启用 http2(例如在您的属性文件中设置服务器。http2.enabled=true)并使用 Multipart 或 HttpServletRequest 进行文件上传,失败率将上升到接近 90% 并获得连接。

结论: 对于遇到文件上传不稳定问题并得到 ERR_CONNECTION_CLOSED 并找到关于

的错误日志的任何人

org.apache.catalina.connector.ClientAbortException: org.apache.coyote.CloseNowException: 连接 [{0}], 流 [{1}], 此流不可写

原因:org.apache.coyote.CloseNowException:连接 [{0}]、流 [{1}]、此流不可写

原因:org.apache.coyote。http2.StreamException:连接 [{0}],流 [{1}],此流不可写

您可以尝试升级到 Spring Boot 2.4.0 并在 Spring Boot 中禁用 http2 支持 Tomcat 通过设置服务器。http2.enabled=false in您的属性文件

您可能会发现稳定性得到了提高,甚至达到了 100% 的成功率。 (我尝试并为所有客户实施)

如果禁用 http2 支持对您不可行。 您可以尝试降级到 Spring Boot 2.3.0 - 2.3.2 版本或通过 HttpServletRequest 实现文件上传(记得在 Spring Boot first 中禁用 Multipart)

您可能会找到相关的开发文档herethere