名称: cdn-media-delivery
描述: 用于配置CDN进行媒体交付、实现缓存失效或设计签名URL模式。涵盖CDN配置、边缘缓存、起源屏蔽以及头less CMS的安全媒体访问。
允许工具: 读取、全局搜索、grep、任务、技能
CDN媒体交付
为头less CMS架构配置CDN交付、缓存管理和安全媒体访问的指南。
何时使用此技能
- 配置CDN进行媒体交付
- 实现缓存失效策略
- 设置签名/安全URL
- 优化边缘缓存
- 配置起源屏蔽
CDN架构
基本CDN设置
┌─────────────────────────────────────────────────────────────┐
│ 用户 │
│ (全球,地理分布) │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ CDN边缘网络 │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ 边缘 │ │ 边缘 │ │ 边缘 │ │ 边缘 │ │
│ │ 美国西部│ │ 美国东部│ │ 欧洲 │ │ 亚洲 │ │
│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 起源屏蔽 │
│ (可选中间缓存层) │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 起源服务器 │
│ ┌───────────────┐ ┌────────────────┐ ┌───────────────┐ │
│ │ 媒体API │ │ 对象存储 │ │ 图像处理器 │ │
│ │ (转换) │ │ (Azure/S3) │ │ │ │
│ └───────────────┘ └────────────────┘ └───────────────┘ │
└─────────────────────────────────────────────────────────────┘
CDN配置
Azure CDN(Front Door)
// appsettings.json
{
"Cdn": {
"Provider": "AzureFrontDoor",
"Endpoint": "https://media.example.com",
"OriginHost": "storage.blob.core.windows.net",
"CacheRules": {
"Images": {
"CacheDuration": "365.00:00:00",
"QueryStringCaching": "IgnoreQueryString"
},
"Transforms": {
"CacheDuration": "30.00:00:00",
"QueryStringCaching": "UseQueryString"
}
}
}
}
CloudFront配置
public class CloudFrontConfiguration
{
public string DistributionId { get; set; } = string.Empty;
public string DomainName { get; set; } = string.Empty;
public string OriginId { get; set; } = string.Empty;
public CacheBehavior DefaultCacheBehavior { get; set; } = new()
{
ViewerProtocolPolicy = "redirect-to-https",
CachePolicyId = "658327ea-f89d-4fab-a63d-7e88639e58f6", // 缓存优化
Compress = true,
AllowedMethods = new[] { "GET", "HEAD", "OPTIONS" },
CachedMethods = new[] { "GET", "HEAD" }
};
public CacheBehavior[] CacheBehaviors { get; set; } =
{
new()
{
PathPattern = "/media/transform/*",
CachePolicyId = "custom-transform-policy",
QueryStringCaching = QueryStringCaching.All
}
};
}
Cloudflare配置
public class CloudflareConfiguration
{
public string ZoneId { get; set; } = string.Empty;
public string ApiToken { get; set; } = string.Empty;
public PageRule[] PageRules { get; set; } =
{
new()
{
Targets = new[] { "*example.com/media/*" },
Actions = new PageRuleAction
{
CacheLevel = "cache_everything",
EdgeCacheTtl = 2592000, // 30天
BrowserCacheTtl = 86400 // 1天
}
}
};
}
缓存头部
设置缓存头部
public class MediaCacheMiddleware
{
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
await next(context);
if (context.Request.Path.StartsWithSegments("/media"))
{
var cacheControl = GetCacheControl(context.Request.Path);
context.Response.Headers["Cache-Control"] = cacheControl;
context.Response.Headers["Vary"] = "Accept, Accept-Encoding";
}
}
private string GetCacheControl(PathString path)
{
// 原始媒体:缓存1年(不可变内容)
if (path.Value?.Contains("/original/") == true)
{
return "public, max-age=31536000, immutable";
}
// 转换图像:缓存30天
if (path.Value?.Contains("/transform/") == true)
{
return "public, max-age=2592000, stale-while-revalidate=86400";
}
// 默认:1天
return "public, max-age=86400";
}
}
Cache-Control指令
| 指令 |
目的 |
示例 |
public |
允许CDN缓存 |
图像、静态资源 |
private |
仅浏览器缓存 |
用户特定内容 |
max-age |
缓存持续时间(秒) |
max-age=86400(1天) |
immutable |
从不重新验证 |
版本化资源 |
stale-while-revalidate |
服务陈旧内容同时获取新内容 |
后台刷新 |
no-cache |
总是重新验证 |
动态内容 |
no-store |
从不缓存 |
敏感数据 |
缓存失效
失效服务
public interface ICdnInvalidationService
{
Task InvalidatePathAsync(string path);
Task InvalidatePathsAsync(IEnumerable<string> paths);
Task InvalidatePrefixAsync(string prefix);
Task InvalidateAllAsync();
}
// Azure CDN实现
public class AzureCdnInvalidationService : ICdnInvalidationService
{
private readonly CdnManagementClient _cdnClient;
public async Task InvalidatePathAsync(string path)
{
await _cdnClient.Endpoints.PurgeContentAsync(
_resourceGroup,
_profileName,
_endpointName,
new PurgeParameters(new[] { path }));
}
public async Task InvalidatePrefixAsync(string prefix)
{
await _cdnClient.Endpoints.PurgeContentAsync(
_resourceGroup,
_profileName,
_endpointName,
new PurgeParameters(new[] { $"{prefix}/*" }));
}
}
// CloudFront实现
public class CloudFrontInvalidationService : ICdnInvalidationService
{
private readonly AmazonCloudFrontClient _client;
public async Task InvalidatePathAsync(string path)
{
var request = new CreateInvalidationRequest
{
DistributionId = _distributionId,
InvalidationBatch = new InvalidationBatch
{
CallerReference = Guid.NewGuid().ToString(),
Paths = new Paths
{
Items = new List<string> { path },
Quantity = 1
}
}
};
await _client.CreateInvalidationAsync(request);
}
}
基于事件的失效
public class MediaUpdatedHandler : INotificationHandler<MediaUpdatedEvent>
{
private readonly ICdnInvalidationService _cdn;
public async Task Handle(MediaUpdatedEvent notification, CancellationToken ct)
{
// 失效原始内容
await _cdn.InvalidatePathAsync($"/media/{notification.MediaId}");
// 失效所有转换
await _cdn.InvalidatePrefixAsync($"/media/transform/{notification.MediaId}");
}
}
签名URL
签名URL生成
public class SignedUrlService
{
public string GenerateSignedUrl(
string path,
TimeSpan validity,
SignedUrlOptions? options = null)
{
options ??= new SignedUrlOptions();
var expiry = DateTime.UtcNow.Add(validity);
var expiryTimestamp = new DateTimeOffset(expiry).ToUnixTimeSeconds();
// 构建带参数的URL
var urlBuilder = new UriBuilder($"{_cdnBaseUrl}{path}");
var query = HttpUtility.ParseQueryString(urlBuilder.Query);
query["expires"] = expiryTimestamp.ToString();
if (options.AllowedIp != null)
{
query["ip"] = options.AllowedIp;
}
// 生成签名
var signatureData = $"{path}|{expiryTimestamp}|{options.AllowedIp}";
var signature = ComputeSignature(signatureData);
query["signature"] = signature;
urlBuilder.Query = query.ToString();
return urlBuilder.ToString();
}
private string ComputeSignature(string data)
{
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(_signingKey));
var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(data));
return Convert.ToBase64String(hash)
.Replace("+", "-")
.Replace("/", "_")
.TrimEnd('=');
}
}
public class SignedUrlOptions
{
public string? AllowedIp { get; set; }
public string? AllowedCountry { get; set; }
public int? MaxDownloads { get; set; }
}
签名URL验证
public class SignedUrlValidationMiddleware
{
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
if (RequiresSignedUrl(context.Request.Path))
{
var query = context.Request.Query;
// 检查过期
if (!long.TryParse(query["expires"], out var expiry) ||
DateTimeOffset.UtcNow.ToUnixTimeSeconds() > expiry)
{
context.Response.StatusCode = 403;
await context.Response.WriteAsync("URL已过期");
return;
}
// 验证签名
var expectedSignature = ComputeSignature(
context.Request.Path,
expiry,
query["ip"]);
if (query["signature"] != expectedSignature)
{
context.Response.StatusCode = 403;
await context.Response.WriteAsync("无效签名");
return;
}
// 检查IP限制
if (!string.IsNullOrEmpty(query["ip"]))
{
var clientIp = context.Connection.RemoteIpAddress?.ToString();
if (clientIp != query["ip"])
{
context.Response.StatusCode = 403;
await context.Response.WriteAsync("IP不允许");
return;
}
}
}
await next(context);
}
}
起源屏蔽
屏蔽配置
public class OriginShieldConfiguration
{
public bool Enabled { get; set; } = true;
public string ShieldRegion { get; set; } = "us-east-1";
public int ShieldCacheTtl { get; set; } = 3600; // 1小时
public int MaxConnectionsToOrigin { get; set; } = 100;
}
优势
| 特性 |
无屏蔽 |
有屏蔽 |
| 起源请求 |
从每个边缘 |
从一个区域 |
| 缓存效率 |
每边缘 |
共享屏蔽缓存 |
| 起源负载 |
高 |
减少90%以上 |
| 延迟 |
可变 |
可预测 |
CDN URL生成
URL服务
public class CdnUrlService
{
public string GetMediaUrl(MediaItem media, MediaUrlOptions? options = null)
{
options ??= new MediaUrlOptions();
var path = $"/media/{media.StoragePath}";
// 添加转换查询参数
if (options.Width.HasValue || options.Height.HasValue)
{
var query = new List<string>();
if (options.Width.HasValue) query.Add($"w={options.Width}");
if (options.Height.HasValue) query.Add($"h={options.Height}");
if (options.Format.HasValue) query.Add($"format={options.Format}");
if (options.Quality.HasValue) query.Add($"q={options.Quality}");
path += "?" + string.Join("&", query);
}
// 如果私有,生成签名URL
if (media.IsPrivate || options.RequireSignature)
{
return _signedUrlService.GenerateSignedUrl(
path,
options.UrlValidity ?? TimeSpan.FromHours(1));
}
return $"{_cdnBaseUrl}{path}";
}
}
public class MediaUrlOptions
{
public int? Width { get; set; }
public int? Height { get; set; }
public ImageFormat? Format { get; set; }
public int? Quality { get; set; }
public bool RequireSignature { get; set; }
public TimeSpan? UrlValidity { get; set; }
}
性能监控
CDN指标
public class CdnMetrics
{
public long TotalRequests { get; set; }
public long CacheHits { get; set; }
public long CacheMisses { get; set; }
public double CacheHitRatio => (double)CacheHits / TotalRequests;
public long BandwidthBytes { get; set; }
public double AverageLatencyMs { get; set; }
public Dictionary<string, long> RequestsByRegion { get; set; } = new();
public Dictionary<int, long> StatusCodeCounts { get; set; } = new();
}
相关技能
media-asset-management - 媒体存储和组织
image-optimization - CDN前的图像处理
headless-api-design - 媒体API端点