From f9dcb377638058100aee655a325f1444dc25c728 Mon Sep 17 00:00:00 2001 From: xuans Date: Wed, 30 Sep 2026 12:09:29 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E5=85=85DNS=E8=BF=87=E6=9C=9F?= =?UTF-8?q?=E5=87=AD=E6=8D=AE=E6=B8=85=E7=90=86=E9=87=8D=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复DNS验证会话过期后临时凭据清理失败的可恢复性,保留原会话以便重试且不恢复过期授权,补充回归测试并更新四份整改清单。 --- backend/internal/setup/dns_verification.go | 55 ++++++++++++++++--- .../internal/setup/dns_verification_test.go | 30 ++++++++++ doc/Scert开发任务清单.md | 11 ++++ doc/Scert整改任务执行清单.md | 11 ++++ doc/Scert整改合并优化清单.md | 11 ++++ doc/基础整改任务规格.md | 10 ++++ 6 files changed, 119 insertions(+), 9 deletions(-) diff --git a/backend/internal/setup/dns_verification.go b/backend/internal/setup/dns_verification.go index 48b2f7a..6e31cf5 100644 --- a/backend/internal/setup/dns_verification.go +++ b/backend/internal/setup/dns_verification.go @@ -79,6 +79,11 @@ type dnsVerificationSession struct { ExpiresAt time.Time } +type expiredDNSVerificationSession struct { + Token string + Session dnsVerificationSession +} + // DNSVerificationSession 仅供服务端完整初始化事务读取验证结果。 // 所有字段均禁止 JSON 序列化,避免凭据引用被误写入 HTTP 响应。 type DNSVerificationSession struct { @@ -89,20 +94,21 @@ type DNSVerificationSession struct { } type DNSVerificationManager struct { - mu sync.Mutex - tester DNSCredentialTester - store SecretWriter - deleter SecretDeleter - now func() time.Time - sessions map[string]dnsVerificationSession - inFlight int + mu sync.Mutex + tester DNSCredentialTester + store SecretWriter + deleter SecretDeleter + now func() time.Time + sessions map[string]dnsVerificationSession + expiredSession map[string]expiredDNSVerificationSession + inFlight int } func NewDNSVerificationManager(tester DNSCredentialTester, store SecretWriter) *DNSVerificationManager { deleter, _ := store.(SecretDeleter) return &DNSVerificationManager{ tester: tester, store: store, deleter: deleter, now: time.Now, - sessions: make(map[string]dnsVerificationSession), + sessions: make(map[string]dnsVerificationSession), expiredSession: make(map[string]expiredDNSVerificationSession), } } @@ -396,7 +402,10 @@ func (m *DNSVerificationManager) clearExpiredExceptLocked(now time.Time, exclude } if !session.ExpiresAt.After(now) { delete(m.sessions, token) - references = append(references, session.CredentialRef) + if reference := strings.TrimSpace(session.CredentialRef); reference != "" { + m.expiredSession[reference] = expiredDNSVerificationSession{Token: token, Session: session} + references = append(references, reference) + } } } return references @@ -416,11 +425,39 @@ func (m *DNSVerificationManager) restoreSession(token string, session dnsVerific func (m *DNSVerificationManager) deleteCredentials(references []string) { for _, reference := range references { if err := m.deleteCredential(reference); err != nil { + m.restoreExpiredSession(reference) m.logCleanupFailure() + continue } + m.forgetExpiredSession(reference) } } +func (m *DNSVerificationManager) restoreExpiredSession(reference string) { + if m == nil || strings.TrimSpace(reference) == "" { + return + } + m.mu.Lock() + defer m.mu.Unlock() + expired, ok := m.expiredSession[reference] + if !ok { + return + } + if _, exists := m.sessions[expired.Token]; !exists { + m.sessions[expired.Token] = expired.Session + } + delete(m.expiredSession, reference) +} + +func (m *DNSVerificationManager) forgetExpiredSession(reference string) { + if m == nil || strings.TrimSpace(reference) == "" { + return + } + m.mu.Lock() + delete(m.expiredSession, reference) + m.mu.Unlock() +} + func (m *DNSVerificationManager) logCleanupFailure() { log.Print("DNS 临时凭据回收失败") } diff --git a/backend/internal/setup/dns_verification_test.go b/backend/internal/setup/dns_verification_test.go index c448dc4..427be63 100644 --- a/backend/internal/setup/dns_verification_test.go +++ b/backend/internal/setup/dns_verification_test.go @@ -546,3 +546,33 @@ func TestDNSVerificationResolveIsServerOnlyAndDoesNotConsumeToken(t *testing.T) t.Fatal("已消费的 DNS 验证令牌仍可被解析") } } + +func TestDNSVerificationManagerRetriesExpiredCredentialCleanup(t *testing.T) { + store := &retryableSecretWriter{failDelete: true} + manager := NewDNSVerificationManager(fakeDNSTester{}, store) + now := time.Date(2026, 9, 30, 12, 0, 0, 0, time.UTC) + manager.now = func() time.Time { return now } + result, err := manager.Verify(t.Context(), DNSVerificationInput{ + ProviderID: "cloudflare", TestDomain: "example.com", + Credentials: map[string]string{"api_token": "secret"}, + }) + if err != nil { + t.Fatal(err) + } + + now = now.Add(dnsVerificationLifetime + time.Second) + if err := manager.Check(result.VerificationToken, "cloudflare", result.CredentialRef, "example.com"); err == nil { + t.Fatal("expired DNS verification token should be rejected") + } + if len(manager.sessions) != 1 || len(store.values) != 1 { + t.Fatalf("failed expired cleanup should remain retryable: sessions=%d values=%d", len(manager.sessions), len(store.values)) + } + + store.failDelete = false + if err := manager.Check(result.VerificationToken, "cloudflare", result.CredentialRef, "example.com"); err == nil { + t.Fatal("expired DNS verification token should remain rejected after cleanup retry") + } + if len(manager.sessions) != 0 || len(store.values) != 0 { + t.Fatalf("expired cleanup retry left temporary state: sessions=%d values=%d", len(manager.sessions), len(store.values)) + } +} diff --git a/doc/Scert开发任务清单.md b/doc/Scert开发任务清单.md index b8b26bf..44e953b 100644 --- a/doc/Scert开发任务清单.md +++ b/doc/Scert开发任务清单.md @@ -3769,3 +3769,14 @@ Linux Ubuntu 22.04 上的 `Scert-admin` 手动启停、无开机自启动、无 - [~] 进行中:DNS 过期清理与初始化回滚矩阵、其他 P0-10 任务链、跨数据库实机验证和统一复审。 - [ ] 未开始:P0-12、真实数据库清理初始化、密码 SSH、远端多服务器部署及最终生产门禁。 - [!] 不得标记 `[✓]`,总体验收仍为 **P0-03:完成,待复审**。 + +## 本轮开发进度(2026-09-30,P0-10 DNS 过期凭据清理重试) + +总体验收继续保持 **P0-03:完成,待复审**,本轮继续先开发、后统一测试。 + +- [x] 已完成,等待复审:过期 DNS 验证会话清理失败时恢复原会话,后续操作可重试删除临时凭据。 +- [x] 已完成,等待复审:清理重试成功后仍拒绝过期令牌,避免凭据回收修复引入授权回退。 +- [x] 已完成:新增过期清理失败回归测试;后端全量测试、静态检查和差异检查均通过。 +- [~] 进行中:DNS 初始化回滚矩阵、令牌生命周期、其他 P0-10 任务链、跨数据库实机验证和统一复审。 +- [ ] 未开始:P0-12、真实数据库清理初始化、密码 SSH、远端多服务器部署及最终生产门禁。 +- [!] 不得标记 `[✓]`,总体验收仍为 **P0-03:完成,待复审**。 diff --git a/doc/Scert整改任务执行清单.md b/doc/Scert整改任务执行清单.md index 9a78270..c752a52 100644 --- a/doc/Scert整改任务执行清单.md +++ b/doc/Scert整改任务执行清单.md @@ -1074,3 +1074,14 @@ P0-05 从 `[!]` 调整为 `[~]`,并未标记完成;P0-04 剩余门禁和全 - [~] 继续执行:DNS 过期清理、初始化回滚、令牌生命周期、任务链、三数据库实机矩阵和统一复审。 - [ ] 尚未执行:P0-12、真实生产数据库清理、密码 SSH、远端部署、真实 HTTPS/DNS/ACME 和最终生产门禁。 - [!] 不新增 `[✓]`;总体验收继续为 **P0-03:完成,待复审**。 + +## 本轮执行记录(2026-09-30,P0-10 DNS 过期凭据清理重试) + +本轮按“先开发、后统一测试”执行,总体验收仍保持 **P0-03:完成,待复审**。 + +- [x] 过期 DNS 会话删除临时凭据失败时恢复原会话,避免清理失败造成不可重试残留。 +- [x] 清理重试成功后令牌仍不可用,验证会话不会因补偿清理重新获得授权。 +- [x] `go test ./...`、`go vet ./...`、`git diff --check` 已通过;本轮仅完成本地开发与自动化回归。 +- [~] 继续执行:DNS 初始化回滚、令牌消费、任务链、三数据库实机矩阵和统一复审。 +- [ ] 尚未执行:P0-12、真实生产数据库清理、密码 SSH、远端部署、真实 HTTPS/DNS/ACME 和最终生产门禁。 +- [!] 不新增 `[✓]`;总体验收继续为 **P0-03:完成,待复审**。 diff --git a/doc/Scert整改合并优化清单.md b/doc/Scert整改合并优化清单.md index df80d3c..ca6fb32 100644 --- a/doc/Scert整改合并优化清单.md +++ b/doc/Scert整改合并优化清单.md @@ -1722,3 +1722,14 @@ P0-11 生产验收门禁 - [~] 进行中:DNS 过期清理、初始化回滚、令牌消费、其他 P0-10 任务链、跨数据库实机矩阵和统一复审。 - [ ] 未开始:P0-12 备份恢复、密码 SSH、远端部署、真实 HTTPS/DNS/ACME 和最终生产门禁。 - [!] 总体验收不得升级:仍为 **P0-03:完成,待复审**,本轮没有新增 `[✓]` 复审通过项。 + +## 本轮开发进度(2026-09-30,P0-10 DNS 过期凭据清理重试) + +总体验收位置继续保持 **P0-03:完成,待复审**;本轮仅补齐 DNS 验证令牌过期后的临时凭据清理重试,不代表 P0-10 统一复审通过或生产部署验收通过。 + +- [x] 已完成,等待复审:过期验证会话对应的临时凭据删除失败时恢复原会话,保留后续清理入口,不再静默丢失。 +- [x] 已完成,等待复审:清理重试成功后过期会话和临时凭据同时移除,令牌仍保持过期不可用。 +- [x] 已完成:新增过期清理失败与重试回归测试;后端 `go test ./...`、`go vet ./...` 和 `git diff --check` 通过。 +- [~] 进行中:DNS 初始化回滚、令牌消费、其他 P0-10 任务链、跨数据库实机矩阵和统一复审。 +- [ ] 未开始:P0-12 备份恢复、密码 SSH、远端部署、真实 HTTPS/DNS/ACME 和最终生产门禁。 +- [!] 总体验收不得升级:仍为 **P0-03:完成,待复审**,本轮没有新增 `[✓]` 复审通过项。 diff --git a/doc/基础整改任务规格.md b/doc/基础整改任务规格.md index cb440d0..0fa70f6 100644 --- a/doc/基础整改任务规格.md +++ b/doc/基础整改任务规格.md @@ -647,3 +647,13 @@ P0-06 与 P0-07 均维持 `[~]`,六阶段中的隔离、生产和最终验收 - [x] 已新增对应回归测试,并通过后端全量测试、静态检查和差异检查。 - [~] DNS 过期清理、初始化回滚、令牌消费及 P0-10 其他任务链继续开发和统一复审。 - [!] 本地自动化结果不得替代真实 DNS/ACME、密码 SSH、远端多服务器部署和最终生产门禁;不得新增 `[✓]`。 + +## 本轮规格执行记录(2026-09-30,P0-10 DNS 过期凭据清理重试) + +本轮仅补齐 DNS 验证令牌过期后的临时凭据清理重试要求,总体验收继续保持 **P0-03:完成,待复审**。 + +- [x] 过期会话临时凭据清理失败时必须恢复原会话,后续操作必须能够再次触发清理。 +- [x] 补偿清理成功后必须保持令牌过期,不得因恢复清理状态而重新授权验证结果。 +- [x] 已新增对应回归测试,并通过后端全量测试、静态检查和差异检查。 +- [~] DNS 初始化回滚、令牌消费及 P0-10 其他任务链继续开发和统一复审。 +- [!] 本地自动化结果不得替代真实 DNS/ACME、密码 SSH、远端多服务器部署和最终生产门禁;不得新增 `[✓]`。