SpringBoot 3.x集成阿里云OSS:文件上传 断点续传 权限控制

发布于:2025-07-17 ⋅ 阅读:(14) ⋅ 点赞:(0)

Spring Boot 3.x 集成阿里云 OSS 终极指南

一、环境准备与依赖配置

1. 添加阿里云 OSS SDK 依赖

<dependency>
    <groupId>com.aliyun.oss</groupId>
    <artifactId>aliyun-sdk-oss</artifactId>
    <version>3.15.1</version>
</dependency>

2. 配置 OSS 连接参数

# application.yml
aliyun:
  oss:
    endpoint: oss-cn-hangzhou.aliyuncs.com # 根据实际区域修改
    access-key-id: your-access-key-id
    access-key-secret: your-access-key-secret
    bucket-name: your-bucket-name
    sts:
      role-arn: acs:ram::1234567890123456:role/oss-sts-role # RAM角色ARN
      policy: | # 权限策略
        {
          "Version": "1",
          "Statement": [
            {
              "Effect": "Allow",
              "Action": [
                "oss:GetObject",
                "oss:PutObject"
              ],
              "Resource": [
                "acs:oss:*:*:your-bucket-name/*"
              ]
            }
          ]
        }

二、基础文件上传服务

1. OSS 客户端配置

@Configuration
public class OssConfig {

    @Value("${aliyun.oss.endpoint}")
    private String endpoint;

    @Value("${aliyun.oss.access-key-id}")
    private String accessKeyId;

    @Value("${aliyun.oss.access-key-secret}")
    private String accessKeySecret;

    @Bean
    public OSS ossClient() {
        return new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
    }
}

2. 文件上传服务

@Service
@Slf4j
public class OssService {

    private final OSS ossClient;
    private final String bucketName;

    public OssService(OSS ossClient, 
                     @Value("${aliyun.oss.bucket-name}") String bucketName) {
        this.ossClient = ossClient;
        this.bucketName = bucketName;
    }

    /**
     * 简单文件上传
     * @param file 上传文件
     * @param objectKey 对象键(OSS路径)
     * @return 文件URL
     */
    public String uploadFile(MultipartFile file, String objectKey) throws IOException {
        try (InputStream inputStream = file.getInputStream()) {
            ossClient.putObject(bucketName, objectKey, inputStream);
            return generateUrl(objectKey);
        } catch (Exception e) {
            log.error("文件上传失败: {}", objectKey, e);
            throw new OssException("文件上传失败");
        }
    }

    /**
     * 生成文件URL(带签名)
     */
    private String generateUrl(String objectKey) {
        Date expiration = new Date(System.currentTimeMillis() + 3600 * 1000); // 1小时有效
        return ossClient.generatePresignedUrl(bucketName, objectKey, expiration).toString();
    }
}

三、断点续传高级实现

1. 断点续传服务

@Service
public class ResumableUploadService {

    private final OSS ossClient;
    private final String bucketName;

    public ResumableUploadService(OSS ossClient, 
                                @Value("${aliyun.oss.bucket-name}") String bucketName) {
        this.ossClient = ossClient;
        this.bucketName = bucketName;
    }

    /**
     * 断点续传上传
     * @param file 上传文件
     * @param objectKey 对象键
     * @return 上传结果
     */
    public UploadResult resumableUpload(MultipartFile file, String objectKey) {
        try {
            // 创建上传请求
            UploadFileRequest request = new UploadFileRequest(
                bucketName, 
                objectKey,
                file.getInputStream(),
                file.getSize()
            );
            
            // 配置上传参数
            request.setPartSize(5 * 1024 * 1024); // 5MB分片
            request.setTaskNum(5); // 并发线程数
            request.setEnableCheckpoint(true); // 开启断点记录
            
            // 设置断点文件存储位置
            String checkpointDir = System.getProperty("java.io.tmpdir") + "/oss-checkpoints";
            request.setCheckpointFile(checkpointDir + "/" + objectKey + ".ucp");
            
            // 执行上传
            UploadFileResult result = ossClient.uploadFile(request);
            
            return new UploadResult(
                generateUrl(objectKey),
                result.getMultipartUploadResult().getETag(),
                result.getMultipartUploadResult().getLocation()
            );
        } catch (Throwable e) {
            throw new OssException("断点续传失败", e);
        }
    }
    
    @Data
    @AllArgsConstructor
    public static class UploadResult {
        private String fileUrl;
        private String eTag;
        private String location;
    }
}

2. 断点续传恢复机制

