阿里云 OSS + CDN 加速方案实践
一、前言
在 Web 应用和 APP 开发中,经常需要存储和分发大量的静态资源,如图片、视频、安装包、文档等。如果这些资源都放在服务器本地,不仅会占用大量服务器存储空间,还会消耗服务器带宽,导致访问速度慢。
阿里云对象存储服务(OSS)是一种海量、安全、低成本、高可靠的云存储服务,配合 CDN 加速使用,可以大幅提升静态资源的访问速度,同时降低服务器成本。
本文将从实际应用角度出发,详细介绍阿里云 OSS + CDN 加速方案的完整实现,包括 OSS 配置、CDN 配置、Java SDK 集成、HTTPS 配置、成本优化等内容。
二、方案概述
2.1 为什么选择 OSS + CDN
- 存储成本低:OSS 存储成本远低于云服务器的系统盘或数据盘
- 访问速度快:CDN 全球节点加速,用户从最近的节点获取资源
- 节省服务器带宽:静态资源走 CDN,不占用服务器带宽
- 弹性扩展:存储空间和流量按需使用,无需提前规划
- 高可靠性:OSS 提供 99.9999999999% 的数据可靠性
2.2 整体架构
典型的 OSS + CDN 架构如下:
- 用户上传文件 → 应用服务器 → OSS 存储
- 用户请求资源 → CDN 节点 → 命中直接返回,未命中回源 OSS
- CDN 缓存资源 → 后续请求直接从 CDN 返回
三、OSS 配置
3.1 创建 Bucket
首先需要在阿里云 OSS 控制台创建一个 Bucket(存储空间):
- Bucket 名称:全局唯一,建议使用业务相关的名称
- 地域:选择离用户最近的地域
- 存储类型:标准存储(默认)
- 读写权限:公共读(用于静态资源分发)
- 服务端加密:根据需要开启
3.2 获取 AccessKey
为了安全起见,建议创建 RAM 子用户并授权:
- 进入 RAM 访问控制控制台
- 创建用户,勾选"编程访问"
- 记录 AccessKey ID 和 AccessKey Secret
- 为用户添加权限:AliyunOSSFullAccess(或更细粒度的权限)
安全提示:AccessKey 是访问 OSS 的密钥,一定要妥善保管,不要泄露到公开场合。建议使用 RAM 子用户,并遵循最小权限原则。
3.3 配置 CORS 跨域
如果前端页面需要直接访问 OSS 资源,需要配置跨域规则:
- 来源:允许的域名,如 *.example.com
- 允许 Methods:GET、POST、PUT、DELETE、HEAD
- 允许 Headers:* 或指定的 header
- 暴露 Headers:ETag、x-oss-request-id 等
- 缓存时间:建议 86400 秒(1天)
四、CDN 配置
4.1 添加加速域名
在 CDN 控制台添加加速域名:
- 加速域名:如 cdn.example.com
- 业务类型:图片小文件、大文件下载、视音频点播
- 源站信息:OSS 域名(选择刚才创建的 Bucket)
- 端口:80 或 443
4.2 配置 CNAME 解析
添加加速域名后,CDN 会分配一个 CNAME 地址。需要在域名 DNS 解析处添加 CNAME 记录:
- 主机记录:cdn(加速域名的前缀)
- 记录类型:CNAME
- 记录值:CDN 分配的 CNAME 地址
4.3 缓存配置
合理配置缓存规则可以提升命中率,降低回源流量:
- 静态文件(图片、CSS、JS):缓存 30 天
- 更新不频繁的文件:缓存 7 天
- 更新频繁的文件:缓存 1 小时或更短
- 动态内容:不缓存
4.4 HTTPS 配置
CDN 支持免费的 SSL 证书,可以开启 HTTPS 加速:
- 在 CDN 控制台的 HTTPS 配置中,选择"免费证书"
- 申请 DigiCert 免费证书
- 等待证书签发(通常几分钟)
- 开启 HTTPS 访问
- 建议开启"强制跳转 HTTPS"
4.5 性能优化配置
建议开启以下优化功能:
- 智能压缩:自动压缩静态文件,减少传输体积
- 过滤参数:忽略 URL 参数,提高缓存命中率
- Range 回源:支持断点续传和分片下载
- 页面优化:自动去除页面空格和注释
五、Java SDK 集成
5.1 添加依赖
在 pom.xml 中添加阿里云 OSS SDK 依赖:
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.15.1</version>
</dependency>
5.2 配置文件
在 application.properties 中添加配置:
# OSS 配置
oss.endpoint=oss-cn-beijing.aliyuncs.com
oss.accessKeyId=your-access-key-id
oss.accessKeySecret=your-access-key-secret
oss.bucketName=your-bucket-name
oss.cdnDomain=https://cdn.example.com
oss.objectPrefix=images/
5.3 工具类封装
封装一个 OSS 工具类,方便使用:
@Component
public class OssUtil {
@Value("${oss.endpoint}")
private String endpoint;
@Value("${oss.accessKeyId}")
private String accessKeyId;
@Value("${oss.accessKeySecret}")
private String accessKeySecret;
@Value("${oss.bucketName}")
private String bucketName;
@Value("${oss.cdnDomain}")
private String cdnDomain;
@Value("${oss.objectPrefix}")
private String objectPrefix;
/**
* 上传文件
*/
public String uploadFile(MultipartFile file) throws Exception {
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
try {
String fileName = file.getOriginalFilename();
// 生成唯一文件名,避免重名
String uniqueName = UUID.randomUUID().toString().replace("-", "")
+ fileName.substring(fileName.lastIndexOf("."));
String objectKey = objectPrefix + uniqueName;
InputStream inputStream = file.getInputStream();
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentLength(file.getSize());
metadata.setContentType(file.getContentType());
ossClient.putObject(bucketName, objectKey, inputStream, metadata);
return objectKey;
} finally {
ossClient.shutdown();
}
}
/**
* 删除文件
*/
public void deleteFile(String objectKey) {
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
try {
ossClient.deleteObject(bucketName, objectKey);
} finally {
ossClient.shutdown();
}
}
/**
* 获取 CDN 访问 URL
*/
public String getCdnUrl(String objectKey) {
if (objectKey == null || objectKey.isEmpty()) {
return "";
}
if (objectKey.startsWith("http://") || objectKey.startsWith("https://")) {
return objectKey;
}
String domain = cdnDomain.endsWith("/") ? cdnDomain : cdnDomain + "/";
String key = objectKey.startsWith("/") ? objectKey.substring(1) : objectKey;
return domain + key;
}
}
5.4 文件上传示例
@RestController
@RequestMapping("/file")
public class FileController {
@Autowired
private OssUtil ossUtil;
@PostMapping("/upload")
public Result upload(@RequestParam("file") MultipartFile file) {
try {
String objectKey = ossUtil.uploadFile(file);
String cdnUrl = ossUtil.getCdnUrl(objectKey);
Map data = new HashMap<>();
data.put("objectKey", objectKey);
data.put("url", cdnUrl);
return Result.success(data);
} catch (Exception e) {
return Result.error("上传失败:" + e.getMessage());
}
}
@DeleteMapping("/delete")
public Result delete(@RequestParam String objectKey) {
try {
ossUtil.deleteFile(objectKey);
return Result.success();
} catch (Exception e) {
return Result.error("删除失败:" + e.getMessage());
}
}
}
六、高级功能
6.1 图片处理
OSS 提供图片处理功能,可以实时对图片进行缩放、裁剪、水印等操作:
// 图片缩放,宽度 200px
String url = ossUtil.getCdnUrl(objectKey) + "?x-oss-process=image/resize,w_200";
// 图片裁剪,宽高 100x100
String url = ossUtil.getCdnUrl(objectKey) + "?x-oss-process=image/crop,w_100,h_100";
// 添加文字水印
String url = ossUtil.getCdnUrl(objectKey) + "?x-oss-process=image/watermark,text_SGVsbG8=";
6.2 签名 URL
对于私有 Bucket,可以生成签名 URL 进行临时访问:
/**
* 生成签名URL(私有Bucket使用)
*/
public String generatePresignedUrl(String objectKey, long expirationMinutes) {
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
try {
Date expiration = new Date(System.currentTimeMillis() + expirationMinutes * 60 * 1000);
URL url = ossClient.generatePresignedUrl(bucketName, objectKey, expiration);
return url.toString();
} finally {
ossClient.shutdown();
}
}
6.3 分片上传
对于大文件,建议使用分片上传,支持断点续传:
// 初始化分片上传
InitiateMultipartUploadRequest request = new InitiateMultipartUploadRequest(bucketName, objectKey);
InitiateMultipartUploadResult result = ossClient.initiateMultipartUpload(request);
String uploadId = result.getUploadId();
// 上传分片
List partETags = new ArrayList<>();
for (int i = 0; i < partCount; i++) {
UploadPartRequest uploadPartRequest = new UploadPartRequest();
uploadPartRequest.setBucketName(bucketName);
uploadPartRequest.setKey(objectKey);
uploadPartRequest.setUploadId(uploadId);
uploadPartRequest.setInputStream(inputStream);
uploadPartRequest.setPartSize(partSize);
uploadPartRequest.setPartNumber(i + 1);
UploadPartResult uploadPartResult = ossClient.uploadPart(uploadPartRequest);
partETags.add(uploadPartResult.getPartETag());
}
// 完成分片上传
CompleteMultipartUploadRequest completeRequest = new CompleteMultipartUploadRequest(
bucketName, objectKey, uploadId, partETags);
ossClient.completeMultipartUpload(completeRequest);
七、成本优化
7.1 存储成本优化
- 生命周期管理:将不常访问的文件转为低频访问或归档存储
- 定期清理:定期删除无用的临时文件和过期文件
- 图片压缩:上传前压缩图片,减少存储空间
7.2 流量成本优化
- 开启 CDN:CDN 流量比 OSS 外网流量便宜
- 提高缓存命中率:合理设置缓存时间,减少回源
- 开启智能压缩:减少传输体积
- 购买流量包:流量包比按量付费更划算
7.3 成本估算示例
以一个中等规模的 APP 为例,每月成本估算:
- 存储:100GB × 0.12元/GB = 12元/月
- CDN 流量:500GB × 0.2元/GB = 100元/月(流量包更便宜)
- 请求费用:约 5元/月
- 合计:约 117元/月
相比服务器带宽,成本降低很多,而且访问速度更快。
八、常见问题
8.1 CDN 缓存更新
文件更新后,CDN 节点的缓存不会立即更新。可以通过以下方式刷新:
- URL 刷新:强制刷新指定 URL
- 目录刷新:刷新整个目录
- 版本号方案:文件名加版本号,如 image-v2.jpg
8.2 防盗链配置
为了防止资源被其他网站盗用,可以配置防盗链:
- Referer 白名单:只允许指定域名访问
- 空 Referer 允许:允许浏览器直接访问
- CDN 也可以配置防盗链
8.3 跨域问题
前端访问 OSS 或 CDN 出现跨域错误时,检查:
- OSS 是否配置了 CORS 规则
- CDN 是否配置了跨域头
- 域名是否在允许列表中
九、安全建议
- 使用 RAM 子用户,遵循最小权限原则
- 定期轮换 AccessKey
- 开启服务端加密,保护数据安全
- 配置防盗链,防止资源被盗用
- 私有 Bucket 使用签名 URL 访问
- 开启日志记录,便于审计和问题排查
十、总结
阿里云 OSS + CDN 是一套成熟的静态资源存储和分发方案,具有成本低、速度快、可靠性高等优点,非常适合用于图片、视频、安装包等静态资源的托管。
本文介绍了 OSS 和 CDN 的配置方法、Java SDK 集成、高级功能、成本优化等内容。在实际使用中,建议根据业务需求选择合适的配置,并定期监控使用量和成本。
希望本文能帮助大家快速上手 OSS + CDN 方案。如有问题,欢迎在评论区交流讨论。