You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.01 16:37:34