安全加固 + 结构清理:修注入/串写/竞态,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:
@@ -8,6 +8,7 @@ import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"oneblog/internal/config"
|
||||
"oneblog/internal/httpx"
|
||||
@@ -19,6 +20,25 @@ type API struct {
|
||||
Store *store.Store
|
||||
Cfg *config.Config
|
||||
Sessions *Sessions
|
||||
|
||||
loginOnce sync.Once
|
||||
logins *loginLimiter
|
||||
}
|
||||
|
||||
func NewAPI(st *store.Store, cfg *config.Config, sessions *Sessions) *API {
|
||||
a := &API{Store: st, Cfg: cfg, Sessions: sessions}
|
||||
a.limiter()
|
||||
return a
|
||||
}
|
||||
|
||||
// limiter 惰性初始化,兼容测试里的 &API{...} 零值构造。
|
||||
func (a *API) limiter() *loginLimiter {
|
||||
a.loginOnce.Do(func() {
|
||||
if a.logins == nil {
|
||||
a.logins = newLoginLimiter()
|
||||
}
|
||||
})
|
||||
return a.logins
|
||||
}
|
||||
|
||||
const cookieName = "one_session"
|
||||
@@ -81,6 +101,11 @@ func (a *API) login(w http.ResponseWriter, r *http.Request) {
|
||||
httpx.Error(w, http.StatusMethodNotAllowed, "POST required")
|
||||
return
|
||||
}
|
||||
key := sourceKey(r)
|
||||
if a.limiter().blocked(key) {
|
||||
httpx.Error(w, http.StatusTooManyRequests, "失败次数过多,请 10 分钟后再试")
|
||||
return
|
||||
}
|
||||
var in loginRequest
|
||||
if err := httpx.Decode(r, &in); err != nil {
|
||||
httpx.BadRequest(w, "invalid body")
|
||||
@@ -89,15 +114,18 @@ func (a *API) login(w http.ResponseWriter, r *http.Request) {
|
||||
userOK := subtle.ConstantTimeCompare([]byte(in.Username), []byte(a.Cfg.AdminUser)) == 1
|
||||
passOK := subtle.ConstantTimeCompare([]byte(in.Password), []byte(a.Cfg.AdminPass)) == 1
|
||||
if !userOK || !passOK {
|
||||
a.limiter().fail(key)
|
||||
httpx.Unauthorized(w)
|
||||
return
|
||||
}
|
||||
a.limiter().reset(key)
|
||||
token, exp := a.Sessions.Issue(a.Cfg.AdminUser)
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: cookieName,
|
||||
Value: token,
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
Secure: isTLS(r),
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
Expires: exp,
|
||||
MaxAge: a.Sessions.TTL(),
|
||||
@@ -113,6 +141,7 @@ func (a *API) logout(w http.ResponseWriter, r *http.Request) {
|
||||
Value: "",
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
Secure: isTLS(r),
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
MaxAge: -1,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user