package com.example.oss.util; import com.aliyun.oss.OSS; import com.aliyun.oss.OSSClientBuilder; import com.aliyun.oss.model.SetBucketCORSRequest; import com.example.oss.config.OssConfig; import com.example.oss.vo.OssTokenVo; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.Arrays; import java.util.Base64; import java.util.List; import java.util.UUID; /** * 阿里云 OSS 工具类(前端直传专用) *
* 仅包含前端直传所需的核心方法: * - generatePostObjectToken: 生成 PostObject 直传 Token * - configureBucketCors: 配置 Bucket CORS 规则 *
*/ @Slf4j @Component @RequiredArgsConstructor public class OssUtils { private final OssConfig ossConfig; /** * 获取 OSS 客户端 * * @return OSS 客户端 */ private OSS getOssClient() { return new OSSClientBuilder().build( ossConfig.getEndpoint(), ossConfig.getAccessKeyId(), ossConfig.getAccessKeySecret() ); } // ==================== 前端直传核心方法 ==================== /** * 生成阿里云 OSS PostObject 直传 Token ** 用于前端直传文件到 OSS,无需经过后端中转。 * 前端拿到此 Token 后,通过 FormData + POST 方式直接上传到 OSS。 *
* ** 需确保 OSS 账号有 oss:PutBucketCors 权限。 * 也可在阿里云控制台手动配置。 *
*/ public void configureBucketCors() { if (!Boolean.TRUE.equals(ossConfig.getCorsEnabled())) { return; } OSS ossClient = getOssClient(); try { SetBucketCORSRequest request = new SetBucketCORSRequest(ossConfig.getBucketName()); SetBucketCORSRequest.CORSRule rule = new SetBucketCORSRequest.CORSRule(); // 允许的来源(开发:localhost,生产:需配置实际域名) String origins = ossConfig.getCorsAllowedOrigins(); if (origins == null || origins.isBlank()) { origins = "http://localhost:5173,http://localhost:5174"; } rule.setAllowedOrigins(Arrays.asList(origins.trim().split("\s*,\s*"))); // 允许的方法:POST(直传)、GET、PUT、DELETE、HEAD rule.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "HEAD")); // 允许的请求头(* 表示全部,PostObject 需要) rule.setAllowedHeaders(Arrays.asList("*")); // 暴露给前端的响应头 rule.setExposeHeaders(Arrays.asList("ETag", "x-oss-request-id")); // 预检请求缓存时间(秒) rule.setMaxAgeSeconds(600); request.setCorsRules(List.of(rule)); ossClient.setBucketCORS(request); log.info("OSS Bucket CORS 配置成功,允许来源: {}", origins); } catch (Exception e) { log.warn("OSS Bucket CORS 配置失败(可在阿里云控制台手动配置): {}", e.getMessage()); } finally { ossClient.shutdown(); } } // ==================== 私有辅助方法 ==================== /** * 构建 PostPolicy * * @param objectKey 对象键 * @param expiration 过期时间戳 * @return Base64 编码的 Policy */ private String buildPostPolicy(String objectKey, long expiration) { // ISO 8601 GMT 格式(阿里云要求 YYYY-MM-DDTHH:mm:ss.sssZ,必须以 Z 结尾) String expireTime = java.time.Instant.ofEpochMilli(expiration).toString(); // 构建 Policy JSON String policyJson = String.format( "{\"expiration\":\"%s\",\"conditions\":[[\"eq\",\"$key\",\"%s\"]]}", expireTime, objectKey ); // Base64 编码 return Base64.getEncoder().encodeToString(policyJson.getBytes(StandardCharsets.UTF_8)); } /** * 计算签名 * * @param policy Base64 编码的 Policy * @param accessKeySecret OSS 访问密钥 * @return 签名(Base64 编码) */ private String computeSignature(String policy, String accessKeySecret) { try { // 使用 HMAC-SHA1 算法 Mac mac = Mac.getInstance("HmacSHA1"); SecretKeySpec keySpec = new SecretKeySpec( accessKeySecret.getBytes(StandardCharsets.UTF_8), "HmacSHA1" ); mac.init(keySpec); // 计算签名 byte[] signData = mac.doFinal(policy.getBytes(StandardCharsets.UTF_8)); // Base64 编码 return Base64.getEncoder().encodeToString(signData); } catch (Exception e) { log.error("计算 OSS 签名失败:{}", e.getMessage(), e); throw new RuntimeException("计算 OSS 签名失败:" + e.getMessage(), e); } } /** * 检查文件扩展名是否允许 * * @param filename 文件名 * @return 是否允许 */ private boolean isAllowedExtension(String filename) { if (filename == null || filename.isEmpty()) { return false; } String extension = getFileExtension(filename).toLowerCase(); return Arrays.stream(ossConfig.getAllowedExtensions()) .anyMatch(allowed -> allowed.toLowerCase().equals(extension)); } /** * 获取文件扩展名 * * @param filename 文件名 * @return 扩展名(包含点) */ private String getFileExtension(String filename) { if (filename == null || filename.isEmpty()) { return ""; } int lastDot = filename.lastIndexOf("."); if (lastDot == -1) { return ""; } return filename.substring(lastDot); } }