缩略图懒生成 + 评论登录限流:时间线不再拉原图
- 新增 /uploads/thumb/{key}?w=:首访从存储端读一次原图,标准库盒均值缩放后
落盘 data/.thumbnail_cache(按内容哈希命名,天然失效);失败写 .failed 冷却
一小时,非图片/超大一律 302 回原图,前端无感。原图路由改用 ServeContent,
补上视频拖动进度条所需的 Range 支持
- 前端 thumbURL/thumbifyHtml 接入两套 UI 的时间线配图、长文封面与短文正文,
并补 loading=lazy;详情页与灯箱仍用原图。/api/site 改为
{settings, uploads_public_base},让前端识别哪些直链属于自家存储
- 后台登录的滑动窗口限速器抽成 internal/ratelimit 共享包(调用面不变),
新增:读者登录失败按 IP 20 次/10 分钟、评论写入按读者 5 条/分钟
(站主豁免,且只计成功写入)
This commit is contained in:
@@ -1,18 +1,17 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"oneblog/internal/ratelimit"
|
||||
)
|
||||
|
||||
// loginLimiter 限制每个来源 IP 的登录失败次数(滑动窗口),
|
||||
// 防止默认/弱口令被在线暴力破解。成功登录后计数清零。
|
||||
type loginLimiter struct {
|
||||
mu sync.Mutex
|
||||
hits map[string][]time.Time
|
||||
}
|
||||
// 实现挪到了 internal/ratelimit(读者登录/评论写入共用),这里保留
|
||||
// 原有调用面与常量。
|
||||
type loginLimiter = ratelimit.Window
|
||||
|
||||
const (
|
||||
maxLoginFails = 10
|
||||
@@ -20,55 +19,10 @@ const (
|
||||
)
|
||||
|
||||
func newLoginLimiter() *loginLimiter {
|
||||
return &loginLimiter{hits: make(map[string][]time.Time)}
|
||||
return ratelimit.New(maxLoginFails, loginWindow)
|
||||
}
|
||||
|
||||
func (l *loginLimiter) blocked(key string) bool {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
return len(l.recent(key, time.Now())) >= maxLoginFails
|
||||
}
|
||||
|
||||
func (l *loginLimiter) fail(key string) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
now := time.Now()
|
||||
l.hits[key] = append(l.recent(key, now), now)
|
||||
}
|
||||
|
||||
func (l *loginLimiter) reset(key string) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
delete(l.hits, key)
|
||||
}
|
||||
|
||||
// recent 返回窗口内的失败时间;调用方必须持有 l.mu。
|
||||
func (l *loginLimiter) recent(key string, now time.Time) []time.Time {
|
||||
hs := l.hits[key]
|
||||
cut := now.Add(-loginWindow)
|
||||
i := 0
|
||||
for ; i < len(hs); i++ {
|
||||
if hs[i].After(cut) {
|
||||
break
|
||||
}
|
||||
}
|
||||
if i > 0 {
|
||||
hs = hs[i:]
|
||||
l.hits[key] = hs
|
||||
}
|
||||
if len(hs) == 0 {
|
||||
delete(l.hits, key)
|
||||
}
|
||||
return hs
|
||||
}
|
||||
|
||||
func sourceKey(r *http.Request) string {
|
||||
// 只信连接层地址;X-Forwarded-For 可被伪造,不作为限速键。
|
||||
if host, _, err := net.SplitHostPort(r.RemoteAddr); err == nil {
|
||||
return host
|
||||
}
|
||||
return r.RemoteAddr
|
||||
}
|
||||
func sourceKey(r *http.Request) string { return ratelimit.SourceKey(r) }
|
||||
|
||||
// isTLS 判断最终用户看到的是不是 HTTPS(含反代 X-Forwarded-Proto)。
|
||||
func isTLS(r *http.Request) bool {
|
||||
|
||||
Reference in New Issue
Block a user