Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b4da3dc05 |
@@ -1092,3 +1092,12 @@
|
||||
- 原因:同一连接上 `group.create`/`group.add` 同步向本连接注入下行时,与 mochi InlineClient 互相等待,`resp` 回不去(`TestUplinkDMOfflineGroupRecall` 在清掉测试客户端 dial deadline 后稳定复现)。
|
||||
- 备选方案:broker 层对 Inline 发布做无锁队列。
|
||||
- 影响:`group_event` 可能略晚于 `resp` 到达;业务结果仍以 `resp` 为准。
|
||||
|
||||
### fix-issue-1
|
||||
|
||||
1. **管理员 IP 锁定不再阻断已认证会话**
|
||||
- 原条款:PRD D18 / F02(密码锁只拦密码登录,不拦已有会话令牌);DEVELOPMENT 第 5/8 节(管理员登录锁定、错误令牌按 IP 计入锁定);issue #1。
|
||||
- 实际做法:去掉 `internal/admin/auth.go` 的 `auth()` 鉴权前 `Check(LockAdminIP)`;登录入口仍 `Check`/`Fail`,错误或停用 API 令牌仍经 `authFail` 计入锁定。有效 Cookie 与合法 Bearer 在锁定期可继续调管理接口。
|
||||
- 原因:先前把「防暴力登录」扩成「封整个管理面」,同 NAT 下刷错误 Bearer 即可锁死已登录管理员,与端侧 nst_ 重连语义不一致。
|
||||
- 备选方案:锁定期对 Cookie 与令牌也拒绝(否决,违背 D18 对齐)。
|
||||
- 影响:仅管理后台鉴权中间件;端侧登录锁定未改。
|
||||
|
||||
@@ -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