fix: 管理员 IP 锁定不再阻断已认证 Cookie 与 API 令牌
This commit is contained in:
@@ -280,6 +280,82 @@ func TestLoginLock(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestAdminLockDoesNotBlockAuthedSession:密码失败触发 IP 锁后,
|
||||
// 已有 Cookie 会话与合法 API 令牌仍可调管理接口;未认证密码登录仍被拒。
|
||||
func TestAdminLockDoesNotBlockAuthedSession(t *testing.T) {
|
||||
_, srv, cookieClient, _ := setup(t)
|
||||
base := srv.URL
|
||||
|
||||
login(t, cookieClient, base)
|
||||
|
||||
res := postJSON(t, cookieClient, base+"/api/admin/tokens",
|
||||
`{"name":"ops-lock"}`,
|
||||
map[string]string{"X-Nixmsg-Request": "1"})
|
||||
env := decodeEnv(t, res)
|
||||
if res.StatusCode != 200 || !env.OK {
|
||||
t.Fatalf("create token: %d %+v", res.StatusCode, env)
|
||||
}
|
||||
var created struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
if err := json.Unmarshal(env.Data, &created); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
bad := &http.Client{}
|
||||
res := postJSON(t, bad, base+"/api/admin/login",
|
||||
`{"username":"admin","password":"wrong-password!!"}`, nil)
|
||||
env := decodeEnv(t, res)
|
||||
if i < 9 {
|
||||
if res.StatusCode != 401 {
|
||||
t.Fatalf("fail %d: want 401 got %d %+v", i, res.StatusCode, env)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if res.StatusCode != 429 || env.Error == nil || env.Error.Code != "rate_limited" {
|
||||
t.Fatalf("10th fail want 429 rate_limited got %d %+v", res.StatusCode, env)
|
||||
}
|
||||
}
|
||||
|
||||
res = doReq(t, cookieClient, http.MethodGet, base+"/api/admin/me", "", nil)
|
||||
env = decodeEnv(t, res)
|
||||
if res.StatusCode != 200 || !env.OK {
|
||||
t.Fatalf("cookie me after lock: want 200 got %d %+v", res.StatusCode, env)
|
||||
}
|
||||
var me map[string]any
|
||||
_ = json.Unmarshal(env.Data, &me)
|
||||
if me["auth"] != "cookie" {
|
||||
t.Fatalf("cookie me auth=%v", me["auth"])
|
||||
}
|
||||
|
||||
tokClient := &http.Client{}
|
||||
hdr := map[string]string{"Authorization": "Bearer " + created.Token}
|
||||
res = doReq(t, tokClient, http.MethodGet, base+"/api/admin/me", "", hdr)
|
||||
env = decodeEnv(t, res)
|
||||
if res.StatusCode != 200 || !env.OK {
|
||||
t.Fatalf("token me after lock: want 200 got %d %+v", res.StatusCode, env)
|
||||
}
|
||||
_ = json.Unmarshal(env.Data, &me)
|
||||
if me["auth"] != "token" {
|
||||
t.Fatalf("token me auth=%v", me["auth"])
|
||||
}
|
||||
|
||||
res = doReq(t, tokClient, http.MethodGet, base+"/api/admin/overview", "", hdr)
|
||||
env = decodeEnv(t, res)
|
||||
if res.StatusCode != 200 || !env.OK {
|
||||
t.Fatalf("token overview after lock: want 200 got %d %+v", res.StatusCode, env)
|
||||
}
|
||||
|
||||
anon := &http.Client{}
|
||||
res = postJSON(t, anon, base+"/api/admin/login",
|
||||
`{"username":"admin","password":"`+testPassword+`"}`, nil)
|
||||
env = decodeEnv(t, res)
|
||||
if res.StatusCode != 429 || env.Error == nil || env.Error.Code != "rate_limited" {
|
||||
t.Fatalf("password login while locked want 429 got %d %+v", res.StatusCode, env)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBadAPITokenCountsTowardLock(t *testing.T) {
|
||||
_, srv, _, _ := setup(t)
|
||||
base := srv.URL
|
||||
|
||||
@@ -43,12 +43,8 @@ func (h *Handler) auth(next http.HandlerFunc) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ip := httpx.ClientIP(r, h.trusted)
|
||||
|
||||
if locked, retry := h.locks.Check(auth.LockKey{Kind: auth.LockAdminIP, IP: ip}); locked {
|
||||
w.Header().Set("Retry-After", formatRetryAfter(retry))
|
||||
httpx.WriteError(w, http.StatusTooManyRequests, "rate_limited", "登录已锁定,请稍后再试")
|
||||
return
|
||||
}
|
||||
|
||||
// 锁定只拦密码登录(login.go)与错误令牌试错累计;
|
||||
// 已认证的 Cookie / 合法 API 令牌在锁定期仍可用(对齐 PRD D18)。
|
||||
p, errCode, errMsg, status := h.authenticate(r, ip)
|
||||
if status != 0 {
|
||||
if status == http.StatusTooManyRequests {
|
||||
|
||||
Reference in New Issue
Block a user