后端: - ORDER BY 白名单(sanitizeOrder)堵住 ?order= SQL 注入,补回归测试 - 登录限速(每 IP 10 次失败/10 分钟 429)、TLS/反代下 Secure cookie、NewAPI 构造器 - Delete/setTags/MergeTags/DeleteTag 包事务;Archive 去 500 篇上限 - 列表接口裁剪:不传 content_md,长文 content_html 截 600,新增 content_len;health 探 DB 前端: - EditorView 路由复用串写修复(RouterView :key + sync watch 回写原文章) - v-html 出口统一过 DOMPurify(sanitizeHtml),stripTags 改 DOMParser - 列表竞态防护(Home/Tag/Posts 请求序号)、TagView 分页修复 - 侧栏接口 30s 缓存去重;one:unauthorized 监听器泄漏修复 - 删 styles.css 498 行重复块;移除 tailwind/marked/vue-tsc 死依赖;CommandPalette a11y 语义
80 lines
1.6 KiB
Go
80 lines
1.6 KiB
Go
package admin
|
|
|
|
import (
|
|
"net"
|
|
"net/http"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// loginLimiter 限制每个来源 IP 的登录失败次数(滑动窗口),
|
|
// 防止默认/弱口令被在线暴力破解。成功登录后计数清零。
|
|
type loginLimiter struct {
|
|
mu sync.Mutex
|
|
hits map[string][]time.Time
|
|
}
|
|
|
|
const (
|
|
maxLoginFails = 10
|
|
loginWindow = 10 * time.Minute
|
|
)
|
|
|
|
func newLoginLimiter() *loginLimiter {
|
|
return &loginLimiter{hits: make(map[string][]time.Time)}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// isTLS 判断最终用户看到的是不是 HTTPS(含反代 X-Forwarded-Proto)。
|
|
func isTLS(r *http.Request) bool {
|
|
if r.TLS != nil {
|
|
return true
|
|
}
|
|
return r.Header.Get("X-Forwarded-Proto") == "https"
|
|
}
|