fix: 用StreamingResponseBody手动控制Content-Type,彻底解决charset注入问题

This commit is contained in:
MaDaLei 2026-04-17 20:27:09 +08:00
parent d7d585ed42
commit fd6bf7c071

View File

@ -3,18 +3,13 @@ package com.petstore.controller;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.HashMap; import java.util.HashMap;
import java.util.Locale; import java.util.Locale;
@ -66,42 +61,55 @@ public class FileController {
} }
@GetMapping("/image/**") @GetMapping("/image/**")
public ResponseEntity<Resource> getImage(HttpServletRequest request) throws IOException { public void getImage(HttpServletRequest request, HttpServletResponse response) throws IOException {
String path = request.getRequestURI().replace("/api/upload/image", ""); String path = request.getRequestURI().replace("/api/upload/image", "");
String basePath = uploadPath.endsWith("/") ? uploadPath : uploadPath + "/"; String basePath = uploadPath.endsWith("/") ? uploadPath : uploadPath + "/";
File file = new File(basePath + path); File file = new File(basePath + path);
if (!file.exists()) { if (!file.exists()) {
return ResponseEntity.notFound().build(); response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
} }
String contentType = Files.probeContentType(file.toPath()); String contentType = Files.probeContentType(file.toPath());
if (contentType == null) contentType = "image/jpeg"; if (contentType == null) contentType = "image/jpeg";
// 去掉可能的 charset 参数避免 video Content-Type 被设为 "video/mp4;charset=UTF-8" // 去掉可能的 charset 参数
int semi = contentType.indexOf(';'); int semi = contentType.indexOf(';');
if (semi >= 0) contentType = contentType.substring(0, semi).trim(); if (semi >= 0) contentType = contentType.substring(0, semi).trim();
// 直接用 header 设置 Content-Type绕过 Spring Accept-Charset 逻辑 // 手动设置响应头彻底掌控 Content-Type绕过 Spring ResourceHttpMessageConverter
return ResponseEntity.ok() response.setContentType(contentType);
.header("Content-Type", contentType) response.setContentLengthLong(file.length());
.body(new FileSystemResource(file)); response.setHeader("Accept-Ranges", "bytes");
// 支持范围请求HTTP 206video 播放器需要
response.setHeader("Content-Disposition", "inline");
try (var in = Files.newInputStream(file.toPath());
var out = response.getOutputStream()) {
in.transferTo(out);
out.flush();
}
} }
// 兼容旧路径/2026/04/01/xxx.jpg // 兼容旧路径/2026/04/01/xxx.jpg
@GetMapping("/legacy/**") @GetMapping("/legacy/**")
public ResponseEntity<Resource> getLegacyImage(HttpServletRequest request) throws IOException { public void getLegacyImage(HttpServletRequest request, HttpServletResponse response) throws IOException {
String path = request.getRequestURI().replace("/api/upload/legacy", ""); String path = request.getRequestURI().replace("/api/upload/legacy", "");
String basePath = uploadPath.endsWith("/") ? uploadPath : uploadPath + "/"; String basePath = uploadPath.endsWith("/") ? uploadPath : uploadPath + "/";
File file = new File(basePath + path); File file = new File(basePath + path);
if (!file.exists()) { if (!file.exists()) {
return ResponseEntity.notFound().build(); response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
} }
String contentType = Files.probeContentType(file.toPath()); String contentType = Files.probeContentType(file.toPath());
if (contentType == null) contentType = "image/jpeg"; if (contentType == null) contentType = "image/jpeg";
// 去掉可能的 charset 参数避免 video Content-Type 被设为 "video/mp4;charset=UTF-8"
int semi = contentType.indexOf(';'); int semi = contentType.indexOf(';');
if (semi >= 0) contentType = contentType.substring(0, semi).trim(); if (semi >= 0) contentType = contentType.substring(0, semi).trim();
// 直接用 header 设置 Content-Type绕过 Spring Accept-Charset 逻辑 response.setContentType(contentType);
return ResponseEntity.ok() response.setContentLengthLong(file.length());
.header("Content-Type", contentType) response.setHeader("Accept-Ranges", "bytes");
.body(new FileSystemResource(file)); response.setHeader("Content-Disposition", "inline");
try (var in = Files.newInputStream(file.toPath());
var out = response.getOutputStream()) {
in.transferTo(out);
out.flush();
}
} }
/** produces 显式 UTF-8避免网关/客户端按 ISO-8859-1 解码导致 message 中文乱码 */ /** produces 显式 UTF-8避免网关/客户端按 ISO-8859-1 解码导致 message 中文乱码 */