Add image upload validations and random naming

This commit is contained in:
Tim
2025-07-01 13:00:47 +08:00
parent a37f046898
commit d69f7251e0
5 changed files with 43 additions and 2 deletions

View File

@@ -3,6 +3,7 @@ package com.openisle.controller;
import com.openisle.model.User; import com.openisle.model.User;
import com.openisle.service.ImageUploader; import com.openisle.service.ImageUploader;
import com.openisle.service.UserService; import com.openisle.service.UserService;
import org.springframework.beans.factory.annotation.Value;
import lombok.Data; import lombok.Data;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@@ -20,6 +21,12 @@ public class UserController {
private final UserService userService; private final UserService userService;
private final ImageUploader imageUploader; private final ImageUploader imageUploader;
@Value("${app.upload.check-type:true}")
private boolean checkImageType;
@Value("${app.upload.max-size:5242880}")
private long maxUploadSize;
@GetMapping("/me") @GetMapping("/me")
public ResponseEntity<UserDto> me(Authentication auth) { public ResponseEntity<UserDto> me(Authentication auth) {
User user = userService.findByUsername(auth.getName()).orElseThrow(); User user = userService.findByUsername(auth.getName()).orElseThrow();
@@ -29,6 +36,12 @@ public class UserController {
@PostMapping("/me/avatar") @PostMapping("/me/avatar")
public ResponseEntity<?> uploadAvatar(@RequestParam("file") MultipartFile file, public ResponseEntity<?> uploadAvatar(@RequestParam("file") MultipartFile file,
Authentication auth) { 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; String url = null;
try { try {
url = imageUploader.upload(file.getBytes(), file.getOriginalFilename()).join(); url = imageUploader.upload(file.getBytes(), file.getOriginalFilename()).join();

View File

@@ -12,6 +12,7 @@ import org.springframework.scheduling.concurrent.CustomizableThreadFactory;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.util.UUID;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
@@ -52,15 +53,22 @@ public class CosImageUploader extends ImageUploader {
@Override @Override
public CompletableFuture<String> upload(byte[] data, String filename) { public CompletableFuture<String> upload(byte[] data, String filename) {
return CompletableFuture.supplyAsync(() -> { 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(); ObjectMetadata meta = new ObjectMetadata();
meta.setContentLength(data.length); meta.setContentLength(data.length);
PutObjectRequest req = new PutObjectRequest( PutObjectRequest req = new PutObjectRequest(
bucketName, bucketName,
filename, randomName,
new ByteArrayInputStream(data), new ByteArrayInputStream(data),
meta); meta);
cosClient.putObject(req); cosClient.putObject(req);
return baseUrl + "/" + filename; return baseUrl + "/" + randomName;
}, executor); }, executor);
} }
} }

View File

@@ -10,5 +10,9 @@ cos.secret-key=${COS_SECRET_KEY:}
cos.region=${COS_REGION:ap-guangzhou} cos.region=${COS_REGION:ap-guangzhou}
cos.bucket-name=${COS_BUCKET_NAME:} 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.secret=${JWT_SECRET:ChangeThisSecretKeyForJwt}
app.jwt.expiration=${JWT_EXPIRATION:86400000} app.jwt.expiration=${JWT_EXPIRATION:86400000}

View File

@@ -57,4 +57,16 @@ class UserControllerTest {
Mockito.verify(userService).updateAvatar("alice", "http://img/a.png"); 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());
}
} }

View File

@@ -11,5 +11,9 @@ cos.secret-key=dummy
cos.region=ap-guangzhou cos.region=ap-guangzhou
cos.bucket-name=testbucket 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.secret=TestSecret
app.jwt.expiration=3600000 app.jwt.expiration=3600000