feat(cache): 新增缓存清理 API

支持通过 HTTP API 主动清理代理缓存,提供精确路径和通配符模式清理。
包含 IP 白名单和 Token 认证支持。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
xfy 2026-04-09 17:58:18 +08:00
parent 612208616c
commit ce43d5c1d6
3 changed files with 390 additions and 0 deletions

329
internal/cache/purge.go vendored Normal file
View File

@ -0,0 +1,329 @@
// Package cache 提供文件缓存和代理缓存功能,支持 LRU 淘汰和缓存锁防击穿。
//
// 该文件实现了缓存清理 API用于主动清理代理缓存。
//
// 主要功能:
// - 精确路径清理:删除指定路径的缓存条目
// - 通配符模式清理:按模式批量删除缓存条目
// - IP 白名单访问控制
// - Token 认证支持
//
// 作者xfy
package cache
import (
"encoding/json"
"hash/fnv"
"net"
"strings"
"github.com/valyala/fasthttp"
"rua.plus/lolly/internal/config"
)
// PurgeAPI 缓存清理 API 处理器。
//
// 提供 HTTP API 用于主动清理代理缓存,支持精确路径和通配符模式清理。
//
// 注意事项:
// - 所有方法均为并发安全
// - 支持 IP 白名单和 Token 认证
// - 仅处理 POST 请求
type PurgeAPI struct {
// cache 代理缓存实例
cache *ProxyCache
// allowed 允许访问的 IP 网络列表
allowed []net.IPNet
// auth 认证配置
auth config.CacheAPIAuthConfig
// path API 端点路径
path string
}
// PurgeRequest 清理请求结构。
type PurgeRequest struct {
// Path 精确路径
Path string `json:"path,omitempty"`
// Pattern 通配符模式(支持 * 通配符)
Pattern string `json:"pattern,omitempty"`
}
// PurgeResponse 清理响应结构。
type PurgeResponse struct {
// Deleted 被删除的缓存条目数
Deleted int `json:"deleted"`
}
// PurgeErrorResponse 错误响应结构。
type PurgeErrorResponse struct {
// Error 错误信息
Error string `json:"error"`
}
// NewPurgeAPI 创建缓存清理 API 处理器。
//
// 参数:
// - cache: 代理缓存实例
// - cfg: 缓存 API 配置
//
// 返回值:
// - *PurgeAPI: 配置好的处理器
// - error: IP 解析失败时返回非 nil 错误
func NewPurgeAPI(cache *ProxyCache, cfg *config.CacheAPIConfig) (*PurgeAPI, error) {
p := &PurgeAPI{
cache: cache,
auth: cfg.Auth,
path: cfg.Path,
}
// 解析允许的 IP 列表
for _, cidr := range cfg.Allow {
_, network, err := net.ParseCIDR(cidr)
if err != nil {
// 尝试作为单个 IP 解析
ip := net.ParseIP(cidr)
if ip == nil {
return nil, err
}
// 转换为 CIDR 格式
if ip.To4() != nil {
_, network, _ = net.ParseCIDR(cidr + "/32")
} else {
_, network, _ = net.ParseCIDR(cidr + "/128")
}
}
if network != nil {
p.allowed = append(p.allowed, *network)
}
}
return p, nil
}
// Path 返回 API 端点路径。
func (p *PurgeAPI) Path() string {
if p.path == "" {
return "/_cache/purge"
}
return p.path
}
// ServeHTTP 处理缓存清理请求。
//
// 仅处理 POST 请求,支持精确路径和通配符模式清理。
// 返回 JSON 格式的响应。
func (p *PurgeAPI) ServeHTTP(ctx *fasthttp.RequestCtx) {
// 仅允许 POST 方法
if string(ctx.Method()) != "POST" {
p.sendError(ctx, fasthttp.StatusMethodNotAllowed, "method not allowed")
return
}
// 检查 IP 访问权限
if !p.checkAccess(ctx) {
p.sendError(ctx, fasthttp.StatusForbidden, "forbidden")
return
}
// 检查认证
if !p.checkAuth(ctx) {
p.sendError(ctx, fasthttp.StatusUnauthorized, "unauthorized")
return
}
// 解析请求体
var req PurgeRequest
if err := json.Unmarshal(ctx.PostBody(), &req); err != nil {
p.sendError(ctx, fasthttp.StatusBadRequest, "invalid request body")
return
}
// 执行清理
deleted := 0
if req.Path != "" {
deleted = p.purgeByPath(req.Path)
} else if req.Pattern != "" {
deleted = p.purgeByPattern(req.Pattern)
} else {
p.sendError(ctx, fasthttp.StatusBadRequest, "missing path or pattern")
return
}
// 返回响应
ctx.SetContentType("application/json; charset=utf-8")
ctx.SetStatusCode(fasthttp.StatusOK)
json.NewEncoder(ctx).Encode(PurgeResponse{Deleted: deleted}) //nolint:errcheck
}
// checkAccess 检查客户端 IP 是否在允许列表中。
func (p *PurgeAPI) checkAccess(ctx *fasthttp.RequestCtx) bool {
// 如果没有配置允许列表,允许所有访问
if len(p.allowed) == 0 {
return true
}
clientIP := p.getClientIP(ctx)
if clientIP == nil {
return false
}
// 检查是否在允许列表中
for _, network := range p.allowed {
if network.Contains(clientIP) {
return true
}
}
return false
}
// checkAuth 检查认证。
func (p *PurgeAPI) checkAuth(ctx *fasthttp.RequestCtx) bool {
// 无需认证
if p.auth.Type == "" || p.auth.Type == "none" {
return true
}
// Token 认证
if p.auth.Type == "token" {
// 从 Authorization header 获取 token
authHeader := ctx.Request.Header.Peek("Authorization")
if len(authHeader) == 0 {
return false
}
// 支持 Bearer token 格式
authStr := string(authHeader)
if token, ok := strings.CutPrefix(authStr, "Bearer "); ok {
return token == p.auth.Token
}
// 也支持直接传递 token
return authStr == p.auth.Token
}
return false
}
// getClientIP 从请求上下文提取客户端 IP。
func (p *PurgeAPI) getClientIP(ctx *fasthttp.RequestCtx) net.IP {
// 检查 X-Forwarded-For 头部
if xff := ctx.Request.Header.Peek("X-Forwarded-For"); len(xff) > 0 {
ips := strings.Split(string(xff), ",")
if len(ips) > 0 {
ipStr := strings.TrimSpace(ips[0])
ip := net.ParseIP(ipStr)
if ip != nil {
return ip
}
}
}
// 检查 X-Real-IP 头部
if xri := ctx.Request.Header.Peek("X-Real-IP"); len(xri) > 0 {
ip := net.ParseIP(string(xri))
if ip != nil {
return ip
}
}
// 使用 RemoteAddr
if addr := ctx.RemoteAddr(); addr != nil {
if tcpAddr, ok := addr.(*net.TCPAddr); ok {
return tcpAddr.IP
}
}
return nil
}
// purgeByPath 按精确路径清理缓存。
func (p *PurgeAPI) purgeByPath(path string) int {
if p.cache == nil {
return 0
}
// 计算缓存键的哈希值
hashKey := hashPath(path)
// 尝试删除
p.cache.mu.Lock()
defer p.cache.mu.Unlock()
if _, ok := p.cache.entries[hashKey]; ok {
delete(p.cache.entries, hashKey)
return 1
}
return 0
}
// purgeByPattern 按通配符模式清理缓存。
func (p *PurgeAPI) purgeByPattern(pattern string) int {
if p.cache == nil {
return 0
}
p.cache.mu.Lock()
defer p.cache.mu.Unlock()
deleted := 0
for hashKey, entry := range p.cache.entries {
if matchPattern(pattern, entry.OrigKey) {
delete(p.cache.entries, hashKey)
deleted++
}
}
return deleted
}
// hashPath 使用 FNV-64a 计算路径的哈希值。
// 与代理层 buildCacheKeyHash 使用相同的算法,确保一致性。
// 注意:代理层的 key 格式为 "METHOD:URI"purge 时默认使用 GET 方法。
func hashPath(path string) uint64 {
// 默认使用 GET 方法,与代理层 key 格式一致
key := "GET:" + path
h := fnv.New64a()
h.Write([]byte(key))
return h.Sum64()
}
// matchPattern 检查路径是否匹配通配符模式。
// 仅支持 * 通配符,匹配任意字符。
func matchPattern(pattern, path string) bool {
// 特殊情况:* 匹配所有
if pattern == "*" {
return true
}
// 检查是否有通配符
if !strings.Contains(pattern, "*") {
return path == pattern
}
// 简单的前缀匹配:/api/users/* 匹配 /api/users/123
if prefix, ok := strings.CutSuffix(pattern, "*"); ok {
return strings.HasPrefix(path, prefix)
}
// 中间通配符:/api/*/users 匹配 /api/v1/users
parts := strings.Split(pattern, "*")
if len(parts) == 2 {
return strings.HasPrefix(path, parts[0]) && strings.HasSuffix(path, parts[1])
}
// 复杂模式不支持,返回 false
return false
}
// sendError 发送错误响应。
func (p *PurgeAPI) sendError(ctx *fasthttp.RequestCtx, status int, errMsg string) {
ctx.SetContentType("application/json; charset=utf-8")
ctx.SetStatusCode(status)
json.NewEncoder(ctx).Encode(PurgeErrorResponse{Error: errMsg}) //nolint:errcheck
}