public void resumeUpload(String objectKey) {
    String checkpointFile = getCheckpointFilePath(objectKey);
    
    if (new File(checkpointFile).exists()) {
        UploadFileRequest request = new UploadFileRequest(bucketName, objectKey);
        request.setCheckpointFile(checkpointFile);
        
        try {
            ossClient.uploadFile(request);
        } catch (Throwable e) {
            throw new OssException("续传失败", e);
        }
    } else {
        throw new OssException("未找到断点记录");
    }
}

四、精细化权限控制

1. STS 临时凭证服务

@Service
public class StsService {

    @Value("${aliyun.oss.sts.role-arn}")
    private String roleArn;
    
    @Value("${aliyun.oss.sts.policy}")
    private String policy;
    
    @Value("${aliyun.oss.access-key-id}")
    private String accessKeyId;
    
    @Value("${aliyun.oss.access-key-secret}")
    private String accessKeySecret;

    /**
     * 获取STS临时凭证
     * @param sessionName 会话名称
     * @param durationSeconds 有效期(秒)
     * @return STS凭证
     */
    public StsToken getStsToken(String sessionName, long durationSeconds) {
        DefaultProfile profile = DefaultProfile.getProfile("cn-hangzhou", accessKeyId, accessKeySecret);
        IAcsClient client = new DefaultAcsClient(profile);
        
        AssumeRoleRequest request = new AssumeRoleRequest();
        request.setRoleArn(roleArn);
        request.setRoleSessionName(sessionName);
        request.setDurationSeconds(durationSeconds);
        request.setPolicy(policy);
        
        try {
            AssumeRoleResponse response = client.getAcsResponse(request);
            AssumeRoleResponse.Credentials credentials = response.getCredentials();
            
            return new StsToken(
                credentials.getAccessKeyId(),
                credentials.getAccessKeySecret(),
                credentials.getSecurityToken(),
                credentials.getExpiration()
            );
        } catch (ClientException e) {
            throw new StsException("STS获取失败", e);
        }
    }
    
    @Data
    @AllArgsConstructor
    public static class StsToken {
        private String accessKeyId;
        private String accessKeySecret;
        private String securityToken;
        private String expiration;
    }
}

2. 前端直传签名服务

@Service
public class OssSignatureService {

    private final OSS ossClient;
    private final String bucketName;

    public OssSignatureService(OSS ossClient, 
                             @Value("${aliyun.oss.bucket-name}") String bucketName) {
        this.ossClient = ossClient;
        this.bucketName = bucketName;
    }

    /**
     * 生成前端直传签名
     * @param objectKey 对象键
     * @param expireSeconds 过期时间(秒)
     * @return 签名信息
     */
    public SignatureInfo generateSignature(String objectKey, long expireSeconds) {
        Date expiration = new Date(System.currentTimeMillis() + expireSeconds * 1000);
        
        // 创建策略
        PolicyConditions policy = new PolicyConditions();
        policy.addConditionItem(PolicyConditions.COND_CONTENT_LENGTH_RANGE, 0, 104857600); // 100MB限制
        policy.addConditionItem(PolicyConditions.COND_KEY, objectKey);
        
        // 生成签名
        String postPolicy = ossClient.generatePostPolicy(expiration, policy);
        String encodedPolicy = BinaryUtil.toBase64String(postPolicy.getBytes());
        String signature = ossClient.calculatePostSignature(postPolicy);
        
        return new SignatureInfo(
            ossClient.getEndpoint().toString(),
            bucketName,
            objectKey,
            encodedPolicy,
            signature,
            expiration
        );
    }
    
    @Data
    @AllArgsConstructor
    public static class SignatureInfo {
        private String endpoint;
        private String bucket;
        private String key;
        private String policy;
        private String signature;
        private Date expiration;
    }
}

五、SDK 坑位指南与最佳实践

1. 常见问题解决方案

问题类型 现象 解决方案
连接超时 上传大文件时超时 增加超时时间:
ossClient.setTimeout(300000)
内存溢出 大文件上传时OOM 使用文件流代替内存流:
request.setUploadFile(filePath)
分片失败 分片上传卡死 设置合理的分片大小:
request.setPartSize(5 * 1024 * 1024)
签名失效 前端直传签名过期 签名有效期至少600秒,建议1200秒
权限不足 STS操作失败 检查RAM角色权限策略

2. 性能优化配置

@Bean
public OSS ossClient(OssProperties properties) {
    ClientBuilderConfiguration config = new ClientBuilderConfiguration();
    
    // 连接池配置
    config.setMaxConnections(200); // 最大连接数
    config.setConnectionTimeout(30 * 1000); // 连接超时30s
    config.setSocketTimeout(120 * 1000); // 读写超时120s
    
    // 开启HTTP重试
    config.setMaxErrorRetry(3); 
    
    // 开启HTTPS
    config.setProtocol(Protocol.HTTPS);
    
    return new OSSClientBuilder()
        .build(properties.getEndpoint(), 
               properties.getAccessKeyId(), 
               properties.getAccessKeySecret(),
               config);
}

