安全加固 + 结构清理:修注入/串写/竞态,DOMPurify 上线,后端补事务与 handler 测试
后端: - 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 语义
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
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"
|
||||
}
|
||||
Reference in New Issue
Block a user