26.9(安全审计修复版)
Go 1.27.1 (Gin+GORM) + Vue 3 文件快传服务: - 安全审计全部修复(docs/security-audit-2026-09-05.md): bcrypt 密码哈希与自动升级、presign 直传服务端大小/内容校验、 全局请求体上限、依赖升级(govulncheck 0 命中)、janitor 后台清理、 管理端审计动作落库、/admin CORS 收紧、通知内容白名单净化、 会话默认 7 天、限流缓存故障降级、robots.txt 端点等 - 前端:取件链接复制修复(不再重复拼接提取码)、markdown 净化器加固 - Redis 支持库号(FCB_REDIS_DB / redis://…/db URL) - 文档:docs/api/* 与 openapi.yaml 同步最新行为(robots.txt、 提码 5 位起、chunk 32MiB 上限、admin 审计动作等) 验证:gofmt/go vet/go test 全绿;二进制端到端冒烟通过
This commit is contained in:
Vendored
+117
@@ -0,0 +1,117 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
// RedisCache 基于 Redis 的缓存实现(可选增强)。
|
||||
type RedisCache struct {
|
||||
client *redis.Client
|
||||
}
|
||||
|
||||
// NewRedis 连接 Redis 并校验可用性。addr 支持两种形式:
|
||||
// - host:port(纯地址,库号由 db 参数指定)
|
||||
// - redis://[:password@]host:port[/db](URL 形式,URL 中的库号优先于 db 参数)
|
||||
func NewRedis(ctx context.Context, addr string, db int) (*RedisCache, error) {
|
||||
opts, err := buildRedisOptions(addr, db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
client := redis.NewClient(opts)
|
||||
if err := client.Ping(ctx).Err(); err != nil {
|
||||
_ = client.Close()
|
||||
return nil, fmt.Errorf("cache: Redis 连接失败 %s: %w", addr, err)
|
||||
}
|
||||
return &RedisCache{client: client}, nil
|
||||
}
|
||||
|
||||
// buildRedisOptions 构造 go-redis 连接选项(纯地址 / URL 形式统一入口)。
|
||||
func buildRedisOptions(addr string, db int) (*redis.Options, error) {
|
||||
opts := &redis.Options{
|
||||
Addr: addr,
|
||||
DB: db,
|
||||
DialTimeout: 5 * time.Second,
|
||||
ReadTimeout: 3 * time.Second,
|
||||
WriteTimeout: 3 * time.Second,
|
||||
PoolSize: 32,
|
||||
}
|
||||
if strings.HasPrefix(addr, "redis://") || strings.HasPrefix(addr, "rediss://") {
|
||||
u, err := redis.ParseURL(addr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cache: Redis 地址解析失败 %s: %w", addr, err)
|
||||
}
|
||||
// URL 未显式携带库号(路径为空或 /)时用 db 参数;显式 /N 优先
|
||||
if u.DB == 0 && !urlHasDBPath(addr) {
|
||||
u.DB = db
|
||||
}
|
||||
u.DialTimeout = opts.DialTimeout
|
||||
u.ReadTimeout = opts.ReadTimeout
|
||||
u.WriteTimeout = opts.WriteTimeout
|
||||
u.PoolSize = opts.PoolSize
|
||||
opts = u
|
||||
}
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
// urlHasDBPath 判断 redis:// URL 是否显式携带了库号路径(如 /5)。
|
||||
func urlHasDBPath(raw string) bool {
|
||||
u, err := url.Parse(raw)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return strings.Trim(u.Path, "/") != ""
|
||||
}
|
||||
|
||||
// Get 读取键值。
|
||||
func (r *RedisCache) Get(ctx context.Context, key string) (string, error) {
|
||||
val, err := r.client.Get(ctx, key).Result()
|
||||
if err == redis.Nil {
|
||||
return "", ErrNotFound
|
||||
}
|
||||
return val, err
|
||||
}
|
||||
|
||||
// Set 写入键值。
|
||||
func (r *RedisCache) Set(ctx context.Context, key, value string, ttl time.Duration) error {
|
||||
return r.client.Set(ctx, key, value, ttl).Err()
|
||||
}
|
||||
|
||||
// Delete 删除键。
|
||||
func (r *RedisCache) Delete(ctx context.Context, keys ...string) error {
|
||||
if len(keys) == 0 {
|
||||
return nil
|
||||
}
|
||||
return r.client.Del(ctx, keys...).Err()
|
||||
}
|
||||
|
||||
// Exists 判断键是否存在。
|
||||
func (r *RedisCache) Exists(ctx context.Context, key string) (bool, error) {
|
||||
n, err := r.client.Exists(ctx, key).Result()
|
||||
return n > 0, err
|
||||
}
|
||||
|
||||
// Incr 原子自增;首次创建时设置窗口 TTL。
|
||||
// 使用 Lua 脚本保证 INCR+EXPIRE 原子性,避免多实例下窗口被反复重置。
|
||||
func (r *RedisCache) Incr(ctx context.Context, key string, ttl time.Duration) (int64, error) {
|
||||
var incrScript = redis.NewScript(`
|
||||
local n = redis.call('INCR', KEYS[1])
|
||||
if n == 1 and ARGV[1] ~= '0' then
|
||||
redis.call('PEXPIRE', KEYS[1], ARGV[1])
|
||||
end
|
||||
return n
|
||||
`)
|
||||
ttlMs := int64(0)
|
||||
if ttl > 0 {
|
||||
ttlMs = ttl.Milliseconds()
|
||||
}
|
||||
return incrScript.Run(ctx, r.client, []string{key}, ttlMs).Int64()
|
||||
}
|
||||
|
||||
// Close 关闭 Redis 连接。
|
||||
func (r *RedisCache) Close() error { return r.client.Close() }
|
||||
Reference in New Issue
Block a user