3. 安全最佳实践

/**
 * 安全文件上传验证
 */
public void validateFileUpload(MultipartFile file, String objectKey) {
    // 1. 文件类型验证
    String contentType = file.getContentType();
    if (!Arrays.asList("image/jpeg", "image/png").contains(contentType)) {
        throw new OssException("不支持的文件类型");
    }
    
    // 2. 文件大小验证
    if (file.getSize() > 10 * 1024 * 1024) { // 10MB限制
        throw new OssException("文件大小超过限制");
    }
    
    // 3. 文件名安全过滤
    if (objectKey.contains("..") || objectKey.contains("/")) {
        throw new OssException("非法文件名");
    }
    
    // 4. 病毒扫描(集成第三方服务)
    if (!virusScanService.scan(file)) {
        throw new OssException("文件安全检测未通过");
    }
}

六、完整文件管理控制器

@RestController
@RequestMapping("/oss")
@RequiredArgsConstructor
public class OssController {

    private final OssService ossService;
    private final ResumableUploadService resumableService;
    private final StsService stsService;
    private final OssSignatureService signatureService;

    /**
     * 普通文件上传
     */
    @PostMapping("/upload")
    public ResponseEntity<String> uploadFile(
            @RequestParam("file") MultipartFile file,
            @RequestParam("path") String path) throws IOException {
        String objectKey = "uploads/" + path + "/" + file.getOriginalFilename();
        String url = ossService.uploadFile(file, objectKey);
        return ResponseEntity.ok(url);
    }

    /**
     * 断点续传接口
     */
    @PostMapping("/resumable-upload")
    public ResponseEntity<ResumableUploadService.UploadResult> resumableUpload(
            @RequestParam("file") MultipartFile file,
            @RequestParam("path") String path) {
        String objectKey = "uploads/" + path + "/" + file.getOriginalFilename();
        return ResponseEntity.ok(resumableService.resumableUpload(file, objectKey));
    }

    /**
     * 获取STS临时凭证
     */
    @GetMapping("/sts-token")
    public ResponseEntity<StsService.StsToken> getStsToken() {
        String sessionName = "user-" + SecurityUtils.getCurrentUserId();
        return ResponseEntity.ok(stsService.getStsToken(sessionName, 3600));
    }

    /**
     * 生成前端直传签名
     */
    @GetMapping("/signature")
    public ResponseEntity<OssSignatureService.SignatureInfo> getSignature(
            @RequestParam String fileName) {
        String objectKey = "uploads/user/" + SecurityUtils.getCurrentUserId() + "/" + fileName;
        return ResponseEntity.ok(signatureService.generateSignature(objectKey, 1200));
    }
}

七、部署与监控

1. 健康检查端点

@RestController
@RequestMapping("/actuator")
public class OssHealthController {

    private final OSS ossClient;
    private final String bucketName;

    @GetMapping("/oss-health")
    public ResponseEntity<String> checkOssHealth() {
        try {
            boolean exists = ossClient.doesBucketExist(bucketName);
            return exists ? 
                ResponseEntity.ok("OSS connection is healthy") :
                ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
                    .body("Bucket not found");
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
                .body("OSS connection failed: " + e.getMessage());
        }
    }
}

2. Prometheus 监控指标

@Bean
public MeterRegistryCustomizer<MeterRegistry> ossMetrics(OSS ossClient) {
    return registry -> {
        Gauge.builder("oss.connection.count", ossClient, 
                client -> client.getClientConfiguration().getMaxConnections())
            .description("OSS connection pool size")
            .register(registry);
        
        Counter.builder("oss.upload.count")
            .description("Total OSS upload operations")
            .register(registry);
    };
}

八、总结与最佳实践

1. 架构选择建议

  • 小文件上传:直接使用简单上传接口
  • 大文件上传:使用断点续传(>10MB)
  • 前端直传:使用STS临时凭证或签名直传
  • 敏感文件:服务端中转上传+病毒扫描

2. 安全防护措施

  1. 权限最小化:STS策略只授予必要权限
  2. 文件类型过滤:限制可上传文件类型
  3. 病毒扫描:集成ClamAV等扫描引擎
  4. 访问日志:开启OSS访问日志审计
  5. WAF防护:配置Web应用防火墙规则

3. 性能优化方案

小文件
大文件
前端直传
客户端
直接上传
分片上传
并发上传
OSS服务端合并
签名/STS
绕过应用服务器

通过本方案,可实现安全高效的OSS文件管理,支持从KB到TB级文件的上传需求,同时满足企业级安全合规要求。


网站公告

今日签到

点亮在社区的每一天
去签到