Files
SKYMirror 7f060dd0e4 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 全绿;二进制端到端冒烟通过
2026-09-05 04:22:41 +08:00

118 lines
3.2 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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() }