From 0e4ed9b2d9d76503b3c4ba62e18d2a6bd4fef521 Mon Sep 17 00:00:00 2001 From: xuans Date: Wed, 16 Sep 2026 01:28:06 +0800 Subject: [PATCH] =?UTF-8?q?=E5=B0=86=E9=98=BF=E9=87=8C=E4=BA=91=20DNS=20?= =?UTF-8?q?=E6=9D=83=E9=99=90=E9=AA=8C=E8=AF=81=E9=99=90=E5=88=B6=E4=B8=BA?= =?UTF-8?q?=E5=85=AD=E7=A7=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/cli/dns_test.go | 2 +- .../dnsprovider/aliyun_authorization.go | 20 +++++++++++-- .../dnsprovider/aliyun_authorization_test.go | 28 +++++++++++++++++++ .../aliyun_authorization_verify_test.go | 2 +- .../internal/dnsprovider/aliyun_provider.go | 9 ++++++ backend/internal/dnsprovider/tester.go | 2 +- backend/internal/httpapi/dns_api_test.go | 4 +-- backend/internal/httpapi/setup_test.go | 8 +++--- frontend/src/views/Dashboard/Dashboard.js | 2 +- .../src/views/Dashboard/Dashboard.test.js | 6 ++-- frontend/src/views/SetupWizard/SetupWizard.js | 2 +- .../src/views/SetupWizard/SetupWizard.test.js | 6 ++-- .../src/views/SetupWizard/SetupWizard.vue | 2 +- 13 files changed, 73 insertions(+), 20 deletions(-) diff --git a/backend/internal/cli/dns_test.go b/backend/internal/cli/dns_test.go index f9286aa..326fb93 100644 --- a/backend/internal/cli/dns_test.go +++ b/backend/internal/cli/dns_test.go @@ -85,7 +85,7 @@ func TestDNSCheckReportsLiveResultWithoutPersistingConfiguration(t *testing.T) { t.Fatal("权限请求必须设置超时") } remaining := time.Until(deadline) - if remaining < 9*time.Second || remaining > 11*time.Second { + if remaining < 5*time.Second || remaining > 7*time.Second { t.Fatalf("阿里云 CLI 权限验证时限错误:%s", remaining) } report := dnsprovider.AuthorizationReport{Provider: "aliyun", RequestedDomain: name, Verified: outcome == "allowed"} diff --git a/backend/internal/dnsprovider/aliyun_authorization.go b/backend/internal/dnsprovider/aliyun_authorization.go index bdd6d9f..42c7223 100644 --- a/backend/internal/dnsprovider/aliyun_authorization.go +++ b/backend/internal/dnsprovider/aliyun_authorization.go @@ -20,7 +20,7 @@ const ( aliyunDefaultRegion = "cn-hangzhou" aliyunAuthorizationTTL = 600 aliyunAuthorizationTimeout = 30 * time.Second - aliyunAPICallTimeout = 3 * time.Second + aliyunAPICallTimeout = 2 * time.Second ) // AuthorizationDomain 描述一个阿里云 DNS 根域名的授权探测结果。 @@ -54,7 +54,7 @@ type AliyunAuthorizationChecker struct { } func NewAliyunAuthorizationChecker() *AliyunAuthorizationChecker { - return &AliyunAuthorizationChecker{now: time.Now, recordAPI: newAliyunRecordAPI} + return &AliyunAuthorizationChecker{now: time.Now, recordAPI: newAliyunAuthorizationRecordAPI} } func (c *AliyunAuthorizationChecker) Supports(providerID string) bool { @@ -270,6 +270,19 @@ func (c *AliyunAuthorizationChecker) currentTime() time.Time { } func newAliyunClient(credentials map[string]string) (*client.Client, error) { + return newAliyunClientWithRetryOptions(credentials, nil) +} + +// newAliyunAuthorizationClient 为权限探测建立独立的单次调用客户端。 +// 探测流程有严格的阶段时限,不能因 SDK 重试或退避拉长用户等待;证书签发仍使用普通客户端策略。 +func newAliyunAuthorizationClient(credentials map[string]string) (*client.Client, error) { + return newAliyunClientWithRetryOptions(credentials, &dara.RetryOptions{ + Retryable: false, + MaxAttempts: 1, + }) +} + +func newAliyunClientWithRetryOptions(credentials map[string]string, retryOptions *dara.RetryOptions) (*client.Client, error) { cleaned, err := ValidateCredentials(Aliyun, credentials) if err != nil { return nil, err @@ -280,6 +293,9 @@ func newAliyunClient(credentials map[string]string) (*client.Client, error) { if token := cleaned["security_token"]; token != "" { config.SetSecurityToken(token) } + if retryOptions != nil { + config.SetRetryOptions(retryOptions) + } result, err := client.NewClient(config) if err != nil { return nil, fmt.Errorf("创建阿里云 DNS 客户端失败:%w", err) diff --git a/backend/internal/dnsprovider/aliyun_authorization_test.go b/backend/internal/dnsprovider/aliyun_authorization_test.go index 4d35d59..fd662f8 100644 --- a/backend/internal/dnsprovider/aliyun_authorization_test.go +++ b/backend/internal/dnsprovider/aliyun_authorization_test.go @@ -7,6 +7,34 @@ import ( "time" ) +func TestAliyunAuthorizationRecordAPIUsesSingleAttemptWithoutRetry(t *testing.T) { + api, err := newAliyunAuthorizationRecordAPI(scopeCredentials()) + if err != nil { + t.Fatalf("创建阿里云权限探测客户端失败:%v", err) + } + records, ok := api.(aliyunRecords) + if !ok || records.client == nil || records.client.RetryOptions == nil { + t.Fatalf("权限探测必须使用带显式重试策略的专用客户端:%T", api) + } + if records.client.RetryOptions.Retryable || records.client.RetryOptions.MaxAttempts != 1 { + t.Fatalf("权限探测客户端不得自动重试:%+v", records.client.RetryOptions) + } +} + +func TestAliyunACMERecordAPIDoesNotUseAuthorizationRetryPolicy(t *testing.T) { + api, err := newAliyunRecordAPI(scopeCredentials()) + if err != nil { + t.Fatalf("创建阿里云 ACME 客户端失败:%v", err) + } + records, ok := api.(aliyunRecords) + if !ok || records.client == nil { + t.Fatalf("ACME 客户端类型错误:%T", api) + } + if records.client.RetryOptions != nil { + t.Fatalf("ACME 客户端不应复用权限探测专用重试策略:%+v", records.client.RetryOptions) + } +} + func TestAliyunAuthorizationCheckerRechecksEveryRequestAndDetectsRevocation(t *testing.T) { now := time.Date(2026, 9, 14, 12, 0, 0, 0, time.UTC) calls := 0 diff --git a/backend/internal/dnsprovider/aliyun_authorization_verify_test.go b/backend/internal/dnsprovider/aliyun_authorization_verify_test.go index 0b2391c..1012498 100644 --- a/backend/internal/dnsprovider/aliyun_authorization_verify_test.go +++ b/backend/internal/dnsprovider/aliyun_authorization_verify_test.go @@ -207,7 +207,7 @@ func TestAliyunAuthorizationVerifyCleansUpAfterRequestCancellation(t *testing.T) } func TestPermissionVerificationTimeoutForAliyunIsIndependentFromPropagation(t *testing.T) { - if got := PermissionVerificationTimeoutFor(Aliyun); got != 10*time.Second { + if got := PermissionVerificationTimeoutFor(Aliyun); got != 6*time.Second { t.Fatalf("阿里云权限验证时限错误:%s", got) } if got := PermissionVerificationTimeoutFor(Cloudflare); got != 60*time.Second { diff --git a/backend/internal/dnsprovider/aliyun_provider.go b/backend/internal/dnsprovider/aliyun_provider.go index 7ba626e..e6a677f 100644 --- a/backend/internal/dnsprovider/aliyun_provider.go +++ b/backend/internal/dnsprovider/aliyun_provider.go @@ -78,6 +78,15 @@ func newAliyunRecordAPI(credentials map[string]string) (aliyunRecordAPI, error) return aliyunRecords{client: dnsClient}, nil } +// newAliyunAuthorizationRecordAPI 仅供短时权限探测使用,不改变 ACME DNS-01 的客户端策略。 +func newAliyunAuthorizationRecordAPI(credentials map[string]string) (aliyunRecordAPI, error) { + dnsClient, err := newAliyunAuthorizationClient(credentials) + if err != nil { + return nil, err + } + return aliyunRecords{client: dnsClient}, nil +} + // 不使用自动发现托管区的上游适配器,也不沿 CNAME 自动扩大写入范围。 type scopedAliyunProvider struct { scope AliyunScope diff --git a/backend/internal/dnsprovider/tester.go b/backend/internal/dnsprovider/tester.go index 47392dc..39f2fcf 100644 --- a/backend/internal/dnsprovider/tester.go +++ b/backend/internal/dnsprovider/tester.go @@ -40,7 +40,7 @@ const ( // AliyunPermissionVerificationTimeout 限制阿里云账号内的创建、查询和清理闭环。 // 该流程不等待公网 DNS 传播。 - AliyunPermissionVerificationTimeout = 10 * time.Second + AliyunPermissionVerificationTimeout = 6 * time.Second dnsCleanupTimeout = 30 * time.Second defaultDNSPermissionTXTLookupTimeout = 30 * time.Second diff --git a/backend/internal/httpapi/dns_api_test.go b/backend/internal/httpapi/dns_api_test.go index b948052..b9c4119 100644 --- a/backend/internal/httpapi/dns_api_test.go +++ b/backend/internal/httpapi/dns_api_test.go @@ -81,8 +81,8 @@ func TestAdminAliyunDNSTestReportsFastVerificationTimeout(t *testing.T) { }`)) request.Header.Set("Content-Type", "application/json") serveAdminHTTP(server, response, request) - if response.Code != http.StatusOK || !strings.Contains(response.Body.String(), `"verification_timeout_secs":10`) { - t.Fatalf("管理端阿里云 DNS 在线测试未返回 10 秒等待上限:%d %s", response.Code, response.Body.String()) + if response.Code != http.StatusOK || !strings.Contains(response.Body.String(), `"verification_timeout_secs":6`) { + t.Fatalf("管理端阿里云 DNS 在线测试未返回 6 秒等待上限:%d %s", response.Code, response.Body.String()) } } diff --git a/backend/internal/httpapi/setup_test.go b/backend/internal/httpapi/setup_test.go index 88cdb29..33c250e 100644 --- a/backend/internal/httpapi/setup_test.go +++ b/backend/internal/httpapi/setup_test.go @@ -298,14 +298,14 @@ func TestSetupAliyunDNSTestUsesFastAccountAPITimeout(t *testing.T) { if response.Code != http.StatusUnprocessableEntity { t.Fatalf("模拟阿里云 DNS 验证失败应返回 422,实际为 %d:%s", response.Code, response.Body.String()) } - if !strings.Contains(response.Body.String(), `"verification_timeout_secs":10`) { - t.Fatalf("阿里云 DNS 验证失败响应应包含 10 秒等待上限:%s", response.Body.String()) + if !strings.Contains(response.Body.String(), `"verification_timeout_secs":6`) { + t.Fatalf("阿里云 DNS 验证失败响应应包含 6 秒等待上限:%s", response.Body.String()) } if !verifier.verifyHasDeadline { t.Fatal("阿里云 DNS 在线测试应收到带截止时间的上下文") } remaining := verifier.verifyDeadline.Sub(started) - if remaining < 9*time.Second || remaining > 11*time.Second { + if remaining < 5*time.Second || remaining > 7*time.Second { t.Fatalf("阿里云权限验证不应继承 ACME 传播配置:剩余时间=%s", remaining) } } @@ -580,7 +580,7 @@ func TestSetupDNSTestReturnsNotImplementedForUnavailableProvider(t *testing.T) { if strings.Contains(response.Body.String(), "access_key_secret") || strings.Contains(response.Body.String(), "secret") { t.Fatal("未开放 Provider 响应不应包含提交的凭据") } - if !strings.Contains(response.Body.String(), `"verification_timeout_secs":10`) { + if !strings.Contains(response.Body.String(), `"verification_timeout_secs":6`) { t.Fatalf("未开放 Provider 响应应包含当前最大等待时间:%s", response.Body.String()) } } diff --git a/frontend/src/views/Dashboard/Dashboard.js b/frontend/src/views/Dashboard/Dashboard.js index 372afc8..01f3975 100644 --- a/frontend/src/views/Dashboard/Dashboard.js +++ b/frontend/src/views/Dashboard/Dashboard.js @@ -1,7 +1,7 @@ import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue"; const defaultDNSVerificationTimeoutSeconds = 60; -const aliyunDNSVerificationTimeoutSeconds = 10; +const aliyunDNSVerificationTimeoutSeconds = 6; function dnsVerificationTimeoutFor(providerID) { return String(providerID || "").trim().toLowerCase() === "aliyun" diff --git a/frontend/src/views/Dashboard/Dashboard.test.js b/frontend/src/views/Dashboard/Dashboard.test.js index ef381fd..e24b73a 100644 --- a/frontend/src/views/Dashboard/Dashboard.test.js +++ b/frontend/src/views/Dashboard/Dashboard.test.js @@ -103,7 +103,7 @@ test("管理设置 DNS 验证显示等待进度,并在结束后停止计时", assert.equal(state.dnsTesting.value, true); assert.equal(state.dnsVerificationElapsedSeconds.value, 0); - assert.equal(state.dnsVerificationTimeoutSeconds.value, 10); + assert.equal(state.dnsVerificationTimeoutSeconds.value, 6); advanceTime(3200); runIntervals(); assert.equal(state.dnsVerificationElapsedSeconds.value, 3); @@ -113,12 +113,12 @@ test("管理设置 DNS 验证显示等待进度,并在结束后停止计时", credential_ref: "secret://test/only", verification_token: "test-token", expires_at: new Date(Date.now() + 60000).toISOString(), - verification_timeout_secs: 10, + verification_timeout_secs: 6, })); await task; assert.equal(state.dnsTesting.value, false); assert.equal(state.dnsVerificationElapsedSeconds.value, 0); - assert.equal(state.dnsVerificationTimeoutSeconds.value, 10); + assert.equal(state.dnsVerificationTimeoutSeconds.value, 6); advanceTime(1000); runIntervals(); assert.equal(state.dnsVerificationElapsedSeconds.value, 0); diff --git a/frontend/src/views/SetupWizard/SetupWizard.js b/frontend/src/views/SetupWizard/SetupWizard.js index a34a928..c9e21cc 100644 --- a/frontend/src/views/SetupWizard/SetupWizard.js +++ b/frontend/src/views/SetupWizard/SetupWizard.js @@ -1,7 +1,7 @@ import { computed, onMounted, onUnmounted, ref, watch } from "vue"; const defaultDNSVerificationTimeoutSeconds = 60; -const aliyunDNSVerificationTimeoutSeconds = 10; +const aliyunDNSVerificationTimeoutSeconds = 6; function dnsVerificationTimeoutFor(providerID) { return String(providerID || "").trim().toLowerCase() === "aliyun" diff --git a/frontend/src/views/SetupWizard/SetupWizard.test.js b/frontend/src/views/SetupWizard/SetupWizard.test.js index de6aae6..a6c57b9 100644 --- a/frontend/src/views/SetupWizard/SetupWizard.test.js +++ b/frontend/src/views/SetupWizard/SetupWizard.test.js @@ -34,7 +34,7 @@ test("初始化 DNS 验证显示等待进度,并采用服务端返回的最长 assert.equal(state.dnsTesting.value, true); assert.equal(state.dnsVerificationElapsedSeconds.value, 0); - assert.equal(state.dnsVerificationTimeoutSeconds.value, 10); + assert.equal(state.dnsVerificationTimeoutSeconds.value, 6); advanceTime(2500); runIntervals(); @@ -45,12 +45,12 @@ test("初始化 DNS 验证显示等待进度,并采用服务端返回的最长 credential_ref: "secret://test/only", verification_token: "test-token", expires_at: new Date(Date.now() + 60000).toISOString(), - verification_timeout_secs: 10, + verification_timeout_secs: 6, })); await task; assert.equal(state.dnsTesting.value, false); assert.equal(state.dnsVerificationElapsedSeconds.value, 0); - assert.equal(state.dnsVerificationTimeoutSeconds.value, 10); + assert.equal(state.dnsVerificationTimeoutSeconds.value, 6); advanceTime(1000); runIntervals(); diff --git a/frontend/src/views/SetupWizard/SetupWizard.vue b/frontend/src/views/SetupWizard/SetupWizard.vue index e9392c8..ce02a0a 100644 --- a/frontend/src/views/SetupWizard/SetupWizard.vue +++ b/frontend/src/views/SetupWizard/SetupWizard.vue @@ -706,7 +706,7 @@ const { :max="form.propagation_timeout_secs" required /> - 默认 10 秒,不能大于最大等待时间。 + 默认 6 秒,不能大于最大等待时间。