Spring Boot集成GCS删除文件报错:匿名调用无storage.objects.get权限
Spring Boot GCS 删除文件权限错误排查与解决
问题描述
我开发了一个Spring Boot应用,视频文件能正常上传到Google Cloud Storage(GCS)存储桶,但通过Thymeleaf界面点击DELETE按钮删除文件时,出现以下报错:
Anonymous caller does not have storage.objects.get access to the Google Cloud Storage object. Permission 'storage.objects.get' denied on resource (or it may not exist).
我已经尝试给存储桶设置全权限,但问题依旧。以下是上传相关的代码:
Service 层
public VideoLesson uploadFile(MultipartFile file) { log.debug("Start file uploading service"); VideoLesson inputFile = new VideoLesson(); String originalFileName = file.getOriginalFilename(); if (originalFileName == null) { throw new BadRequestException("Original file name is null"); } Path path = new File(originalFileName).toPath(); try { String contentType = Files.probeContentType(path); VideoLessonDto fileDto = dataBucketUtil.uploadVideo(file, originalFileName, contentType); if (fileDto != null) { inputFile.setName(fileDto.getFileName()); inputFile.setFileUrl(fileDto.getFileUrl()); videoLessonRepository.save(inputFile); log.debug("File uploaded successfully, file name: {} and url: {}", fileDto.getFileName(), fileDto.getFileUrl()); } } catch (Exception e) { log.error("Error occurred while uploading. Error: ", e); throw new GCPFileUploadException("Error occurred while uploading"); } log.debug("File details successfully saved in the database"); return inputFile; }
DataBucketUtil.java
@Component @Slf4j public class DataBucketUtil { @Value("${gcp.config.file}") private String gcpConfigFile; @Value("${gcp.project.id}") private String gcpProjectId; @Value("${gcp.bucket.id}") private String gcpBucketId; public VideoLessonDto uploadVideo(MultipartFile multipartFile, String fileName, String contentType) { try { log.debug("Start file uploading process on GCS"); byte[] fileData = FileUtils.readFileToByteArray(convertFile(multipartFile)); InputStream inputStream = new ClassPathResource(gcpConfigFile).getInputStream(); StorageOptions options = StorageOptions.newBuilder().setProjectId(gcpProjectId) .setCredentials(GoogleCredentials.fromStream(inputStream)).build(); Storage storage = options.getService(); Bucket bucket = storage.get(gcpBucketId, Storage.BucketGetOption.fields()); RandomString id = new RandomString(6, ThreadLocalRandom.current()); Blob blob = bucket.create(fileName + checkFileExtension(fileName), fileData, contentType); if (blob != null) { log.debug("File successfully uploaded to GCS"); return new VideoLessonDto(blob.getName(), blob.getMediaLink()); } } catch (Exception e) { log.error("An error occurred while uploading data. Exception: ", e); throw new GCPFileUploadException("An error occurred while storing data to GCS"); } return null; } private File convertFile(MultipartFile file) { try { if (file.getOriginalFilename() == null) { throw new BadRequestException("Original file name is null"); } File convertedFile = new File(file.getOriginalFilename()); FileOutputStream outputStream = new FileOutputStream(convertedFile); outputStream.write(file.getBytes()); outputStream.close(); log.debug("Converting multipart file : {}", convertedFile); return convertedFile; } catch (Exception e) { throw new FileWriteException("An error has occurred while converting the file"); } } private String checkFileExtension(String fileName) { if (fileName != null && fileName.contains(".")) { String extension = ".mp4"; log.debug("Accepted file type : {}", extension); return extension; } log.error("Not a permitted file type"); throw new InvalidFileTypeException("Not a permitted file type"); } }
Controller 层
@PostMapping(value = "/video_lesson/upload") public String uploadFile(@RequestParam("file") MultipartFile file, Model model) { String message; try { videoLessonService.uploadFile(file); message = "Video bazaya müvəfəqiyyətlə yükləndi: " + file.getOriginalFilename(); model.addAttribute("message", message); Thread.sleep(4000); } catch (Exception e) { message = "Diqqət bir video seçməlisiniz!"; model.addAttribute("message", message); } return "redirect:/video_lesson/files"; }
问题根源
核心问题是删除操作没有使用与上传一致的GCP服务账号凭证:
- 上传时你通过
GoogleCredentials.fromStream(inputStream)加载了服务账号密钥文件,创建了带权限的Storage客户端; - 但删除操作的代码(未提供)大概率没有复用这个凭证逻辑,导致请求以匿名身份发送,GCS直接拒绝了无权限的请求;
- 就算给存储桶设置全权限,GCS也不会允许匿名用户执行删除/读取(用于确认对象存在)操作,公共权限仅开放读权限给匿名用户,不会涉及修改类操作。
解决步骤
1. 在DataBucketUtil中添加删除方法,复用凭证逻辑
在DataBucketUtil里新增删除文件的方法,确保使用和上传相同的服务账号凭证:
public void deleteVideo(String fileName) { try { // 复用上传时的凭证加载逻辑 InputStream inputStream = new ClassPathResource(gcpConfigFile).getInputStream(); StorageOptions options = StorageOptions.newBuilder() .setProjectId(gcpProjectId) .setCredentials(GoogleCredentials.fromStream(inputStream)) .build(); Storage storage = options.getService(); // 构建BlobID,指定桶名和文件名 BlobId blobId = BlobId.of(gcpBucketId, fileName); boolean isDeleted = storage.delete(blobId); if (isDeleted) { log.debug("GCS中的文件 {} 已成功删除", fileName); } else { log.warn("GCS中未找到文件 {}", fileName); throw new ResourceNotFoundException("目标文件不存在于GCS"); } } catch (Exception e) { log.error("删除GCS文件时出错: ", e); throw new GCPFileDeleteException("删除GCS文件失败"); } }
2. 在Service层添加删除业务逻辑
在视频服务类中,先删除GCS文件,再删除数据库记录:
public void deleteVideoLesson(Long id) { // 先从数据库查询记录 VideoLesson videoLesson = videoLessonRepository.findById(id) .orElseThrow(() -> new ResourceNotFoundException("未找到该视频课程")); // 删除GCS上的文件 dataBucketUtil.deleteVideo(videoLesson.getName()); // 删除数据库记录 videoLessonRepository.delete(videoLesson); log.debug("ID为 {} 的视频课程已完全删除", id); }
3. 在Controller层添加删除接口
处理Thymeleaf页面的删除请求(注意Thymeleaf通常用POST请求配合_method=DELETE参数):
@PostMapping("/video_lesson/delete/{id}") public String deleteVideoLesson(@PathVariable Long id, Model model) { String message; try { videoLessonService.deleteVideoLesson(id); message = "视频已成功删除"; } catch (Exception e) { message = "删除视频时出错: " + e.getMessage(); } model.addAttribute("message", message); return "redirect:/video_lesson/files"; }
额外检查点
- 确认服务账号密钥文件对应的账号拥有
storage.objects.delete和storage.objects.get权限(删除操作前需要先读取对象确认存在,因此需要这两个权限); - 确认数据库中存储的
fileName与GCS中Blob的名称完全一致(比如上传时拼接了.mp4后缀,删除时要传相同的名称); - 不要依赖存储桶的公共权限执行删除,必须使用服务账号凭证,GCS不会对匿名用户开放修改类操作权限。
内容的提问来源于stack exchange,提问作者Elkhan Ismayilov
相关产品推荐
相关产品推荐