View File

@ -271,6 +271,10 @@ type ServerConfig struct {
// 支持单位b, kb, mb, gb 或纯数字表示字节
// 默认值为 1MB
ClientMaxBodySize string `yaml:"client_max_body_size"`
// CacheAPI 缓存 API 配置
// 用于主动清理代理缓存
CacheAPI *CacheAPIConfig `yaml:"cache_api"`
}
// StaticConfig 静态文件服务配置。
@ -1445,6 +1449,54 @@ type StatusConfig struct {
Format string `yaml:"format"`
}
// CacheAPIConfig 缓存 API 配置。
//
// 配置缓存清理 API 端点,支持主动清理代理缓存。
//
// 注意事项:
// - Enabled 默认为 false需显式启用
// - Allow 限制可访问的 IP 地址列表
// - Auth 配置认证方式,推荐使用 token 认证
//
// 使用示例:
//
// cache_api:
// enabled: true
// path: "/_cache/purge"
// allow: ["127.0.0.1", "10.0.0.0/8"]
// auth:
// type: "token"
// token: "${CACHE_API_TOKEN}"
type CacheAPIConfig struct {
// Enabled 是否启用缓存 API
// 默认为 false
Enabled bool `yaml:"enabled"`
// Path API 端点路径
// 默认为 "/_cache/purge"
Path string `yaml:"path"`
// Allow 允许访问的 IP 列表
// 可访问缓存 API 的 IP 地址或 CIDR
Allow []string `yaml:"allow"`
// Auth 认证配置
Auth CacheAPIAuthConfig `yaml:"auth"`
}
// CacheAPIAuthConfig 缓存 API 认证配置。
type CacheAPIAuthConfig struct {
// Type 认证类型
// 支持 "none" 和 "token" 两种类型
// 默认为 "none"
Type string `yaml:"type"`
// Token 认证令牌
// 当 Type 为 "token" 时使用
// 支持环境变量替换,如 "${CACHE_API_TOKEN}"
Token string `yaml:"token"`
}
// StreamConfig TCP/UDP Stream 代理配置。
//
// 用于四层网络代理如数据库、Redis 等 TCP/UDP 服务。

View File

@ -45,6 +45,15 @@ func DefaultConfig() *Config {
IdleTimeout: 120 * time.Second,
MaxConnsPerIP: 1000,
MaxRequestsPerConn: 10000,
CacheAPI: &CacheAPIConfig{
Enabled: false,
Path: "/_cache/purge",
Allow: []string{"127.0.0.1"},
Auth: CacheAPIAuthConfig{
Type: "none",
Token: "",
},
},
Static: []StaticConfig{{
Path: "/",
Root: "/var/www/html",