fix: remove error message leakage in LicenseController and ContractController

Replaced try-catch blocks returning e.getMessage() in HTTP 500 responses with proper ResponseStatusException propagation through global ApiExceptionHandler. Added file size (50MB) and MIME type whitelist validation to contract attachment upload.

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-05-27 08:36:53 +08:00
parent 0abb60fd2d
commit 25395a648b
2 changed files with 52 additions and 22 deletions
@@ -16,10 +16,13 @@ import jakarta.validation.Valid;
import jakarta.validation.constraints.Max; import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min; import jakarta.validation.constraints.Min;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.DeleteMapping;
@@ -34,6 +37,7 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.server.ResponseStatusException;
@RestController @RestController
@RequestMapping("/api/v1/contracts") @RequestMapping("/api/v1/contracts")
@@ -50,6 +54,16 @@ public class ContractController {
this.contractStatusTransitionService = contractStatusTransitionService; this.contractStatusTransitionService = contractStatusTransitionService;
} }
private static final long MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB
private static final Set<String> ALLOWED_CONTENT_TYPES = Set.of(
MediaType.APPLICATION_PDF_VALUE,
"image/jpeg", "image/png", "image/tiff",
"application/msword",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/vnd.ms-excel",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
);
@GetMapping @GetMapping
public PageResponse<ContractResponse> list( public PageResponse<ContractResponse> list(
@RequestParam(value = "page", defaultValue = "0") @Min(0) int page, @RequestParam(value = "page", defaultValue = "0") @Min(0) int page,
@@ -113,28 +127,44 @@ public class ContractController {
public ResponseEntity<Map<String, Object>> uploadAttachment( public ResponseEntity<Map<String, Object>> uploadAttachment(
@PathVariable Long id, @PathVariable Long id,
@RequestParam("file") MultipartFile file) { @RequestParam("file") MultipartFile file) {
try { if (file.isEmpty()) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "上传文件为空");
}
if (file.getSize() > MAX_FILE_SIZE) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "文件大小超过限制(最大 50MB");
}
String contentType = file.getContentType();
if (contentType == null || !ALLOWED_CONTENT_TYPES.contains(contentType)) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
"不支持的文件类型: " + (contentType != null ? contentType : "未知"));
}
String uploadDir = System.getProperty("user.dir") + "/uploads/contracts/" + id + "/"; String uploadDir = System.getProperty("user.dir") + "/uploads/contracts/" + id + "/";
File dir = new File(uploadDir); File dir = new File(uploadDir);
if (!dir.exists()) dir.mkdirs(); if (!dir.exists()) dir.mkdirs();
String fileName = System.currentTimeMillis() + "_" + file.getOriginalFilename(); String originalName = file.getOriginalFilename();
File dest = new File(uploadDir + fileName); String ext = originalName != null && originalName.contains(".")
? originalName.substring(originalName.lastIndexOf('.'))
: "";
String storedName = java.util.UUID.randomUUID().toString() + ext;
File dest = new File(uploadDir + storedName);
try {
file.transferTo(dest); file.transferTo(dest);
} catch (IOException e) {
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "文件存储失败");
}
PlatformContractAttachment attachment = new PlatformContractAttachment(); PlatformContractAttachment attachment = new PlatformContractAttachment();
attachment.setContractId(id); attachment.setContractId(id);
attachment.setFileName(file.getOriginalFilename()); attachment.setFileName(originalName);
attachment.setFilePath(dest.getAbsolutePath()); attachment.setFilePath(dest.getAbsolutePath());
attachment.setFileSize(file.getSize()); attachment.setFileSize(file.getSize());
attachment.setContentType(file.getContentType()); attachment.setContentType(contentType);
attachment.setCreatedAt(OffsetDateTime.now()); attachment.setCreatedAt(OffsetDateTime.now());
attachmentMapper.insert(attachment); attachmentMapper.insert(attachment);
return ResponseEntity.ok(Map.of("id", attachment.getId(), "fileName", attachment.getFileName())); return ResponseEntity.ok(Map.of("id", attachment.getId(), "fileName", attachment.getFileName()));
} catch (Exception e) {
return ResponseEntity.status(500).body(Map.of("error", e.getMessage()));
}
} }
@GetMapping("/{id}/attachments") @GetMapping("/{id}/attachments")
@@ -22,7 +22,7 @@ public class LicenseController {
try { try {
return ResponseEntity.ok(licenseService.create(request)); return ResponseEntity.ok(licenseService.create(request));
} catch (Exception e) { } catch (Exception e) {
return ResponseEntity.internalServerError().body(Map.of("error", e.getMessage())); throw new RuntimeException(e);
} }
} }