From d69f7251e099950d06864c9a139f3e082bd9b4c1 Mon Sep 17 00:00:00 2001 From: Tim <135014430+nagisa77@users.noreply.github.com> Date: Tue, 1 Jul 2025 13:00:47 +0800 Subject: [PATCH] Add image upload validations and random naming --- .../com/openisle/controller/UserController.java | 13 +++++++++++++ .../java/com/openisle/service/CosImageUploader.java | 12 ++++++++++-- src/main/resources/application.properties | 4 ++++ .../com/openisle/controller/UserControllerTest.java | 12 ++++++++++++ src/test/resources/application.properties | 4 ++++ 5 files changed, 43 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/openisle/controller/UserController.java b/src/main/java/com/openisle/controller/UserController.java index 96ae30ebd..dac951982 100644 --- a/src/main/java/com/openisle/controller/UserController.java +++ b/src/main/java/com/openisle/controller/UserController.java @@ -3,6 +3,7 @@ package com.openisle.controller; import com.openisle.model.User; import com.openisle.service.ImageUploader; import com.openisle.service.UserService; +import org.springframework.beans.factory.annotation.Value; import lombok.Data; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -20,6 +21,12 @@ public class UserController { private final UserService userService; private final ImageUploader imageUploader; + @Value("${app.upload.check-type:true}") + private boolean checkImageType; + + @Value("${app.upload.max-size:5242880}") + private long maxUploadSize; + @GetMapping("/me") public ResponseEntity me(Authentication auth) { User user = userService.findByUsername(auth.getName()).orElseThrow(); @@ -29,6 +36,12 @@ public class UserController { @PostMapping("/me/avatar") public ResponseEntity uploadAvatar(@RequestParam("file") MultipartFile file, Authentication auth) { + if (checkImageType && (file.getContentType() == null || !file.getContentType().startsWith("image/"))) { + return ResponseEntity.badRequest().body(Map.of("error", "File is not an image")); + } + if (file.getSize() > maxUploadSize) { + return ResponseEntity.badRequest().body(Map.of("error", "File too large")); + } String url = null; try { url = imageUploader.upload(file.getBytes(), file.getOriginalFilename()).join(); diff --git a/src/main/java/com/openisle/service/CosImageUploader.java b/src/main/java/com/openisle/service/CosImageUploader.java index 2142fd30d..9e5d28375 100644 --- a/src/main/java/com/openisle/service/CosImageUploader.java +++ b/src/main/java/com/openisle/service/CosImageUploader.java @@ -12,6 +12,7 @@ import org.springframework.scheduling.concurrent.CustomizableThreadFactory; import org.springframework.stereotype.Service; import java.io.ByteArrayInputStream; +import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -52,15 +53,22 @@ public class CosImageUploader extends ImageUploader { @Override public CompletableFuture upload(byte[] data, String filename) { return CompletableFuture.supplyAsync(() -> { + String ext = ""; + int dot = filename.lastIndexOf('.'); + if (dot != -1) { + ext = filename.substring(dot); + } + String randomName = UUID.randomUUID().toString().replace("-", "") + ext; + ObjectMetadata meta = new ObjectMetadata(); meta.setContentLength(data.length); PutObjectRequest req = new PutObjectRequest( bucketName, - filename, + randomName, new ByteArrayInputStream(data), meta); cosClient.putObject(req); - return baseUrl + "/" + filename; + return baseUrl + "/" + randomName; }, executor); } } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index b59558ad2..6cb56ee1b 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -10,5 +10,9 @@ cos.secret-key=${COS_SECRET_KEY:} cos.region=${COS_REGION:ap-guangzhou} cos.bucket-name=${COS_BUCKET_NAME:} +# Image upload configuration +app.upload.check-type=${UPLOAD_CHECK_TYPE:true} +app.upload.max-size=${UPLOAD_MAX_SIZE:5242880} + app.jwt.secret=${JWT_SECRET:ChangeThisSecretKeyForJwt} app.jwt.expiration=${JWT_EXPIRATION:86400000} diff --git a/src/test/java/com/openisle/controller/UserControllerTest.java b/src/test/java/com/openisle/controller/UserControllerTest.java index 4e789776a..eec00f78c 100644 --- a/src/test/java/com/openisle/controller/UserControllerTest.java +++ b/src/test/java/com/openisle/controller/UserControllerTest.java @@ -57,4 +57,16 @@ class UserControllerTest { Mockito.verify(userService).updateAvatar("alice", "http://img/a.png"); } + + @Test + void uploadAvatarRejectsNonImage() throws Exception { + MockMultipartFile file = new MockMultipartFile("file", "a.txt", MediaType.TEXT_PLAIN_VALUE, "text".getBytes()); + + mockMvc.perform(multipart("/api/users/me/avatar").file(file).principal(new UsernamePasswordAuthenticationToken("alice","p"))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.error").value("File is not an image")); + + Mockito.verify(imageUploader, Mockito.never()).upload(any(), any()); + } + } diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties index 0ff8e0dfa..489fa5f31 100644 --- a/src/test/resources/application.properties +++ b/src/test/resources/application.properties @@ -11,5 +11,9 @@ cos.secret-key=dummy cos.region=ap-guangzhou cos.bucket-name=testbucket +# Image upload configuration for tests +app.upload.check-type=true +app.upload.max-size=1048576 + app.jwt.secret=TestSecret app.jwt.expiration=3600000