CDN媒体交付Skill cdn-media-delivery

这个技能专注于配置和管理内容分发网络(CDN)以优化媒体文件的高效交付。它涵盖CDN架构设置、缓存策略优化、缓存失效实现、签名URL生成与验证、起源屏蔽配置,适用于头less CMS系统。关键词:CDN配置、媒体交付、缓存管理、安全访问、云服务、头less CMS、边缘缓存、签名URL、缓存失效。

云原生架构 0 次安装 0 次浏览 更新于 3/11/2026

名称: 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端点